Aggiunge heatmaps first-party (click/scroll) con pagina admin Analytics.
Raccoglie eventi aggregati dietro consenso cookie, senza PII né tracking su admin/regia/live/replay; GA4 resta invariato. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -935,3 +935,45 @@ body.admin-body {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.admin-scroll-bar {
|
||||
position: relative;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
background: #0d0d12;
|
||||
border: 1px solid var(--card-border);
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.admin-scroll-bar__avg {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #1565c0, #43a047);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.admin-scroll-bar__max {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
bottom: -2px;
|
||||
width: 2px;
|
||||
background: #e53935;
|
||||
transform: translateX(-1px);
|
||||
}
|
||||
|
||||
.admin-heatmap-wrap {
|
||||
overflow: auto;
|
||||
max-height: 70vh;
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 10px;
|
||||
background: #0d0d12;
|
||||
}
|
||||
|
||||
.admin-heatmap {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -70,6 +70,9 @@
|
||||
function applyConsent(consent) {
|
||||
if (consent && consent.analytics) {
|
||||
loadGoogleAnalytics();
|
||||
if (typeof window.mltvSiteAnalyticsStart === "function") {
|
||||
window.mltvSiteAnalyticsStart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/*! Match Live TV first-party site analytics (click + 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 = 8000;
|
||||
var MAX_QUEUE = 40;
|
||||
|
||||
var queue = [];
|
||||
var maxScroll = 0;
|
||||
var flushTimer = null;
|
||||
var started = false;
|
||||
|
||||
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 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 maxScrollable = Math.max(height - view, 1);
|
||||
var pct = ((scrollTop + view) / height) * 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(true);
|
||||
}
|
||||
|
||||
function flush(useBeacon) {
|
||||
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 doc = document.documentElement;
|
||||
var body = document.body;
|
||||
var width = Math.max(body.scrollWidth || 0, doc.scrollWidth || 0, window.innerWidth || 1);
|
||||
var height = Math.max(body.scrollHeight || 0, doc.scrollHeight || 0, window.innerHeight || 1);
|
||||
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 / width) * 100)),
|
||||
y: Math.max(0, Math.min(100, (y / height) * 100)),
|
||||
ts: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user