From fb2914d8538de9e4829079c7076d63460ea57d1e Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 25 Sep 2026 18:22:43 +0000 Subject: [PATCH] Don't accrue reading time across sleep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inactivity cutoff timer cannot fire while the computer is asleep, and performance.now() keeps running through sleep on some platforms, so after a wake the whole slept span could be credited as reading time (inflating min/visit stats). A gap longer than the inactivity timeout now closes the span at its intended cutoff, and the cutoff timer clamps its accumulation to its scheduled fire time — covering both event-first and timer-first wake orderings. --- docs/analytics.md | 6 +++++- frontend/src/pagerite.js | 24 +++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) 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);