feat: add support for umami tracking

This commit is contained in:
hadestructhor 2025-02-09 14:06:25 +01:00
parent bd118ea3ea
commit afae736972
9 changed files with 79 additions and 0 deletions

View file

@ -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

View file

@ -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;

View file

@ -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,

View file

@ -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 = $("<meta property=\"og:title\" content=\"\" />").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,
};
}

View file

@ -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);

36
server/umami-analytics.js Normal file
View file

@ -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 `
<script defer src="https://${escapedDomainUrlHTMLAttribute}/script.js" data-website-id="${escapedWebsiteIdHTMLAttribute}"></script>
`;
}
module.exports = {
getUmamiAnalyticsScript,
};

View file

@ -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",

View file

@ -98,6 +98,16 @@
<input id="googleAnalyticsTag" v-model="config.googleAnalyticsId" type="text" class="form-control" data-testid="google-analytics-input">
</div>
<!-- Umami Analytics -->
<div class="my-3">
<label for="umamiAnalyticsDomainUrl" class="form-label">{{ $t("Umami Analytics Domain Url") }}</label>
<input id="umamiAnalyticsDomainUrl" v-model="config.umamiAnalyticsDomainUrl" type="text" class="form-control" data-testid="umami-analytics-domain-url-input">
</div>
<div class="my-3">
<label for="umamiAnalyticsWebsite" class="form-label">{{ $t("Umami Analytics Website ID") }}</label>
<input id="umamiAnalyticsWebsite" v-model="config.umamiAnalyticsWebsiteId" type="text" class="form-control" data-testid="umami-analytics-website-id-input">
</div>
<!-- Custom CSS -->
<div class="my-3">
<div class="mb-1">{{ $t("Custom CSS") }}</div>

View file

@ -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)");