/*! 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(); } })();