Replay: rrweb `Mirror.removeNodeFromMap` never descends into shadow roots, so every removed web component leaks its shadow tree
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 2/5
- Tiempo estimado
- 1-3 horas
- Aptitud para principiantes
- 85/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- javascript, typescript
- Área
- frontend, performance
Línea de trabajo
Comienza en packages/rrweb-snapshot/src/utils.ts, en Mirror.removeNodeFromMap y junto a los helpers existentes isNativeShadowDom e isShadowRoot. Ejecuta la reproducción mínima con un custom element y verifica que eliminar un shadow host también elimine sus nodos serializados del shadow tree de idNodeMap, de modo que los objetos ShadowRoot separados ya no se conserven.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Is there an existing issue for this?
- I have checked for existing issues https://github.com/getsentry/sentry-javascript/issues
- I have reviewed the documentation https://docs.sentry.io/
- I am using the latest SDK release https://github.com/getsentry/sentry-javascript/releases
(observed on 10.63.0; verified that 10.74.0 bundles the identical code via@sentry/rrweb-snapshot2.43.2, and that 2.44.0 and rrweb master still contain the same method)
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
- Start Replay on a page that renders open-shadow-root custom elements (Stencil, Lit, or the plain element above).
- Re-render a region repeatedly so that shadow hosts are removed and recreated (table paging, list refresh, the interval above).
- Take a heap snapshot after a few hundred re-renders and look at the retainers of the detached
ShadowRootobjects, or count entries of the rrwebMirror.idNodeMap(the object withidNodeMap: MapandnodeMetaMap: 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/rrwebmaster and ingetsentry/rrwebas well;isNativeShadowDom/isShadowRoothelpers are already available in the same file. A fix is to recurse into(n as Element).shadowRootinremoveNodeFromMapin addition tochildNodes. 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).
- Lenguaje dominante
- TypeScript
- Estrellas
- 8.7k
- Forks
- 1.9k
- Merge medio
- 1 d 16 h
- PR fusionados (30 d)
- 576
Guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de getsentry/sentry-javascript
-
Browser Bug Next.js Traces Waiting for: Product Owner
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
getsentry/sentry-javascript#24672 · 1 comentario ·
-
javascript
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
getsentry/sentry-javascript#24200 · 2 comentarios ·
-
javascript Task
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
getsentry/sentry-javascript#24134 · 1 comentario ·
-
Cloudflare Workers javascript Tests
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
getsentry/sentry-javascript#24051 · 1 comentario ·
-
Bug Bun javascript
Dificultad 2/5 1-3 horas Aptitud para principiantes 92/100
getsentry/sentry-javascript#24045 · 1 comentario ·
Todos los issues de getsentry/sentry-javascript
Issues similares
-
bug(cli): hapi doctor inline-media prints a fabricated B:\ helper-script path in packaged installs Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
-
Crush Abierto
Dificultad 1/5 Menos de una hora Aptitud para principiantes 85/100
catppuccin/catppuccin#3125 ·
-
Add a SECURITY.md Abierto
Dificultad 1/5 Menos de una hora Aptitud para principiantes 90/100
ElementsProject/cln-application#167 · 1 comentario · 1 reacción ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
Quantco/pnpm-licenses#17 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100