feat(replay): count attribute mutations the throttler drops (#4658)
* fix(replay): heal recordings when the mutation throttler drops attributes The mutation throttler drops per-node attribute mutations once a node burns through its token bucket. A dropped class or style change (e.g. the one that hides an outgoing subtree) never reaches the player, so replay shows DOM that already left the live page. Nothing counted the drop and nothing healed it. Count dropped attribute mutations and, while awake, take a debounced full snapshot to re-sync the player's mirror. The debounce coalesces a dense burst into one snapshot and leaves sustained animation to the periodic snapshot. Report the count as `$sdk_debug_replay_throttled_mutations_dropped` so the path is measurable. Also fix `BucketedRateLimiter.consumeRateLimit`, which returned `true` on the call that spent the last token and so rate limited one request more than the bucket size. Generated-By: PostHog Desktop Task-Id: e2bd5bea-bdff-4ffb-ac49-eb5ff8f2cd7c Generated-By: PostHog Desktop Task-Id: e2bd5bea-bdff-4ffb-ac49-eb5ff8f2cd7c * fix(replay): reset throttler drop count on session rotation _throttledMutationsDropped is reported as $sdk_debug_replay_throttled_mutations_dropped with a comment promising a count "cumulative across the session", but _restartForSessionIdChange reset only the snapshot cost metrics and left this accumulator alone. A long-lived tab that rotates (activity timeout or 24h cap) then carried the old session's drop count into the new session's events, so a session with no drops reported a non-zero count and a session with drops reported an inflated one — sending an engineer to a recording that has no duplicate-DOM risk. Reset the counter alongside the other per-session metrics, after the old recorder stops (so its teardown flush still attributes drops to the old session) and before the new one starts. Adds a rotation test asserting the new session starts at zero. Files affected: packages/browser (web) Generated-By: PostHog Desktop Task-Id: f7ba0fd1-833d-4278-a0ba-f62adf476e34 * fix(replay): heal throttler drops for trigger-pending recordings The awake throttle-drop heal only fired when status was 'sampled' or 'active', so a trigger-pending recorder (status 'buffering') got no heal. But a trigger-pending buffer ships on activation via _activateTrigger, which flushes the held buffer without taking a fresh snapshot — so if a URL/event/linked-flag trigger fired before the next pending- interval snapshot, the shipped recording still showed the dropped-attribute duplicate DOM this PR exists to remove. The sibling idle-wake heal already handles this exact state with `['sampled', 'active'].includes(status) || _strategy?.hasPendingTriggers(sessionId)`. Align the throttle-drop heal's gate with that same bufferCanShip condition so the two heals in the same file agree. Taking a full snapshot while trigger-pending re-bases the buffer via _clearBufferBeforeMostRecentMeta — the established pending-buffer behaviour, not a new side effect. Adds a parametrized test covering both buffering cases (pending trigger heals, no pending trigger does not). Files affected: packages/browser (web) Generated-By: PostHog Desktop Task-Id: f7ba0fd1-833d-4278-a0ba-f62adf476e34 * fix(replay): don't fire throttle heal snapshot after going idle The throttle-drop heal timer only checked _isIdle when scheduling, not when firing. The same rrweb emit that reports a dropped attribute (via the throttler callback) runs the idle check just afterwards, so one event can schedule the heal and then flip the recorder to idle. The idle transition cleared only _fullSnapshotTimer, leaving the heal timer to fire ~1s later and run a full DOM snapshot — the synchronous "freeze a user would notice" task — on an idle page, where the idle gate then discards the event. Dense throttler bursts on animating pages make a pending timer likely at exactly the moment of the flip. Two-part fix, matching the file's own conventions: - the timer callback bails out when _isIdle === true (fire-time safety net), and - the idle transition cancels the pending heal timer alongside _fullSnapshotTimer. The mirror still repairs itself on wake via the existing wake heal. Adds tests for both the clear-on-idle path and the callback's fire-time guard. Files affected: packages/browser (web) Generated-By: PostHog Desktop Task-Id: f7ba0fd1-833d-4278-a0ba-f62adf476e34 * refactor(replay): count throttler-dropped mutations without the heal snapshot Drops the debounced full-snapshot heal and the BucketedRateLimiter off-by-one, leaving only the drop counter and its debug property. * chore(changeset): bump to minor for the new debug property * chore(changeset): reword for developers reading the changelog * chore: regenerate mangled property names * chore(replay): prove the drop-count reset against the path that increments it --------- Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com> Co-authored-by: Anna Garcia <anna.g@posthog.com> Co-authored-by: Tue Haulund <tue@posthog.com>
P
posthog[bot] committed
db8687c46bc34d1364c092a030adaef7c67c6c67
Parent: 1959a2e
Committed by GitHub <noreply@github.com>
on 8/27/2026, 1:54:47 PM