diff --git a/docs/analytics.md b/docs/analytics.md index 36bad59..dc20e2d 100644 --- a/docs/analytics.md +++ b/docs/analytics.md @@ -119,7 +119,11 @@ transition applies the new `` only after the ping goes out) and follows the session, so the activity of a visit stays tied together, and while the user is active the accumulated reading time is flushed every few seconds: the times are incremental, so a disconnection simply leaves the -last reported time in place (no close beacon). After 5 minutes without +last reported time in place (no close beacon). A sleeping computer accrues +no reading time: the inactivity-cutoff timer cannot fire while asleep (and +`performance.now()` keeps running through sleep on some platforms), so a +gap longer than the timeout closes the span at its intended cutoff, and a +late-firing cutoff timer clamps its accumulation the same way. After 5 minutes without any activity the client closes the socket itself — a sleeping browser tab would lose it anyway — and the next activity reconnects; reconnects are attempted only on user activity, with an exponential backoff between diff --git a/frontend/src/pagerite.js b/frontend/src/pagerite.js index 2f4c2ec..770b117 100644 --- a/frontend/src/pagerite.js +++ b/frontend/src/pagerite.js @@ -684,16 +684,34 @@ import { reconnectPolicy, socketSlot, watchConnecting } from "./reconnect"; let reading = true; let readInactivityTimer = null; let idleTimer = null; + let lastActivityAt = performance.now(); function markReadActivity() { - if (!reading) { + const t = performance.now(); + if (reading) { + // A gap longer than the inactivity timeout means the cutoff timer + // could not run on time (the computer slept, or the tab was throttled + // hard): close the span at its intended cutoff — the gap itself was + // not reading time, and performance.now() keeps running through sleep + // on some platforms. + const cutoff = lastActivityAt + INACTIVE_MS; + if (t > cutoff) { + readElapsed += Math.max(0, cutoff - readStart); + readStart = t; + } + } else { reading = true; - readStart = performance.now(); + readStart = t; } + lastActivityAt = t; clearTimeout(readInactivityTimer); + // Same sleep guard, other ordering: when this timer itself fires late + // (waking from sleep before any user event), clamp the accumulation to + // its intended fire time. + const stopAt = t + INACTIVE_MS; readInactivityTimer = setTimeout(() => { if (reading) { - readElapsed += performance.now() - readStart; + readElapsed += Math.max(0, Math.min(performance.now(), stopAt) - readStart); reading = false; } }, INACTIVE_MS);