Don't accrue reading time across sleep

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.
This commit is contained in:
LeoVasanko committed 2026-09-25 18:22:43 +00:00
1 parent d4add54f10
commit fb2914d853
2 files changed
+26 -4

No files matched your search

+5 -1
View File
@@ -119,7 +119,11 @@ transition applies the new `<html lang>` 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
+21 -3
View File
@@ -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);