diff --git a/server/analytics/analytics.js b/server/analytics/analytics.js new file mode 100644 index 000000000..a5e2c1120 --- /dev/null +++ b/server/analytics/analytics.js @@ -0,0 +1,44 @@ +const googleAnalytics = require("./google-analytics"); +const umamiAnalytics = require("./umami-analytics"); +const plausibleAnalytics = require("./plausible-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.analyticsDomainUrl, statusPage.analyticsId); + case "plausible": + return plausibleAnalytics.getPlausibleAnalyticsScript(statusPage.analyticsDomainUrl, 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": + return statusPage.analyticsId != null && statusPage.analyticsDomainUrl != 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/plausible-analytics.js b/server/analytics/plausible-analytics.js new file mode 100644 index 000000000..14df98115 --- /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} plausibleDomainUrl Domain name with tld to use with the Plausible Analytics script. + * @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(plausibleDomainUrl, domainsToMonitor) { + let escapedDomainUrlJS = jsesc(plausibleDomainUrl, { isScriptContext: true }); + let escapedWebsiteIdJS = jsesc(domainsToMonitor, { 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 = { + getPlausibleAnalyticsScript +}; diff --git a/server/umami-analytics.js b/server/analytics/umami-analytics.js similarity index 100% rename from server/umami-analytics.js rename to server/analytics/umami-analytics.js diff --git a/server/model/status_page.js b/server/model/status_page.js index b3cf434cb..32188ffaa 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -3,8 +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 umamiAnalytics = require("../umami-analytics"); +const analytics = require("../analytics/analytics"); const { marked } = require("marked"); const { Feed } = require("feed"); const config = require("../config"); @@ -121,14 +120,9 @@ class StatusPage extends BeanModel { const head = $("head"); - if (statusPage.analyticsType === "google" && statusPage.analyticsId) { - let escapedGoogleAnalyticsScript = googleAnalytics.getGoogleAnalyticsScript(statusPage.analyticsId); - head.append($(escapedGoogleAnalyticsScript)); - } - - if (statusPage.analyticsType === "umami" && statusPage.analyticsDomainUrl && statusPage.analyticsId) { - let escapedUmamiAnalyticsScript = umamiAnalytics.getUmamiAnalyticsScript(statusPage.analyticsDomainUrl, statusPage.analyticsId); - head.append($(escapedUmamiAnalyticsScript)); + if (analytics.isValidAnalyticsConfig(statusPage)) { + let escapedAnalyticsScript = analytics.getAnalyticsScript(statusPage); + head.append($(escapedAnalyticsScript)); } // OG Meta Tags diff --git a/test/e2e/specs/status-page.spec.js b/test/e2e/specs/status-page.spec.js index 9e5de4a43..43c5d43a6 100644 --- a/test/e2e/specs/status-page.spec.js +++ b/test/e2e/specs/status-page.spec.js @@ -20,6 +20,8 @@ test.describe("Status Page", () => { const googleAnalyticsId = "G-123"; const umamiAnalyticsDomainUrl = "example.com"; const umamiAnalyticsWebsiteId = "606487e2-bc25-45f9-9132-fa8b065aad46"; + const plausibleAnalyticsDomainUrl = "example.com"; + const plausibleAnalyticsDomainsUrls = "one.com,two.com"; const customCss = "body { background: rgb(0, 128, 128) !important; }"; const descriptionText = "This is an example status page."; const incidentTitle = "Example Outage Incident"; @@ -132,6 +134,16 @@ test.describe("Status Page", () => { expect(await page.locator("head").innerHTML()).toContain(umamiAnalyticsDomainUrl); expect(await page.locator("head").innerHTML()).toContain(umamiAnalyticsWebsiteId); + + await page.getByTestId("edit-button").click(); + // Fill in plausible analytics after editing + await page.getByTestId("analytics-type-select").selectOption("plausible"); + await page.getByTestId("analytics-domain-url-input").fill(plausibleAnalyticsDomainUrl); + await page.getByTestId("analytics-id-input").fill(plausibleAnalyticsDomainsUrls); + await page.getByTestId("save-button").click(); + await screenshot(testInfo, page); + expect(await page.locator("head").innerHTML()).toContain(plausibleAnalyticsDomainUrl); + expect(await page.locator("head").innerHTML()).toContain(plausibleAnalyticsDomainsUrls); }); // @todo Test certificate expiry