From afae736972babf3017e04d577b0c4a5b470d8993 Mon Sep 17 00:00:00 2001 From: hadestructhor <60148800+hadestructhor@users.noreply.github.com> Date: Sun, 9 Feb 2025 14:06:25 +0100 Subject: [PATCH] feat: add support for umami tracking --- db/knex_init_db.js | 2 ++ .../patch-add-umami-analytics-status-page.sql | 10 ++++++ server/database.js | 1 + server/model/status_page.js | 10 ++++++ .../status-page-socket-handler.js | 2 ++ server/umami-analytics.js | 36 +++++++++++++++++++ src/lang/en.json | 2 ++ src/pages/StatusPage.vue | 10 ++++++ test/e2e/specs/status-page.spec.js | 6 ++++ 9 files changed, 79 insertions(+) create mode 100644 db/old_migrations/patch-add-umami-analytics-status-page.sql create mode 100644 server/umami-analytics.js diff --git a/db/knex_init_db.js b/db/knex_init_db.js index 46bff4bfa..42b076c45 100644 --- a/db/knex_init_db.js +++ b/db/knex_init_db.js @@ -203,6 +203,8 @@ async function createTables() { table.text("custom_css"); table.boolean("show_powered_by").notNullable().defaultTo(true); table.string("google_analytics_tag_id"); + table.string("umami_analytics_domain_url"); + table.string("umami_analytics_website_id"); }); // maintenance_status_page diff --git a/db/old_migrations/patch-add-umami-analytics-status-page.sql b/db/old_migrations/patch-add-umami-analytics-status-page.sql new file mode 100644 index 000000000..e8b15f4d5 --- /dev/null +++ b/db/old_migrations/patch-add-umami-analytics-status-page.sql @@ -0,0 +1,10 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +BEGIN TRANSACTION; + +ALTER TABLE status_page + ADD umami_analytics_domain_url VARCHAR; + +ALTER TABLE status_page +ADD umami_analytics_website_id VARCHAR; + +COMMIT; diff --git a/server/database.js b/server/database.js index 0e6a7405d..6aa302745 100644 --- a/server/database.js +++ b/server/database.js @@ -95,6 +95,7 @@ class Database { "patch-maintenance-table2.sql": true, "patch-add-gamedig-monitor.sql": true, "patch-add-google-analytics-status-page-tag.sql": true, + "patch-add-umami-analytics-status-page.sql": true, "patch-http-body-encoding.sql": true, "patch-add-description-monitor.sql": true, "patch-api-key-table.sql": true, diff --git a/server/model/status_page.js b/server/model/status_page.js index 38f548ebb..6548effc6 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -4,6 +4,7 @@ const cheerio = require("cheerio"); const { UptimeKumaServer } = require("../uptime-kuma-server"); const jsesc = require("jsesc"); const googleAnalytics = require("../google-analytics"); +const umamiAnalytics = require("../umami-analytics"); const { marked } = require("marked"); const { Feed } = require("feed"); const config = require("../config"); @@ -125,6 +126,11 @@ class StatusPage extends BeanModel { head.append($(escapedGoogleAnalyticsScript)); } + if (statusPage.umamiAnalyticsDomainUrl && statusPage.umamiAnalyticsWebsiteId) { + let escapedUmamiAnalyticsScript = umamiAnalytics.getUmamiAnalyticsScript(statusPage.umamiAnalyticsDomainUrl, statusPage.umamiAnalyticsWebsiteId); + head.append($(escapedUmamiAnalyticsScript)); + } + // OG Meta Tags let ogTitle = $("").attr("content", statusPage.title); head.append(ogTitle); @@ -408,6 +414,8 @@ class StatusPage extends BeanModel { footerText: this.footer_text, showPoweredBy: !!this.show_powered_by, googleAnalyticsId: this.google_analytics_tag_id, + umamiAnalyticsDomainUrl: this.umami_analytics_domain_url, + umamiAnalyticsWebsiteId: this.umami_analytics_website_id, showCertificateExpiry: !!this.show_certificate_expiry, }; } @@ -431,6 +439,8 @@ class StatusPage extends BeanModel { footerText: this.footer_text, showPoweredBy: !!this.show_powered_by, googleAnalyticsId: this.google_analytics_tag_id, + umamiAnalyticsDomainUrl: this.umami_analytics_domain_url, + umamiAnalyticsWebsiteId: this.umami_analytics_website_id, 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..c6a474cac 100644 --- a/server/socket-handlers/status-page-socket-handler.js +++ b/server/socket-handlers/status-page-socket-handler.js @@ -167,6 +167,8 @@ module.exports.statusPageSocketHandler = (socket) => { statusPage.show_certificate_expiry = config.showCertificateExpiry; statusPage.modified_date = R.isoDateTime(); statusPage.google_analytics_tag_id = config.googleAnalyticsId; + statusPage.umami_analytics_domain_url = config.umamiAnalyticsDomainUrl; + statusPage.umami_analytics_website_id = config.umamiAnalyticsWebsiteId; await R.store(statusPage); diff --git a/server/umami-analytics.js b/server/umami-analytics.js new file mode 100644 index 000000000..5a4a172f9 --- /dev/null +++ b/server/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} domainUrl Domain name with tld to use with the Umami Analytics script. + * @param {string} websiteId Website ID to use with the Umami Analytics script. + * @returns {string} HTML script tags to inject into page + */ +function getUmamiAnalyticsScript(domainUrl, websiteId) { + let escapedDomainUrlJS = jsesc(domainUrl, { isScriptContext: true }); + let escapedWebsiteIdJS = jsesc(websiteId, { isScriptContext: true }); + + if (escapedDomainUrlJS) { + escapedDomainUrlJS = escapedDomainUrlJS.trim(); + } + + if (escapedWebsiteIdJS) { + escapedWebsiteIdJS = escapedWebsiteIdJS.trim(); + } + + // Escape the domain url for use in an HTML attribute. + let escapedDomainUrlHTMLAttribute = escape(escapedDomainUrlJS); + + // Escape the website id for use in an HTML attribute. + let escapedWebsiteIdHTMLAttribute = escape(escapedWebsiteIdJS); + + return ` + + `; +} + +module.exports = { + getUmamiAnalyticsScript, +}; diff --git a/src/lang/en.json b/src/lang/en.json index e215f1031..c06547fc5 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -782,6 +782,8 @@ "wayToGetClickSendSMSToken": "You can get API Username and API Key from {0} .", "Custom Monitor Type": "Custom Monitor Type", "Google Analytics ID": "Google Analytics ID", + "Umami Analytics Domain Url": "Umami Analytics Domain Url", + "Umami Analytics Website ID": "Umami Analytics Website ID", "Edit Tag": "Edit Tag", "Server Address": "Server Address", "Learn More": "Learn More", diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index 116968282..8edb8f4e1 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -98,6 +98,16 @@ + +
+ + +
+
+ + +
+
{{ $t("Custom CSS") }}
diff --git a/test/e2e/specs/status-page.spec.js b/test/e2e/specs/status-page.spec.js index f525dfc6f..0bec78c79 100644 --- a/test/e2e/specs/status-page.spec.js +++ b/test/e2e/specs/status-page.spec.js @@ -18,6 +18,8 @@ test.describe("Status Page", () => { const refreshInterval = 30; const theme = "dark"; const googleAnalyticsId = "G-123"; + const umamiAnalyticsDomainUrl = "example.com"; + const umamiAnalyticsWebsiteId = "606487e2-bc25-45f9-9132-fa8b065aad46"; const customCss = "body { background: rgb(0, 128, 128) !important; }"; const descriptionText = "This is an example status page."; const incidentTitle = "Example Outage Incident"; @@ -58,6 +60,8 @@ test.describe("Status Page", () => { await page.getByTestId("show-powered-by-checkbox").uncheck(); await page.getByTestId("show-certificate-expiry-checkbox").uncheck(); await page.getByTestId("google-analytics-input").fill(googleAnalyticsId); + await page.getByTestId("umami-analytics-domain-url-input").fill(umamiAnalyticsDomainUrl); + await page.getByTestId("umami-analytics-website-id-input").fill(umamiAnalyticsWebsiteId); await page.getByTestId("custom-css-input").getByTestId("textarea").fill(customCss); // Prism await expect(page.getByTestId("description-editable")).toHaveText(descriptionText); await expect(page.getByTestId("custom-footer-editable")).toHaveText(footerText); @@ -101,6 +105,8 @@ test.describe("Status Page", () => { await expect(page.locator("body")).toHaveClass(theme); expect(await page.locator("head").innerHTML()).toContain(googleAnalyticsId); + expect(await page.locator("head").innerHTML()).toContain(umamiAnalyticsDomainUrl); + expect(await page.locator("head").innerHTML()).toContain(umamiAnalyticsWebsiteId); const backgroundColor = await page.evaluate(() => window.getComputedStyle(document.body).backgroundColor); expect(backgroundColor).toEqual("rgb(0, 128, 128)");