Traccia i movimenti aggregati, overlay a gradienti sull’iframe della pagina e toggle click/move in admin. Co-authored-by: Cursor <cursoragent@cursor.com>
229 lines
6.6 KiB
JavaScript
229 lines
6.6 KiB
JavaScript
/*! Match Live TV first-party site analytics (click + mouse move + scroll). Requires analytics cookie consent. */
|
|
(function () {
|
|
"use strict";
|
|
|
|
var ENDPOINT = "/analytics/events";
|
|
var STORAGE_KEY = "mltv_cookie_consent";
|
|
var COOKIE_NAME = "mltv_cookie_consent";
|
|
var FLUSH_MS = 6000;
|
|
var MAX_QUEUE = 60;
|
|
var MOVE_THROTTLE_MS = 180;
|
|
var MOVE_GRID = 40;
|
|
|
|
var queue = [];
|
|
var moveBuckets = {};
|
|
var maxScroll = 0;
|
|
var flushTimer = null;
|
|
var started = false;
|
|
var lastMoveAt = 0;
|
|
|
|
function excludedPath(pathname) {
|
|
var p = pathname || "/";
|
|
if (p.indexOf("/admin") === 0) return true;
|
|
if (p.indexOf("/regia") === 0) return true;
|
|
if (p.indexOf("/analytics") === 0) return true;
|
|
if (/^\/live\/[^/]+/.test(p)) return true;
|
|
if (/^\/replay\/[^/]+/.test(p)) return true;
|
|
return false;
|
|
}
|
|
|
|
function hasAnalyticsConsent() {
|
|
try {
|
|
var raw = localStorage.getItem(STORAGE_KEY);
|
|
if (raw) {
|
|
var parsed = JSON.parse(raw);
|
|
if (parsed && parsed.analytics) return true;
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
var match = document.cookie.match(
|
|
new RegExp("(?:^|; )" + COOKIE_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "=([^;]*)")
|
|
);
|
|
return !!(match && decodeURIComponent(match[1]) === "all");
|
|
}
|
|
|
|
function deviceBucket() {
|
|
var w = window.innerWidth || document.documentElement.clientWidth || 1024;
|
|
if (w < 768) return "mobile";
|
|
if (w < 1024) return "tablet";
|
|
return "desktop";
|
|
}
|
|
|
|
function pageSize() {
|
|
var doc = document.documentElement;
|
|
var body = document.body;
|
|
return {
|
|
width: Math.max(body.scrollWidth || 0, doc.scrollWidth || 0, window.innerWidth || 1),
|
|
height: Math.max(body.scrollHeight || 0, doc.scrollHeight || 0, window.innerHeight || 1)
|
|
};
|
|
}
|
|
|
|
function scrollPct() {
|
|
var doc = document.documentElement;
|
|
var body = document.body;
|
|
var scrollTop = window.pageYOffset || doc.scrollTop || body.scrollTop || 0;
|
|
var height = Math.max(
|
|
body.scrollHeight || 0,
|
|
doc.scrollHeight || 0,
|
|
body.offsetHeight || 0,
|
|
doc.offsetHeight || 0,
|
|
doc.clientHeight || 0
|
|
);
|
|
var view = window.innerHeight || doc.clientHeight || 0;
|
|
var pct = ((scrollTop + view) / Math.max(height, 1)) * 100;
|
|
if (scrollTop <= 0 && view >= height) return 100;
|
|
return Math.max(0, Math.min(100, Math.round(pct)));
|
|
}
|
|
|
|
function pushEvent(evt) {
|
|
if (!started) return;
|
|
queue.push(evt);
|
|
if (queue.length >= MAX_QUEUE) flush(false);
|
|
}
|
|
|
|
function flushMovesIntoQueue() {
|
|
var keys = Object.keys(moveBuckets);
|
|
if (!keys.length) return;
|
|
var device = deviceBucket();
|
|
var path = location.pathname;
|
|
var ts = Date.now();
|
|
keys.forEach(function (key) {
|
|
var parts = key.split(",");
|
|
var cx = parseInt(parts[0], 10);
|
|
var cy = parseInt(parts[1], 10);
|
|
var n = moveBuckets[key];
|
|
if (!n) return;
|
|
// cell center in %
|
|
var x = ((cx + 0.5) / MOVE_GRID) * 100;
|
|
var y = ((cy + 0.5) / MOVE_GRID) * 100;
|
|
pushEvent({ type: "move", path: path, device: device, x: x, y: y, n: n, ts: ts });
|
|
});
|
|
moveBuckets = {};
|
|
}
|
|
|
|
function flush(useBeacon) {
|
|
flushMovesIntoQueue();
|
|
if (!queue.length || !hasAnalyticsConsent()) {
|
|
queue = [];
|
|
return;
|
|
}
|
|
var batch = queue.splice(0, MAX_QUEUE);
|
|
var body = JSON.stringify({ events: batch });
|
|
try {
|
|
if (useBeacon && navigator.sendBeacon) {
|
|
var blob = new Blob([body], { type: "application/json" });
|
|
navigator.sendBeacon(ENDPOINT, blob);
|
|
return;
|
|
}
|
|
} catch (e) { /* fall through */ }
|
|
try {
|
|
fetch(ENDPOINT, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
body: body,
|
|
keepalive: true,
|
|
credentials: "same-origin"
|
|
}).catch(function () { /* ignore */ });
|
|
} catch (err) { /* ignore */ }
|
|
}
|
|
|
|
function trackPageview() {
|
|
pushEvent({
|
|
type: "pageview",
|
|
path: location.pathname,
|
|
device: deviceBucket(),
|
|
ts: Date.now()
|
|
});
|
|
}
|
|
|
|
function onClick(event) {
|
|
var target = event.target;
|
|
if (!(target instanceof Element)) return;
|
|
if (target.closest("input, textarea, select, [contenteditable='true']")) return;
|
|
|
|
var size = pageSize();
|
|
var x = event.pageX;
|
|
var y = event.pageY;
|
|
if (typeof x !== "number" || typeof y !== "number") return;
|
|
|
|
pushEvent({
|
|
type: "click",
|
|
path: location.pathname,
|
|
device: deviceBucket(),
|
|
x: Math.max(0, Math.min(100, (x / size.width) * 100)),
|
|
y: Math.max(0, Math.min(100, (y / size.height) * 100)),
|
|
ts: Date.now()
|
|
});
|
|
}
|
|
|
|
function onMouseMove(event) {
|
|
var now = Date.now();
|
|
if (now - lastMoveAt < MOVE_THROTTLE_MS) return;
|
|
lastMoveAt = now;
|
|
var size = pageSize();
|
|
var x = event.pageX;
|
|
var y = event.pageY;
|
|
if (typeof x !== "number" || typeof y !== "number") return;
|
|
var xPct = Math.max(0, Math.min(100, (x / size.width) * 100));
|
|
var yPct = Math.max(0, Math.min(100, (y / size.height) * 100));
|
|
var cx = Math.min(MOVE_GRID - 1, Math.max(0, Math.floor((xPct / 100) * MOVE_GRID)));
|
|
var cy = Math.min(MOVE_GRID - 1, Math.max(0, Math.floor((yPct / 100) * MOVE_GRID)));
|
|
var key = cx + "," + cy;
|
|
moveBuckets[key] = (moveBuckets[key] || 0) + 1;
|
|
}
|
|
|
|
function onScroll() {
|
|
var pct = scrollPct();
|
|
if (pct > maxScroll) maxScroll = pct;
|
|
}
|
|
|
|
function flushScroll() {
|
|
if (maxScroll <= 0) return;
|
|
pushEvent({
|
|
type: "scroll",
|
|
path: location.pathname,
|
|
device: deviceBucket(),
|
|
scroll: maxScroll,
|
|
ts: Date.now()
|
|
});
|
|
}
|
|
|
|
function scheduleFlush() {
|
|
if (flushTimer) return;
|
|
flushTimer = setInterval(function () {
|
|
flushScroll();
|
|
flush(false);
|
|
}, FLUSH_MS);
|
|
}
|
|
|
|
function start() {
|
|
if (started) return;
|
|
if (excludedPath(location.pathname)) return;
|
|
if (!hasAnalyticsConsent()) return;
|
|
started = true;
|
|
maxScroll = scrollPct();
|
|
trackPageview();
|
|
document.addEventListener("click", onClick, true);
|
|
document.addEventListener("mousemove", onMouseMove, { passive: true });
|
|
window.addEventListener("scroll", onScroll, { passive: true });
|
|
document.addEventListener("visibilitychange", function () {
|
|
if (document.visibilityState === "hidden") {
|
|
flushScroll();
|
|
flush(true);
|
|
}
|
|
});
|
|
window.addEventListener("pagehide", function () {
|
|
flushScroll();
|
|
flush(true);
|
|
});
|
|
scheduleFlush();
|
|
}
|
|
|
|
window.mltvSiteAnalyticsStart = start;
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", start);
|
|
} else {
|
|
start();
|
|
}
|
|
})();
|