Skip to content

[Bug]: Same-origin iframe documents: mutation buffers never released, one buffer per full snapshot, no release on removal/navigation #363

Open

Description

@salzahrani-salla

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):

  1. Buffers are never released from the registry. initMutationObserver pushes a MutationBuffer into the module-level mutationBuffers, 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/Document is never collectable (0/1 collected across removal, churn, and 4-concurrent-frames probes).
  2. One buffer per full snapshot. Every full snapshot re-serializes an existing same-origin iframe and calls attachIframe, which invokes the load listener, which observes iframe.contentDocument again. After 3 buffer-mode checkouts, one attribute change inside the iframe is emitted 4× (baseline: 1). Duplicate emission also grows every observer's MutationBuffer work for the rest of the session.

  3. 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-place src navigation keeps the old realm's events flowing into the new recording (old document still emits after navigation).

  4. getImplementation() pins a sandbox realm (same leak family, different mechanism): the cached untainted setTimeout/requestAnimationFrame implementations are functions bound to a hidden helper iframe's Window, which keeps that realm alive for the client's lifetime (retained chain: getImplementation → bound timer → detached NativeContext/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 getImplementation mechanism 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

  1. linear-code commented on Nov 15, 2026

    @linear-code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions