Skip to content

Session replay canvas freezes mid-session while the event timeline keeps advancing (rrweb recorder/replayer version skew + chunk-ordering) #2569

Description

@cory-dragonfly

Session replay canvas freezes mid-session while the event timeline keeps advancing (rrweb recorder/replayer version skew + chunk-ordering)

Summary

Session replays freeze on the canvas partway through a session — the visual playback stops updating while the event/timeline list keeps advancing. It reproduces most reliably on pages that open a modal/overlay (lots of DOM mutations). We traced it to two independent, mutually-reinforcing root causes in the replay path:

  • A. rrweb recorder/replayer major-version skew — the browser SDK records with rrweb@1.1.3, but the app replays with rrweb@2.0.0-alpha.8.
  • B. Chunk reassembly ordered by timestamp only (no chunk-index tiebreak) + silent event-drop in DOMPlayer.tsx.

Root cause B is independently fixable and does not depend on the rrweb-version decision in A — you may want to split it into its own issue.

Environment / versions

Component Package rrweb version
Recorder (browser SDK) @hyperdx/browser@0.24.0@hyperdx/otel-web-session-recorder@2.0.0 rrweb@1.1.3
Replayer (app UI) @hyperdx/app (main, 2.29.0) → packages/app/package.json rrweb@2.0.0-alpha.8
  • Storage/query backend: ClickHouse (self-hosted ClickStack), rrweb events routed via the logs/out-rrweb collector pipeline.
  • Session source Timestamp column configured to the ingest timestamp column.

Symptom

  • Replay canvas freezes at a stable point mid-session (for us, consistently right after a modal opens).
  • The events/timeline panel continues to progress past the freeze point — because it is driven by a separate metadata query, not by the rrweb replayer's mirror state.
  • No error is visible in the UI (see Root cause B — errors are swallowed unless ?debug=true).

Evidence

Using a metadata query over the recorded rrweb rows for one frozen session, we can see a node that is added and then referenced ~3s later, yet the replayer reports it missing at the reference time (the classic Node with id X not found divergence):

node <id> first added at 12:08:54.605 (in a ~40 KB, multi-chunk mutation event)
same node referenced at 12:08:57.5xx
=> replayer never has the node in its mirror; canvas freezes at ~12:08:57

The add event is present in storage, so the data is complete — the replayer simply never applied it (or applied a corrupted reassembly of it).

Root cause A — rrweb recorder/replayer version skew

The recorder emits events with rrweb@1.1.3 semantics, but the replayer is rrweb@2.0.0-alpha.8. Between 1.x and 2.x, rrweb replaced the INode/node.__sn model with a Mirror as the source of truth (rrweb-io/rrweb#868, a documented breaking change). Replaying 1.x-recorded incremental mutations on a 2.x-alpha Mirror can diverge and throw Node with id X not found on incremental snapshot events. Related upstream reports on adjacent versions:

The recorder has never moved off rrweb@1.1.3 (still 1.1.3 in the latest next build), and the app replayer has been 2.0.0-alpha.8 unchanged. Upstream rrweb has since shipped 2.0.0, 2.0.1, and 2.1.0.

Decision needed from maintainers: align the two sides — either (a) upgrade the recorder (packages/session-recorder) to rrweb 2.x and the replayer to a matching stable 2.x, or (b) pin the replayer back to a 1.1.3-compatible line so it matches already-recorded data. This affects whether existing recordings replay correctly, so it's a call only the maintainers should make.

Root cause B — chunk reassembly ordered by timestamp only + silent drop

Large rrweb events are split into chunks (rr-web.chunk / rr-web.total-chunks). The replay stream query selects these:

  • packages/app/src/sessions.ts:392-397 — selects rr-web.chunk AS ck, rr-web.total-chunks AS tcks.
  • packages/app/src/sessions.ts:411orderBy: \${source.timestampValueExpression} ASC`— **ordered by timestamp only, with nock` (chunk index) tiebreak.**

Chunks of a single event are written with the same timestamp, so ClickHouse may return them out of order. The player then concatenates and parses them:

  • packages/app/src/DOMPlayer.tsx:145currentRrwebEvent += body;
  • packages/app/src/DOMPlayer.tsx:147const parsedEvent = JSON.parse(currentRrwebEvent); (only when !chunk || chunk === totalChunks)
  • packages/app/src/DOMPlayer.tsx:150replayer.current.addEvent(parsedEvent);
  • packages/app/src/DOMPlayer.tsx:171-176catch (e) { if (debug) { console.error(e); } currentRrwebEvent = ''; }

Two failure modes fall out of this:

  1. Out-of-order chunks → corrupted JSONJSON.parse throws → the entire event is dropped. If the dropped event is a full snapshot or a key mutation (like the ~40 KB add above), the mirror is missing nodes and every later reference throws Node with id X not found.
  2. addEvent throws are swallowed — the addEvent call is inside the same try, and in production (debug false) the error is silently discarded, the buffer reset, and playback continues with a permanently diverged mirror. That's why the canvas freezes while the timeline keeps moving, with no visible error.

Suggested fix for B (version-independent)

  • Add a chunk-index tiebreak to the replay query ordering, e.g. ORDER BY <timestamp> ASC, ck ASC (and/or explicitly sort by ck before concatenating in DOMPlayer.tsx).
  • Stop silently dropping on addEvent failure: surface the error outside debug, and attempt recovery (re-seek / rebuild from the last full snapshot) instead of continuing with a diverged mirror.

What we're asking for

  1. Confirmation of the intended rrweb version direction (Root cause A) so recorder and replayer are aligned.
  2. Consideration of the chunk-ordering tiebreak + non-silent error handling (Root cause B), which is safe to fix independently and would reduce freezes even before the version alignment lands.

Happy to provide additional metadata dumps or test against a specific build. Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions