Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion db/knex_init_db.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,10 @@ async function createTables() {
table.text("footer_text");
table.text("custom_css");
table.boolean("show_powered_by").notNullable().defaultTo(true);
table.string("google_analytics_tag_id");
table.string("analytics_id");
table.string("analytics_domain_url");
table.enu("analytics_type", [ "google", "umami", "plausible" ]).defaultTo(null);

});

// maintenance_status_page
Expand Down
23 changes: 23 additions & 0 deletions db/knex_migrations/2025-02-17-2142-generalize-analytics.js
Original file line number Diff line number Diff line change
@@ -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_domain_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_domain_url");
table.dropColumn("analytics_type");
});
};
48 changes: 48 additions & 0 deletions server/analytics/analytics.js
Original file line number Diff line number Diff line change
@@ -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.analyticsDomainUrl, statusPage.analyticsId);
case "plausible":
return plausibleAnalytics.getPlausibleAnalyticsScript(statusPage.analyticsDomainUrl, statusPage.analyticsId);
case "matomo":
return matomoAnalytics.getMatomoAnalyticsScript(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":
case "matomo":
return statusPage.analyticsId != null && statusPage.analyticsDomainUrl != null;
default:
return false;
}
}

module.exports = {
getAnalyticsScript,
isValidAnalyticsConfig
};
File renamed without changes.
47 changes: 47 additions & 0 deletions server/analytics/matomo-analytics.js
Original file line number Diff line number Diff line change
@@ -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 `
<script type="text/javascript">
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//${escapedMatomoUrlHTMLAttribute}/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', ${escapedSiteIdHTMLAttribute}]);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
`;
}

module.exports = {
getMatomoAnalyticsScript,
};
36 changes: 36 additions & 0 deletions server/analytics/plausible-analytics.js
Original file line number Diff line number Diff line change
@@ -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 `
<script defer src="https://${escapedDomainUrlHTMLAttribute}/js/script.js" data-domain="${escapedWebsiteIdHTMLAttribute}"></script>
`;
}

module.exports = {
getPlausibleAnalyticsScript
};
36 changes: 36 additions & 0 deletions server/analytics/umami-analytics.js
Original file line number Diff line number Diff line change
@@ -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,
};
16 changes: 10 additions & 6 deletions server/model/status_page.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
analyticsDomainUrl: this.analytics_domain_url,
analyticsType: this.analytics_type,
showCertificateExpiry: !!this.show_certificate_expiry,
};
}
Expand All @@ -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,
analyticsDomainUrl: this.analytics_domain_url,
analyticsType: this.analytics_type,
showCertificateExpiry: !!this.show_certificate_expiry,
};
}
Expand Down
4 changes: 3 additions & 1 deletion server/socket-handlers/status-page-socket-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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_domain_url = config.analyticsDomainUrl;
statusPage.analytics_type = config.analyticsType;

await R.store(statusPage);

Expand Down
5 changes: 5 additions & 0 deletions src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,11 @@
"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 Domain URL": "Analytics Domain URL",
"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",
Expand Down
47 changes: 44 additions & 3 deletions src/pages/StatusPage.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
<script setup>
// Analytics options
const analyticsOptions = [
{
name: "None",
value: null
},
{
name: "Google",
value: "google"
},
{
name: "Umami",
value: "umami"
},
{
name: "Plausible",
value: "plausible"
},
{
name: "Matomo",
value: "matomo"
}
];
</script>

<template>
<div v-if="loadedTheme" class="container mt-3">
<!-- Sidebar for edit mode -->
Expand Down Expand Up @@ -92,10 +118,25 @@
</ul>
</div>

<!-- Google Analytics -->
<!-- Analytics -->

<div class="my-3">
<label for="googleAnalyticsTag" class="form-label">{{ $t("Google Analytics ID") }}</label>
<input id="googleAnalyticsTag" v-model="config.googleAnalyticsId" type="text" class="form-control" data-testid="google-analytics-input">
<label for="analyticsType" class="form-label">{{ $t("Analytics Type") }}</label>
<select id="analyticsType" v-model="config.analyticsType" class="form-select" data-testid="analytics-type-select">
<option v-for="(analyticOption, index) in analyticsOptions" :key="index" :value="analyticOption.value">
{{ analyticOption.name }}
</option>
</select>
</div>

<div v-if="config.analyticsType !== null && config.analyticsType !== undefined" class="my-3">
<label for="analyticsId" class="form-label">{{ $t("Analytics ID") }}</label>
<input id="analyticsId" v-model="config.analyticsId" type="text" class="form-control" data-testid="analytics-id-input">
</div>

<div v-if="config.analyticsType !== null && config.analyticsType !== undefined && config.analyticsType !== 'google'" class="my-3">
<label for="analyticsDomainUrl" class="form-label">{{ $t("Analytics Domain URL") }}</label>
<input id="analyticsDomainUrl" v-model="config.analyticsDomainUrl" type="text" class="form-control" data-testid="analytics-domain-url-input">
</div>

<!-- Custom CSS -->
Expand Down
Loading
Loading