syntheogenesis / dee /static /telemetry.js
Tengo Gzirishvili
Analytics (events/dwell/run-completion) + sidebar sign-in row
9856abf
Raw
History Blame Contribute Delete
3.94 kB
/* 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);
})();