diff --git a/db/knex_migrations/2025-02-17-2142-generalize-analytics.js b/db/knex_migrations/2025-02-17-2142-generalize-analytics.js new file mode 100644 index 000000000..8c5fda990 --- /dev/null +++ b/db/knex_migrations/2025-02-17-2142-generalize-analytics.js @@ -0,0 +1,23 @@ +// Udpate status_page table to generalize analytics fields +exports.up = function (knex) { + return knex.schema + .alterTable("status_page", function (table) { + table.renameColumn("google_analytics_tag_id", "analytics_id"); + table.string("analytics_script_url"); + table.enu("analytics_type", [ "google", "umami", "plausible", "matomo" ]).defaultTo(null); + + }).then(() => { + // After a succesful migration, add google as default for previous pages + knex("status_page").whereNotNull("analytics_id").update({ + "analytics_type": "google", + }); + }); +}; + +exports.down = function (knex) { + return knex.schema.alterTable("status_page", function (table) { + table.renameColumn("analytics_id", "google_analytics_tag_id"); + table.dropColumn("analytics_script_url"); + table.dropColumn("analytics_type"); + }); +}; diff --git a/server/analytics/analytics.js b/server/analytics/analytics.js new file mode 100644 index 000000000..229d463e0 --- /dev/null +++ b/server/analytics/analytics.js @@ -0,0 +1,48 @@ +const googleAnalytics = require("./google-analytics"); +const umamiAnalytics = require("./umami-analytics"); +const plausibleAnalytics = require("./plausible-analytics"); +const matomoAnalytics = require("./matomo-analytics"); + +/** + * Returns a string that represents the javascript that is required to insert the selected Analytics' script + * into a webpage. + * @param {typeof import("../model/status_page").StatusPage} statusPage Status page populate HTML with + * @returns {string} HTML script tags to inject into page + */ +function getAnalyticsScript(statusPage) { + switch (statusPage.analyticsType) { + case "google": + return googleAnalytics.getGoogleAnalyticsScript(statusPage.analyticsId); + case "umami": + return umamiAnalytics.getUmamiAnalyticsScript(statusPage.analyticsScriptUrl, statusPage.analyticsId); + case "plausible": + return plausibleAnalytics.getPlausibleAnalyticsScript(statusPage.analyticsScriptUrl, statusPage.analyticsId); + case "matomo": + return matomoAnalytics.getMatomoAnalyticsScript(statusPage.analyticsScriptUrl, statusPage.analyticsId); + default: + return null; + } +} + +/** + * Function that checks wether the selected analytics has been configured properly + * @param {typeof import("../model/status_page").StatusPage} statusPage Status page populate HTML with + * @returns {boolean} Boolean defining if the analytics config is valid + */ +function isValidAnalyticsConfig(statusPage) { + switch (statusPage.analyticsType) { + case "google": + return statusPage.analyticsId != null; + case "umami": + case "plausible": + case "matomo": + return statusPage.analyticsId != null && statusPage.analyticsScriptUrl != null; + default: + return false; + } +} + +module.exports = { + getAnalyticsScript, + isValidAnalyticsConfig +}; diff --git a/server/google-analytics.js b/server/analytics/google-analytics.js similarity index 100% rename from server/google-analytics.js rename to server/analytics/google-analytics.js diff --git a/server/analytics/matomo-analytics.js b/server/analytics/matomo-analytics.js new file mode 100644 index 000000000..fdc009e63 --- /dev/null +++ b/server/analytics/matomo-analytics.js @@ -0,0 +1,47 @@ +const jsesc = require("jsesc"); +const { escape } = require("html-escaper"); + +/** + * Returns a string that represents the javascript that is required to insert the Matomo Analytics script + * into a webpage. + * @param {string} matomoUrl Domain name with tld to use with the Matomo Analytics script. + * @param {string} siteId Site ID to use with the Matomo Analytics script. + * @returns {string} HTML script tags to inject into page + */ +function getMatomoAnalyticsScript(matomoUrl, siteId) { + let escapedMatomoUrlJS = jsesc(matomoUrl, { isScriptContext: true }); + let escapedSiteIdJS = jsesc(siteId, { isScriptContext: true }); + + if (escapedMatomoUrlJS) { + escapedMatomoUrlJS = escapedMatomoUrlJS.trim(); + } + + if (escapedSiteIdJS) { + escapedSiteIdJS = escapedSiteIdJS.trim(); + } + + // Escape the domain url for use in an HTML attribute. + let escapedMatomoUrlHTMLAttribute = escape(escapedMatomoUrlJS); + + // Escape the website id for use in an HTML attribute. + let escapedSiteIdHTMLAttribute = escape(escapedSiteIdJS); + + return ` + + `; +} + +module.exports = { + getMatomoAnalyticsScript, +}; diff --git a/server/analytics/plausible-analytics.js b/server/analytics/plausible-analytics.js new file mode 100644 index 000000000..131f1136b --- /dev/null +++ b/server/analytics/plausible-analytics.js @@ -0,0 +1,36 @@ +const jsesc = require("jsesc"); +const { escape } = require("html-escaper"); + +/** + * Returns a string that represents the javascript that is required to insert the Plausible Analytics script + * into a webpage. + * @param {string} scriptUrl the Plausible Analytics script url. + * @param {string} domainsToMonitor Domains to track seperated by a ',' to add Plausible Analytics script. + * @returns {string} HTML script tags to inject into page + */ +function getPlausibleAnalyticsScript(scriptUrl, domainsToMonitor) { + let escapedScriptUrlJS = jsesc(scriptUrl, { isScriptContext: true }); + let escapedWebsiteIdJS = jsesc(domainsToMonitor, { isScriptContext: true }); + + if (escapedScriptUrlJS) { + escapedScriptUrlJS = escapedScriptUrlJS.trim(); + } + + if (escapedWebsiteIdJS) { + escapedWebsiteIdJS = escapedWebsiteIdJS.trim(); + } + + // Escape the domain url for use in an HTML attribute. + let escapedScriptUrlHTMLAttribute = escape(escapedScriptUrlJS); + + // Escape the website id for use in an HTML attribute. + let escapedWebsiteIdHTMLAttribute = escape(escapedWebsiteIdJS); + + return ` + + `; +} + +module.exports = { + getPlausibleAnalyticsScript +}; diff --git a/server/analytics/umami-analytics.js b/server/analytics/umami-analytics.js new file mode 100644 index 000000000..48c8b2eca --- /dev/null +++ b/server/analytics/umami-analytics.js @@ -0,0 +1,36 @@ +const jsesc = require("jsesc"); +const { escape } = require("html-escaper"); + +/** + * Returns a string that represents the javascript that is required to insert the Umami Analytics script + * into a webpage. + * @param {string} scriptUrl the Umami Analytics script url. + * @param {string} websiteId Website ID to use with the Umami Analytics script. + * @returns {string} HTML script tags to inject into page + */ +function getUmamiAnalyticsScript(scriptUrl, websiteId) { + let escapedScriptUrlJS = jsesc(scriptUrl, { isScriptContext: true }); + let escapedWebsiteIdJS = jsesc(websiteId, { isScriptContext: true }); + + if (escapedScriptUrlJS) { + escapedScriptUrlJS = escapedScriptUrlJS.trim(); + } + + if (escapedWebsiteIdJS) { + escapedWebsiteIdJS = escapedWebsiteIdJS.trim(); + } + + // Escape the Script url for use in an HTML attribute. + let escapedScriptUrlHTMLAttribute = escape(escapedScriptUrlJS); + + // Escape the website id for use in an HTML attribute. + let escapedWebsiteIdHTMLAttribute = escape(escapedWebsiteIdJS); + + return ` + + `; +} + +module.exports = { + getUmamiAnalyticsScript, +}; diff --git a/server/model/status_page.js b/server/model/status_page.js index 38f548ebb..f4af41cfb 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -3,7 +3,7 @@ const { R } = require("redbean-node"); const cheerio = require("cheerio"); const { UptimeKumaServer } = require("../uptime-kuma-server"); const jsesc = require("jsesc"); -const googleAnalytics = require("../google-analytics"); +const analytics = require("../analytics/analytics"); const { marked } = require("marked"); const { Feed } = require("feed"); const config = require("../config"); @@ -120,9 +120,9 @@ class StatusPage extends BeanModel { const head = $("head"); - if (statusPage.googleAnalyticsTagId) { - let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.googleAnalyticsTagId); - head.append($(escapedGoogleAnalyticsScript)); + if (analytics.isValidAnalyticsConfig(statusPage)) { + let escapedAnalyticsScript = analytics.getAnalyticsScript(statusPage); + head.append($(escapedAnalyticsScript)); } // OG Meta Tags @@ -407,7 +407,9 @@ class StatusPage extends BeanModel { customCSS: this.custom_css, footerText: this.footer_text, showPoweredBy: !!this.show_powered_by, - googleAnalyticsId: this.google_analytics_tag_id, + analyticsId: this.analytics_id, + analyticsScriptUrl: this.analytics_script_url, + analyticsType: this.analytics_type, showCertificateExpiry: !!this.show_certificate_expiry, }; } @@ -430,7 +432,9 @@ class StatusPage extends BeanModel { customCSS: this.custom_css, footerText: this.footer_text, showPoweredBy: !!this.show_powered_by, - googleAnalyticsId: this.google_analytics_tag_id, + analyticsId: this.analytics_id, + analyticsScriptUrl: this.analytics_script_url, + analyticsType: this.analytics_type, showCertificateExpiry: !!this.show_certificate_expiry, }; } diff --git a/server/socket-handlers/status-page-socket-handler.js b/server/socket-handlers/status-page-socket-handler.js index 1114d81fd..5d0320e10 100644 --- a/server/socket-handlers/status-page-socket-handler.js +++ b/server/socket-handlers/status-page-socket-handler.js @@ -166,7 +166,9 @@ module.exports.statusPageSocketHandler = (socket) => { statusPage.show_powered_by = config.showPoweredBy; statusPage.show_certificate_expiry = config.showCertificateExpiry; statusPage.modified_date = R.isoDateTime(); - statusPage.google_analytics_tag_id = config.googleAnalyticsId; + statusPage.analytics_id = config.analyticsId; + statusPage.analytics_script_url = config.analyticsScriptUrl; + statusPage.analytics_type = config.analyticsType; await R.store(statusPage); diff --git a/src/lang/en.json b/src/lang/en.json index cb704b0fe..333e9e56f 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -787,6 +787,9 @@ "wayToGetClickSendSMSToken": "You can get API Username and API Key from {0} .", "Custom Monitor Type": "Custom Monitor Type", "Google Analytics ID": "Google Analytics ID", + "Analytics Type": "Analytics Type", + "Analytics ID": "Analytics ID", + "Analytics Script URL": "Analytics Script URL", "Edit Tag": "Edit Tag", "Server Address": "Server Address", "Learn More": "Learn More", @@ -1067,5 +1070,9 @@ "YZJ Robot Token": "YZJ Robot token", "Plain Text": "Plain Text", "Message Template": "Message Template", - "Template Format": "Template Format" + "Template Format": "Template Format", + "Google": "Google", + "Plausible": "Plausible", + "Matomo": "Matomo", + "Umami": "Umami" } diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index 116968282..78e592b36 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -1,3 +1,29 @@ + +