Add centralized analytics tracking

This commit is contained in:
thePR0M3TH3AN
2025-09-25 10:09:45 -04:00
parent 73e5a6bc83
commit dbe4563bb7
5 changed files with 221 additions and 0 deletions

View File

@@ -34,6 +34,10 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<script type="module">
import { trackPageView } from "./js/analytics.js";
trackPageView(window.location.pathname);
</script>
<script type="module" crossorigin>
var pv = Object.defineProperty;
var gv = (e, t, n) =>

170
js/analytics.js Normal file
View File

@@ -0,0 +1,170 @@
// js/analytics.js
import { ANALYTICS_CONFIG } from "./analyticsConfig.js";
const SCRIPT_ATTR = "data-bitvid-analytics";
const SCRIPT_IDENTIFIER = "umami";
const pendingCalls = [];
let flushTimerId = null;
let scriptLoadedOnce = false;
function isBrowserEnvironment() {
return typeof window !== "undefined" && typeof document !== "undefined";
}
function flushPendingCalls() {
if (!isBrowserEnvironment()) {
return;
}
const umami = window.umami;
if (!umami) {
return;
}
while (pendingCalls.length > 0) {
const { method, args } = pendingCalls.shift();
const fn = typeof umami[method] === "function" ? umami[method] : null;
if (!fn) {
continue;
}
try {
fn.apply(umami, args);
} catch (err) {
console.warn("[analytics] Failed to call", method, err);
}
}
}
function scheduleFlush() {
if (!isBrowserEnvironment()) {
return;
}
if (flushTimerId !== null) {
return;
}
flushTimerId = window.setInterval(() => {
if (window.umami && typeof window.umami.trackEvent === "function") {
window.clearInterval(flushTimerId);
flushTimerId = null;
flushPendingCalls();
}
}, 500);
}
export function ensureAnalyticsLoaded(doc = typeof document !== "undefined" ? document : null) {
if (!doc) {
return null;
}
let script = doc.querySelector(`script[${SCRIPT_ATTR}]`);
if (script) {
if (!scriptLoadedOnce) {
// If a server rendered script already exists, make sure we flush asap.
scriptLoadedOnce = true;
flushPendingCalls();
}
return script;
}
script = doc.createElement("script");
script.defer = true;
script.src = ANALYTICS_CONFIG.scriptSrc;
script.setAttribute(SCRIPT_ATTR, SCRIPT_IDENTIFIER);
script.dataset.websiteId = ANALYTICS_CONFIG.websiteId;
script.addEventListener("load", () => {
scriptLoadedOnce = true;
flushPendingCalls();
});
doc.head.appendChild(script);
scheduleFlush();
return script;
}
function queueCall(method, args) {
pendingCalls.push({ method, args });
scheduleFlush();
}
function invokeUmami(method, args) {
if (!isBrowserEnvironment()) {
return;
}
ensureAnalyticsLoaded();
const umami = window.umami;
if (umami && typeof umami[method] === "function") {
try {
umami[method].apply(umami, args);
} catch (err) {
console.warn("[analytics] Failed to call", method, err);
}
return;
}
queueCall(method, args);
}
export function trackPageView(path, referrer) {
if (!isBrowserEnvironment()) {
return;
}
const resolvedPath =
typeof path === "string" && path.length > 0
? path
: `${window.location.pathname}${window.location.hash || ""}`;
const resolvedReferrer =
typeof referrer === "string" ? referrer : document.referrer || "";
invokeUmami("trackView", [resolvedPath, resolvedReferrer]);
}
export function trackVideoView({
videoId,
title,
source,
hasMagnet,
hasUrl,
} = {}) {
if (!isBrowserEnvironment()) {
return;
}
const payload = {};
if (videoId) {
payload.videoId = String(videoId);
}
if (title) {
payload.title = String(title);
}
if (source) {
payload.source = String(source);
}
if (typeof hasMagnet === "boolean") {
payload.hasMagnet = hasMagnet;
}
if (typeof hasUrl === "boolean") {
payload.hasUrl = hasUrl;
}
invokeUmami("trackEvent", [
ANALYTICS_CONFIG.videoViewEventName,
payload,
]);
}
// Immediately queue the analytics script so page views are captured early.
if (isBrowserEnvironment()) {
ensureAnalyticsLoaded();
}

18
js/analyticsConfig.js Normal file
View File

@@ -0,0 +1,18 @@
// js/analyticsConfig.js
// Central configuration for site-wide analytics tracking.
export const ANALYTICS_CONFIG = Object.freeze({
/**
* Hosted Umami script that powers analytics.
* Update this value if the tracker is relocated.
*/
scriptSrc: "https://umami.malin.onl/script.js",
/**
* The Umami website identifier for this deployment.
*/
websiteId: "1f8eead2-79f0-4dba-8c3b-ed9b08b6e877",
/**
* Event name used when recording individual video views.
*/
videoViewEventName: "video_view",
});

View File

@@ -9,6 +9,7 @@ import { safeDecodeMagnet } from "./magnetUtils.js";
import { normalizeAndAugmentMagnet } from "./magnet.js";
import { deriveTorrentPlaybackConfig } from "./playbackUtils.js";
import { URL_FIRST_ENABLED } from "./constants.js";
import { trackVideoView } from "./analytics.js";
import {
initialWhitelist,
initialBlacklist,
@@ -3111,6 +3112,14 @@ class bitvidApp {
const magnetSupported = isValidMagnetUri(usableMagnetCandidate);
const sanitizedMagnet = magnetSupported ? usableMagnetCandidate : "";
trackVideoView({
videoId: video.id || eventId,
title: video.title || "Untitled",
source: "event",
hasMagnet: !!sanitizedMagnet,
hasUrl: !!trimmedUrl,
});
this.currentVideo = {
...video,
url: trimmedUrl,
@@ -3195,6 +3204,17 @@ class bitvidApp {
const magnetSupported = isValidMagnetUri(usableMagnet);
const sanitizedMagnet = magnetSupported ? usableMagnet : "";
trackVideoView({
videoId:
typeof title === "string" && title.trim().length > 0
? `direct:${title.trim()}`
: "direct-playback",
title,
source: "direct",
hasMagnet: !!sanitizedMagnet,
hasUrl: !!sanitizedUrl,
});
if (!sanitizedUrl && !sanitizedMagnet) {
const message = trimmedMagnet && !magnetSupported
? UNSUPPORTED_BTITH_MESSAGE

View File

@@ -1,5 +1,7 @@
// js/index.js
import { trackPageView } from "./analytics.js";
// 1) Load modals (login, application, etc.)
async function loadModal(url) {
try {
@@ -301,6 +303,11 @@ function handleQueryParams() {
/**
* Handle #view=... in the hash and load the correct partial view.
*/
function recordView(viewName) {
const path = `${window.location.pathname}#view=${viewName}`;
trackPageView(path);
}
function handleHashChange() {
console.log("handleHashChange called, current hash =", window.location.hash);
@@ -317,6 +324,7 @@ function handleHashChange() {
if (typeof initFn === "function") {
initFn();
}
recordView("most-recent-videos");
});
});
return;
@@ -332,6 +340,7 @@ function handleHashChange() {
if (typeof initFn === "function") {
initFn();
}
recordView(viewName);
});
});
}