/* TuringDNA dwell-time heartbeat. * * Measures how long a user actually spends ON the engine (foreground time), * which the server-side run/event timestamps can't capture — they only mark * discrete actions, not the reading/thinking gaps between them. * * Posts { session_id, visible_ms } to /api/ping: * - an initial beat ~3s after load (captures the user_id early, so even a * short visit gets attributed before they leave), * - then every ~30s WHILE the tab is visible (background tabs are skipped — * we only count foreground time), and * - once more on tab-hide / pagehide via navigator.sendBeacon (survives the * tab going away, where a normal fetch would be cancelled). * * visible_ms is CUMULATIVE foreground time for the session; the server keeps the * max, so duplicate or out-of-order beats are harmless. auth.js has already * wrapped fetch to attach the JWT on /api/* calls, so periodic beats are * attributed to the signed-in user. The sendBeacon fallback is unauthenticated * (anonymous), but the server preserves an already-known user_id on that row. * * Privacy: sends only an opaque random session id + a millisecond duration. * No sequence content, no page paths, no identity — matches PRIVACY.md. */ (function () { 'use strict'; var PING_MS = 30000; // periodic cadence while visible var INITIAL_MS = 3000; // first beat — early user_id capture for short visits function uuid() { try { if (window.crypto && crypto.randomUUID) return crypto.randomUUID(); } catch (e) {} return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } var sessionId = uuid(); var visibleMs = 0; // accumulated foreground time from FINISHED segments var segStart = (document.visibilityState === 'visible') ? Date.now() : 0; function currentVisibleMs() { var ongoing = segStart ? (Date.now() - segStart) : 0; return Math.round(visibleMs + ongoing); } function send(useBeacon) { var body = JSON.stringify({ session_id: sessionId, visible_ms: currentVisibleMs() }); if (useBeacon && navigator.sendBeacon) { try { navigator.sendBeacon('/api/ping', new Blob([body], { type: 'application/json' })); return; } catch (e) { /* fall through to fetch */ } } try { fetch('/api/ping', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: body, keepalive: true, }).catch(function () {}); } catch (e) { /* ignore — telemetry is best-effort */ } } // Close the foreground segment on hide (and flush via beacon); reopen on show. document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'hidden') { if (segStart) { visibleMs += Date.now() - segStart; segStart = 0; } send(true); } else if (document.visibilityState === 'visible') { if (!segStart) segStart = Date.now(); } }); // pagehide — last reliable chance to record the tail of the session // (beforeunload is unreliable on mobile; pagehide fires on bfcache too). window.addEventListener('pagehide', function () { if (segStart) { visibleMs += Date.now() - segStart; segStart = 0; } send(true); }); // Initial beat, then steady cadence. setInterval is throttled in background // tabs — fine, since we gate on visibility and only want foreground time. setTimeout(function () { if (document.visibilityState === 'visible') send(false); }, INITIAL_MS); setInterval(function () { if (document.visibilityState === 'visible') send(false); }, PING_MS); })();