Repository navigation
Description
Preflight Checklist
- I have searched the rrweb-io/rrweb and getsentry/rrweb issue trackers for a matching report without success.
What package is this bug report for?
rrweb
Version
getsentry/rrweb, branch sentry-v2, commit 3e057b3 (vendored as the rrweb fork inside @sentry/replay@11.12.0, fork version line 2.46.0)
Expected Behavior
Recording a document that contains (or gains) same-origin iframes should release all per-document recording state when the recording stops, when an iframe element is removed, and when an iframe navigates in place. A same-origin iframe document should be observed exactly once for the lifetime of its current document.
Actual Behavior
Measured with a real-Chrome regression harness (mountable same-origin iframes + shadow hosts, recording active in the parent, forced GC via CDP, WeakRef probes on the removed documents/hosts):
-
Buffers are never released from the registry.
initMutationObserverpushes aMutationBufferinto the module-levelmutationBuffers, and nothing removes it except the shadow-root teardown path added in fix(rrweb): tear down shadow-DOM observers when hosts are removed #337.initObservers' stop handler resets every buffer and disconnects only its own observer, so:- after
stop(), the recording's own buffer stays registered; - after an iframe element is removed, its document's buffer stays registered, and the detached
Window/Documentis never collectable (0/1 collected across removal, churn, and 4-concurrent-frames probes).
- after
-
One buffer per full snapshot. Every full snapshot re-serializes an existing same-origin iframe and calls
attachIframe, which invokes the load listener, which observesiframe.contentDocumentagain. After 3 buffer-mode checkouts, one attribute change inside the iframe is emitted 4× (baseline: 1). Duplicate emission also grows every observer'sMutationBufferwork for the rest of the session. -
No release on removal or navigation. Detached shadow roots are released through the document buffer's removal loop (
removeShadowRootsFor), but iframe documents have no equivalent path: removing the element leaves the observer connected and the buffer registered; in-placesrcnavigation keeps the old realm's events flowing into the new recording (old document still emits after navigation). -
getImplementation()pins a sandbox realm (same leak family, different mechanism): the cached untaintedsetTimeout/requestAnimationFrameimplementations are functions bound to a hidden helper iframe'sWindow, which keeps that realm alive for the client's lifetime (retained chain:getImplementation→ bound timer → detachedNativeContext/Window; ~800 KB per cached implementation in production heaps).
Steps to Reproduce
The linked PR adds four jsdom regression tests that encode items 1–3 and fail on current sentry-v2 (packages/rrweb/test/record/iframe-observer-cleanup.test.ts). Real-browser reproduction: mount a same-origin iframe, start recording, take 3 full snapshots, remove the iframe, force GC — the child realm stays strongly reachable through the module registry.
Additional Information
- The shadow-root half of this leak family was fixed by fix(snapshot): release removed shadow trees from the mirror #331/fix(rrweb): tear down shadow-DOM observers when hosts are removed #337; these are the remaining classes on the same code paths.
- rrweb-io already has the registry-release + observe-once + unload-disposal behavior on main (#1971, released in rrweb-io 2.2.0, with open follow-ups #1963 and #1975); rrweb-io also replaced the
getImplementationmechanism with@rrweb/utils' untainted-prototype machinery, which avoids the helper iframe entirely on browsers with native prototypes. The failing tests in the PR are the contract a backport should satisfy. - Probe-by-probe transcripts and heap snapshots for the measurements above are available on request. The probes are self-contained and runnable locally without any credentials.
Activity
linear-code commentedon Nov 15, 2026
RRWEB-273