Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Replay: rrweb `Mirror.removeNodeFromMap` never descends into shadow roots, so every removed web component leaks its shadow tree

クローズ 初心者向け
#24,577 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
85/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
javascript, typescript

調査の方向性

packages/rrweb-snapshot/src/utils.ts の Mirror.removeNodeFromMap から始め、既存の isNativeShadowDom および isShadowRoot ヘルパーの近くを確認します。最小限の custom-element reproduction を実行し、shadow host を削除すると、そのシリアライズされた shadow-tree ノードも idNodeMap から削除され、分離された ShadowRoot オブジェクトが保持されなくなることを確認します。

索引モデルが issue の本文から書いたものです。

説明

Browser Waiting for: Product Owner
Is there an existing issue for this?
How do you use Sentry?

Self-hosted/on-premise

Which SDK are you using?

@sentry/browser

SDK Version

10.63.0 (code unchanged in 10.74.0)

Framework Version

No framework. Server-rendered Symfony app, Stimulus controllers, UI built from Stencil web components (open shadow roots).

Link to Sentry event

n/a (memory growth, no event)

Reproduction Example/SDK Setup
Sentry.init({
  dsn: __YOUR_DSN__,
  integrations: [Sentry.browserTracingIntegration()],
  tracesSampleRate: 1,
  replaysSessionSampleRate: 0,
  replaysOnErrorSampleRate: 1.0,
});

// added later on requestIdleCallback
Sentry.addIntegration(Sentry.replayIntegration({
  maskAllText: true,
  maskAllInputs: true,
  blockAllMedia: true,
}));

Minimal reproduction without any framework:

<script>
  customElements.define('x-box', class extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' }).innerHTML =
        '<style>:host{display:block}</style><span><slot></slot></span>';
    }
  });
</script>
<div id="host"></div>
<script>
  // start Replay first (buffer or session mode, both affected), then:
  const host = document.getElementById('host');
  let i = 0;
  setInterval(() => {
    host.innerHTML = '';
    for (let k = 0; k < 100; k++) {
      const el = document.createElement('x-box');
      el.textContent = 'item ' + (i++);
      host.appendChild(el);
    }
  }, 500);
</script>
Steps to Reproduce
  1. Start Replay on a page that renders open-shadow-root custom elements (Stencil, Lit, or the plain element above).
  2. Re-render a region repeatedly so that shadow hosts are removed and recreated (table paging, list refresh, the interval above).
  3. Take a heap snapshot after a few hundred re-renders and look at the retainers of the detached ShadowRoot objects, or count entries of the rrweb Mirror.idNodeMap (the object with idNodeMap: Map and nodeMetaMap: WeakMap).
Expected Result

When a shadow host is removed from the document, all nodes rrweb serialized for it, including the nodes inside its shadow root, are dropped from the mirror. Memory returns to baseline once the removed elements are garbage collected.

Actual Result

Mirror.removeNodeFromMap only recurses over n.childNodes and never looks at n.shadowRoot, so the nodes inside the shadow root of every removed host stay in idNodeMap forever (@sentry/replay/build/npm/esm/index.js, removeNodeFromMap, corresponds to packages/rrweb-snapshot/src/utils.ts in rrweb). Because idNodeMap holds strong references, this pins the detached ShadowRoot, its slots, text nodes, style elements and adopted stylesheets for the lifetime of the recording.

Measured on our production admin app (Chrome 140, Stencil components, one datatable being paged programmatically 50 times per run):

before after 50 page changes
live DOM nodes (light + shadow) ~6,000 ~6,000
rrweb idNodeMap entries 12,068 13,580
retained ShadowRoot objects 1,640 3,360

Retained JS heap grows about 1 MB per interaction and never comes back; the leaked map entries are exclusively shadow-tree nodes (<slot>, Text, <span>, <style>, nested hosts). Retainer chain from the heap snapshot:

Window.open (patched closure) → context → Mirror (G) → idNodeMap (Map) → <slot> / <style> → ShadowRoot

Users who keep a tab open for a working day end up with tabs of 3 to 4 GB.

A second, smaller consequence: Replay flips from buffer to session mode after the first captured error, so in practice the recorder, and with it this leak, runs for the rest of the session even with replaysSessionSampleRate: 0.

Additional Context
  • The gap is in rrweb-snapshot and exists in upstream rrweb-io/rrweb master and in getsentry/rrweb as well; isNativeShadowDom/isShadowRoot helpers are already available in the same file. A fix is to recurse into (n as Element).shadowRoot in removeNodeFromMap in addition to childNodes. Happy to open the matching PR against the fork if you prefer to take it there rather than upstream.
  • Related but not covering this: #7474 (shadow DOM support investigation), #6615 (corrupt replay with web components), #14001 (DOM node counts in replay).
主要言語
TypeScript
スター
8.7k
フォーク
1.9k
平均マージ
1日 16時間
マージ済み PR(30日)
576

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

getsentry/sentry-javascript のほかの issue

getsentry/sentry-javascript の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。