feat: add tracking support on status pages for rybbit analytics (#7525)

This commit is contained in:
Daniel Oberlechner
2026-06-20 01:07:35 +02:00
committed by GitHub
parent a2ac12fb65
commit a57aabf8c3
6 changed files with 76 additions and 1 deletions
@@ -0,0 +1,33 @@
const newValues = ["google", "umami", "plausible", "matomo", "rybbit"];
const oldValues = ["google", "umami", "plausible", "matomo"];
/**
* Rebuild the status_page.analytics_type enum with the given values, keeping existing data.
* The column is dropped and re-created because SQLite's .enu().alter() does not replace the old CHECK constraint.
* @param {import("knex").Knex} knex The knex instance
* @param {string[]} allowedValues Allowed analytics_type values
* @returns {Promise<void>}
*/
async function rebuildAnalyticsType(knex, allowedValues) {
const rows = await knex("status_page").whereNotNull("analytics_type").select("id", "analytics_type");
await knex.schema.alterTable("status_page", (table) => {
table.dropColumn("analytics_type");
});
await knex.schema.alterTable("status_page", (table) => {
table.enu("analytics_type", allowedValues).defaultTo(null);
});
for (const row of rows) {
await knex("status_page").where("id", row.id).update({ analytics_type: row.analytics_type });
}
}
exports.up = function (knex) {
return rebuildAnalyticsType(knex, newValues);
};
exports.down = async function (knex) {
await knex("status_page").where("analytics_type", "rybbit").update({ analytics_type: null });
await rebuildAnalyticsType(knex, oldValues);
};
+4
View File
@@ -2,6 +2,7 @@ const googleAnalytics = require("./google-analytics");
const umamiAnalytics = require("./umami-analytics"); const umamiAnalytics = require("./umami-analytics");
const plausibleAnalytics = require("./plausible-analytics"); const plausibleAnalytics = require("./plausible-analytics");
const matomoAnalytics = require("./matomo-analytics"); const matomoAnalytics = require("./matomo-analytics");
const rybbitAnalytics = require("./rybbit-analytics");
/** /**
* Returns a string that represents the javascript that is required to insert the selected Analytics' script * Returns a string that represents the javascript that is required to insert the selected Analytics' script
@@ -22,6 +23,8 @@ function getAnalyticsScript(statusPage) {
); );
case "matomo": case "matomo":
return matomoAnalytics.getMatomoAnalyticsScript(statusPage.analyticsScriptUrl, statusPage.analyticsId); return matomoAnalytics.getMatomoAnalyticsScript(statusPage.analyticsScriptUrl, statusPage.analyticsId);
case "rybbit":
return rybbitAnalytics.getRybbitAnalyticsScript(statusPage.analyticsScriptUrl, statusPage.analyticsId);
default: default:
return null; return null;
} }
@@ -39,6 +42,7 @@ function isValidAnalyticsConfig(statusPage) {
case "umami": case "umami":
case "plausible": case "plausible":
case "matomo": case "matomo":
case "rybbit":
return statusPage.analyticsId != null && statusPage.analyticsScriptUrl != null; return statusPage.analyticsId != null && statusPage.analyticsScriptUrl != null;
default: default:
return false; return false;
+36
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 Rybbit Analytics script
* into a webpage.
* @param {string} scriptUrl the Rybbit Analytics script url.
* @param {string} siteId Site ID to use with the Rybbit Analytics script.
* @returns {string} HTML script tags to inject into page
*/
function getRybbitAnalyticsScript(scriptUrl, siteId) {
let escapedScriptUrlJS = jsesc(scriptUrl, { isScriptContext: true });
let escapedSiteIdJS = jsesc(siteId, { isScriptContext: true });
if (escapedScriptUrlJS) {
escapedScriptUrlJS = escapedScriptUrlJS.trim();
}
if (escapedSiteIdJS) {
escapedSiteIdJS = escapedSiteIdJS.trim();
}
// Escape the Script url for use in an HTML attribute.
let escapedScriptUrlHTMLAttribute = escape(escapedScriptUrlJS);
// Escape the site id for use in an HTML attribute.
let escapedSiteIdHTMLAttribute = escape(escapedSiteIdJS);
return `
<script defer src="${escapedScriptUrlHTMLAttribute}" data-site-id="${escapedSiteIdHTMLAttribute}"></script>
`;
}
module.exports = {
getRybbitAnalyticsScript,
};
@@ -339,7 +339,7 @@ module.exports.statusPageSocketHandler = (socket) => {
statusPage.modified_date = R.isoDateTime(); statusPage.modified_date = R.isoDateTime();
statusPage.analytics_id = config.analyticsId; statusPage.analytics_id = config.analyticsId;
statusPage.analytics_script_url = config.analyticsScriptUrl; statusPage.analytics_script_url = config.analyticsScriptUrl;
const validAnalyticsTypes = ["google", "umami", "plausible", "matomo"]; const validAnalyticsTypes = ["google", "umami", "plausible", "matomo", "rybbit"];
if (config.analyticsType !== null && !validAnalyticsTypes.includes(config.analyticsType)) { if (config.analyticsType !== null && !validAnalyticsTypes.includes(config.analyticsType)) {
throw new Error("Invalid analytics type"); throw new Error("Invalid analytics type");
} }
+1
View File
@@ -1383,6 +1383,7 @@
"Plausible": "Plausible", "Plausible": "Plausible",
"Matomo": "Matomo", "Matomo": "Matomo",
"Umami": "Umami", "Umami": "Umami",
"Rybbit": "Rybbit",
"Disable URL in Notification": "Disable URL in Notification", "Disable URL in Notification": "Disable URL in Notification",
"Suppress Notifications": "Suppress Notifications", "Suppress Notifications": "Suppress Notifications",
"discordSuppressNotificationsHelptext": "When enabled, messages will be posted to the channel but won't trigger push or desktop notifications for recipients.", "discordSuppressNotificationsHelptext": "When enabled, messages will be posted to the channel but won't trigger push or desktop notifications for recipients.",
+1
View File
@@ -161,6 +161,7 @@
<option value="umami">{{ $t("Umami") }}</option> <option value="umami">{{ $t("Umami") }}</option>
<option value="plausible">{{ $t("Plausible") }}</option> <option value="plausible">{{ $t("Plausible") }}</option>
<option value="matomo">{{ $t("Matomo") }}</option> <option value="matomo">{{ $t("Matomo") }}</option>
<option value="rybbit">{{ $t("Rybbit") }}</option>
</select> </select>
</div> </div>