fix(browser): Clean up pageload readystatechange listener - #21632

Merged
andreiborza merged 2 commits into
developfrom
fix/browser-tracing-readystatechange-listener-leak
Jun 18, 2026
Merged

fix(browser): Clean up pageload readystatechange listener#21632
andreiborza merged 2 commits into
developfrom
fix/browser-tracing-readystatechange-listener-leak

Conversation

@andreiborza

Copy link
Copy Markdown
Member

The auto-finish readystatechange listener registered for each pageload span in browserTracingIntegration was never removed. Since the handler closes over the idleSpan (and the rest of the route-span closure), every pageload leaked a listener and kept the span it retained alive — the growing set of Sentry functions reported in #21630.

The previous handler was an anonymous arrow function, so it could not be removed via removeEventListener. The { once: true } option suggested in the issue does not fully address the leak either: in the common case where the document is already loaded when the span starts, the listener never fires and therefore is never auto-removed.

This change passes emitFinish directly as the handler and removes it as soon as the auto-finish signal is emitted. This covers both cases:

  • Document already loaded when the span starts → signal emitted synchronously, listener removed immediately.
  • Document still loading → listener fires on the next readystatechange, emits, and removes itself.

Since the framework integrations (Vue, Astro, Next.js, Remix, SvelteKit, Ember) all wrap the browser package's browserTracingIntegration, this single fix covers them too.

Root cause

optionalWindowDocument.addEventListener('readystatechange', () => { emitFinish(); }) added a fresh, unremovable listener for every pageload span without ever detaching it.

Fixes#21630

The auto-finish `readystatechange` listener registered for each pageload
span was never removed. Because it closes over the `idleSpan` (and the
rest of the route-span closure), every pageload leaked a listener and the
span it retained. The previous anonymous handler could not be removed, and
the suggested `{ once: true }` would not help in the common case where the
document is already loaded when the span starts, since the listener never
fires.
Pass `emitFinish` directly as the handler and remove it as soon as the
auto-finish signal is emitted, covering both the already-loaded and
load-later cases.
Fixes#21630
Co-Authored-By: Opus 4.8 <noreply@anthropic.com>
@andreiborza
andreiborza requested a review from a team as a code ownerJune 18, 2026 11:55
@andreiborza
andreiborza requested review from Lms24 and logaretm and removed request for a team, Lms24 and logaretmJune 18, 2026 11:55
@andreiborza
andreiborza marked this pull request as draft June 18, 2026 12:01
@github-actions

github-actionsBot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.45 kB--
@sentry/browser - with treeshaking flags25.88 kB--
@sentry/browser (incl. Tracing)45.89 kB+0.03%+12 B 🔺
@sentry/browser (incl. Tracing + Span Streaming)48.12 kB+0.03%+11 B 🔺
@sentry/browser (incl. Tracing, Profiling)50.67 kB+0.03%+14 B 🔺
@sentry/browser (incl. Tracing, Replay)85.08 kB+0.01%+4 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.69 kB+0.01%+2 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas)89.78 kB+0.01%+6 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback)102.45 kB+0.01%+7 B 🔺
@sentry/browser (incl. Feedback)44.62 kB--
@sentry/browser (incl. sendFeedback)32.25 kB--
@sentry/browser (incl. FeedbackAsync)37.38 kB--
@sentry/browser (incl. Metrics)28.52 kB--
@sentry/browser (incl. Logs)28.76 kB--
@sentry/browser (incl. Metrics & Logs)29.45 kB--
@sentry/react29.25 kB--
@sentry/react (incl. Tracing)48.18 kB+0.02%+5 B 🔺
@sentry/vue32.56 kB--
@sentry/vue (incl. Tracing)47.76 kB+0.03%+10 B 🔺
@sentry/svelte27.48 kB--
CDN Bundle29.86 kB--
CDN Bundle (incl. Tracing)48.29 kB+0.03%+11 B 🔺
CDN Bundle (incl. Logs, Metrics)31.4 kB--
CDN Bundle (incl. Tracing, Logs, Metrics)49.59 kB+0.03%+11 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics)70.71 kB--
CDN Bundle (incl. Tracing, Replay)85.62 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.88 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback)91.46 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.71 kB+0.01%+8 B 🔺
CDN Bundle - uncompressed88.8 kB--
CDN Bundle (incl. Tracing) - uncompressed146.08 kB+0.03%+38 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed93.5 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed150.06 kB+0.03%+38 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.33 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed264.95 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed268.91 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed278.65 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed282.6 kB+0.02%+38 B 🔺
@sentry/nextjs (client)50.58 kB+0.01%+2 B 🔺
@sentry/sveltekit (client)46.27 kB+0.02%+9 B 🔺
@sentry/core/server76.16 kB--
@sentry/core/browser63.31 kB--
@sentry/node-core61.87 kB-0.01%-1 B 🔽
@sentry/node127.69 kB--
@sentry/node - without tracing74.26 kB--
@sentry/aws-serverless85.35 kB--
@sentry/cloudflare (withSentry) - minified174.48 kB--
@sentry/cloudflare (withSentry)436.52 kB--

View base workflow run

optionalWindowDocument.addEventListener('readystatechange', () => {
emitFinish();
});
optionalWindowDocument.addEventListener('readystatechange', emitFinish);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't { once: true } suffice?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually never mind, you want to remove it once it is called down there as well.

@andreiborza
andreiborza marked this pull request as ready for review June 18, 2026 15:24
@andreiborza

Copy link
Copy Markdown
MemberAuthor

For the record: I'm not actually sure this fixes an actual leak in a real-world scenario, because it seems strange to me that we'd have more than one pageload span. That being said, cleaning up here seems like good form.

@andreiborza
andreiborza merged commit d64420f into developJun 18, 2026
368 of 372 checks passed
@andreiborza
andreiborza deleted the fix/browser-tracing-readystatechange-listener-leak branch June 18, 2026 15:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

memory leak in browserTracingIntegration.ts

3 participants

@andreiborza@logaretm@Lms24
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(browser): Clean up pageload readystatechange listener - #21632

Merged
andreiborza merged 2 commits into
developfrom
fix/browser-tracing-readystatechange-listener-leak
Jun 18, 2026
Merged

fix(browser): Clean up pageload readystatechange listener#21632
andreiborza merged 2 commits into
developfrom
fix/browser-tracing-readystatechange-listener-leak

Conversation

@andreiborza

Copy link
Copy Markdown
Member

The auto-finish readystatechange listener registered for each pageload span in browserTracingIntegration was never removed. Since the handler closes over the idleSpan (and the rest of the route-span closure), every pageload leaked a listener and kept the span it retained alive — the growing set of Sentry functions reported in #21630.

The previous handler was an anonymous arrow function, so it could not be removed via removeEventListener. The { once: true } option suggested in the issue does not fully address the leak either: in the common case where the document is already loaded when the span starts, the listener never fires and therefore is never auto-removed.

This change passes emitFinish directly as the handler and removes it as soon as the auto-finish signal is emitted. This covers both cases:

  • Document already loaded when the span starts → signal emitted synchronously, listener removed immediately.
  • Document still loading → listener fires on the next readystatechange, emits, and removes itself.

Since the framework integrations (Vue, Astro, Next.js, Remix, SvelteKit, Ember) all wrap the browser package's browserTracingIntegration, this single fix covers them too.

Root cause

optionalWindowDocument.addEventListener('readystatechange', () => { emitFinish(); }) added a fresh, unremovable listener for every pageload span without ever detaching it.

Fixes#21630

The auto-finish `readystatechange` listener registered for each pageload
span was never removed. Because it closes over the `idleSpan` (and the
rest of the route-span closure), every pageload leaked a listener and the
span it retained. The previous anonymous handler could not be removed, and
the suggested `{ once: true }` would not help in the common case where the
document is already loaded when the span starts, since the listener never
fires.
Pass `emitFinish` directly as the handler and remove it as soon as the
auto-finish signal is emitted, covering both the already-loaded and
load-later cases.
Fixes#21630
Co-Authored-By: Opus 4.8 <noreply@anthropic.com>
@andreiborza
andreiborza requested a review from a team as a code ownerJune 18, 2026 11:55
@andreiborza
andreiborza requested review from Lms24 and logaretm and removed request for a team, Lms24 and logaretmJune 18, 2026 11:55
@andreiborza
andreiborza marked this pull request as draft June 18, 2026 12:01
@github-actions

github-actionsBot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.45 kB--
@sentry/browser - with treeshaking flags25.88 kB--
@sentry/browser (incl. Tracing)45.89 kB+0.03%+12 B 🔺
@sentry/browser (incl. Tracing + Span Streaming)48.12 kB+0.03%+11 B 🔺
@sentry/browser (incl. Tracing, Profiling)50.67 kB+0.03%+14 B 🔺
@sentry/browser (incl. Tracing, Replay)85.08 kB+0.01%+4 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.69 kB+0.01%+2 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas)89.78 kB+0.01%+6 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback)102.45 kB+0.01%+7 B 🔺
@sentry/browser (incl. Feedback)44.62 kB--
@sentry/browser (incl. sendFeedback)32.25 kB--
@sentry/browser (incl. FeedbackAsync)37.38 kB--
@sentry/browser (incl. Metrics)28.52 kB--
@sentry/browser (incl. Logs)28.76 kB--
@sentry/browser (incl. Metrics & Logs)29.45 kB--
@sentry/react29.25 kB--
@sentry/react (incl. Tracing)48.18 kB+0.02%+5 B 🔺
@sentry/vue32.56 kB--
@sentry/vue (incl. Tracing)47.76 kB+0.03%+10 B 🔺
@sentry/svelte27.48 kB--
CDN Bundle29.86 kB--
CDN Bundle (incl. Tracing)48.29 kB+0.03%+11 B 🔺
CDN Bundle (incl. Logs, Metrics)31.4 kB--
CDN Bundle (incl. Tracing, Logs, Metrics)49.59 kB+0.03%+11 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics)70.71 kB--
CDN Bundle (incl. Tracing, Replay)85.62 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.88 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback)91.46 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.71 kB+0.01%+8 B 🔺
CDN Bundle - uncompressed88.8 kB--
CDN Bundle (incl. Tracing) - uncompressed146.08 kB+0.03%+38 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed93.5 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed150.06 kB+0.03%+38 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.33 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed264.95 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed268.91 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed278.65 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed282.6 kB+0.02%+38 B 🔺
@sentry/nextjs (client)50.58 kB+0.01%+2 B 🔺
@sentry/sveltekit (client)46.27 kB+0.02%+9 B 🔺
@sentry/core/server76.16 kB--
@sentry/core/browser63.31 kB--
@sentry/node-core61.87 kB-0.01%-1 B 🔽
@sentry/node127.69 kB--
@sentry/node - without tracing74.26 kB--
@sentry/aws-serverless85.35 kB--
@sentry/cloudflare (withSentry) - minified174.48 kB--
@sentry/cloudflare (withSentry)436.52 kB--

View base workflow run

optionalWindowDocument.addEventListener('readystatechange', () => {
emitFinish();
});
optionalWindowDocument.addEventListener('readystatechange', emitFinish);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't { once: true } suffice?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually never mind, you want to remove it once it is called down there as well.

@andreiborza
andreiborza marked this pull request as ready for review June 18, 2026 15:24
@andreiborza

Copy link
Copy Markdown
MemberAuthor

For the record: I'm not actually sure this fixes an actual leak in a real-world scenario, because it seems strange to me that we'd have more than one pageload span. That being said, cleaning up here seems like good form.

@andreiborza
andreiborza merged commit d64420f into developJun 18, 2026
368 of 372 checks passed
@andreiborza
andreiborza deleted the fix/browser-tracing-readystatechange-listener-leak branch June 18, 2026 15:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

memory leak in browserTracingIntegration.ts

3 participants

@andreiborza@logaretm@Lms24
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(browser): Clean up pageload readystatechange listener - #21632

Merged
andreiborza merged 2 commits into
developfrom
fix/browser-tracing-readystatechange-listener-leak
Jun 18, 2026
Merged

fix(browser): Clean up pageload readystatechange listener#21632
andreiborza merged 2 commits into
developfrom
fix/browser-tracing-readystatechange-listener-leak

Conversation

@andreiborza

Copy link
Copy Markdown
Member

The auto-finish readystatechange listener registered for each pageload span in browserTracingIntegration was never removed. Since the handler closes over the idleSpan (and the rest of the route-span closure), every pageload leaked a listener and kept the span it retained alive — the growing set of Sentry functions reported in #21630.

The previous handler was an anonymous arrow function, so it could not be removed via removeEventListener. The { once: true } option suggested in the issue does not fully address the leak either: in the common case where the document is already loaded when the span starts, the listener never fires and therefore is never auto-removed.

This change passes emitFinish directly as the handler and removes it as soon as the auto-finish signal is emitted. This covers both cases:

  • Document already loaded when the span starts → signal emitted synchronously, listener removed immediately.
  • Document still loading → listener fires on the next readystatechange, emits, and removes itself.

Since the framework integrations (Vue, Astro, Next.js, Remix, SvelteKit, Ember) all wrap the browser package's browserTracingIntegration, this single fix covers them too.

Root cause

optionalWindowDocument.addEventListener('readystatechange', () => { emitFinish(); }) added a fresh, unremovable listener for every pageload span without ever detaching it.

Fixes#21630

The auto-finish `readystatechange` listener registered for each pageload
span was never removed. Because it closes over the `idleSpan` (and the
rest of the route-span closure), every pageload leaked a listener and the
span it retained. The previous anonymous handler could not be removed, and
the suggested `{ once: true }` would not help in the common case where the
document is already loaded when the span starts, since the listener never
fires.
Pass `emitFinish` directly as the handler and remove it as soon as the
auto-finish signal is emitted, covering both the already-loaded and
load-later cases.
Fixes#21630
Co-Authored-By: Opus 4.8 <noreply@anthropic.com>
@andreiborza
andreiborza requested a review from a team as a code ownerJune 18, 2026 11:55
@andreiborza
andreiborza requested review from Lms24 and logaretm and removed request for a team, Lms24 and logaretmJune 18, 2026 11:55
@andreiborza
andreiborza marked this pull request as draft June 18, 2026 12:01
@github-actions

github-actionsBot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.45 kB--
@sentry/browser - with treeshaking flags25.88 kB--
@sentry/browser (incl. Tracing)45.89 kB+0.03%+12 B 🔺
@sentry/browser (incl. Tracing + Span Streaming)48.12 kB+0.03%+11 B 🔺
@sentry/browser (incl. Tracing, Profiling)50.67 kB+0.03%+14 B 🔺
@sentry/browser (incl. Tracing, Replay)85.08 kB+0.01%+4 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.69 kB+0.01%+2 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas)89.78 kB+0.01%+6 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback)102.45 kB+0.01%+7 B 🔺
@sentry/browser (incl. Feedback)44.62 kB--
@sentry/browser (incl. sendFeedback)32.25 kB--
@sentry/browser (incl. FeedbackAsync)37.38 kB--
@sentry/browser (incl. Metrics)28.52 kB--
@sentry/browser (incl. Logs)28.76 kB--
@sentry/browser (incl. Metrics & Logs)29.45 kB--
@sentry/react29.25 kB--
@sentry/react (incl. Tracing)48.18 kB+0.02%+5 B 🔺
@sentry/vue32.56 kB--
@sentry/vue (incl. Tracing)47.76 kB+0.03%+10 B 🔺
@sentry/svelte27.48 kB--
CDN Bundle29.86 kB--
CDN Bundle (incl. Tracing)48.29 kB+0.03%+11 B 🔺
CDN Bundle (incl. Logs, Metrics)31.4 kB--
CDN Bundle (incl. Tracing, Logs, Metrics)49.59 kB+0.03%+11 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics)70.71 kB--
CDN Bundle (incl. Tracing, Replay)85.62 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.88 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback)91.46 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.71 kB+0.01%+8 B 🔺
CDN Bundle - uncompressed88.8 kB--
CDN Bundle (incl. Tracing) - uncompressed146.08 kB+0.03%+38 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed93.5 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed150.06 kB+0.03%+38 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.33 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed264.95 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed268.91 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed278.65 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed282.6 kB+0.02%+38 B 🔺
@sentry/nextjs (client)50.58 kB+0.01%+2 B 🔺
@sentry/sveltekit (client)46.27 kB+0.02%+9 B 🔺
@sentry/core/server76.16 kB--
@sentry/core/browser63.31 kB--
@sentry/node-core61.87 kB-0.01%-1 B 🔽
@sentry/node127.69 kB--
@sentry/node - without tracing74.26 kB--
@sentry/aws-serverless85.35 kB--
@sentry/cloudflare (withSentry) - minified174.48 kB--
@sentry/cloudflare (withSentry)436.52 kB--

View base workflow run

optionalWindowDocument.addEventListener('readystatechange', () => {
emitFinish();
});
optionalWindowDocument.addEventListener('readystatechange', emitFinish);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't { once: true } suffice?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually never mind, you want to remove it once it is called down there as well.

@andreiborza
andreiborza marked this pull request as ready for review June 18, 2026 15:24
@andreiborza

Copy link
Copy Markdown
MemberAuthor

For the record: I'm not actually sure this fixes an actual leak in a real-world scenario, because it seems strange to me that we'd have more than one pageload span. That being said, cleaning up here seems like good form.

@andreiborza
andreiborza merged commit d64420f into developJun 18, 2026
368 of 372 checks passed
@andreiborza
andreiborza deleted the fix/browser-tracing-readystatechange-listener-leak branch June 18, 2026 15:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

memory leak in browserTracingIntegration.ts

3 participants

@andreiborza@logaretm@Lms24
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(browser): Clean up pageload readystatechange listener - #21632

Merged
andreiborza merged 2 commits into
developfrom
fix/browser-tracing-readystatechange-listener-leak
Jun 18, 2026
Merged

fix(browser): Clean up pageload readystatechange listener#21632
andreiborza merged 2 commits into
developfrom
fix/browser-tracing-readystatechange-listener-leak

Conversation

@andreiborza

Copy link
Copy Markdown
Member

The auto-finish readystatechange listener registered for each pageload span in browserTracingIntegration was never removed. Since the handler closes over the idleSpan (and the rest of the route-span closure), every pageload leaked a listener and kept the span it retained alive — the growing set of Sentry functions reported in #21630.

The previous handler was an anonymous arrow function, so it could not be removed via removeEventListener. The { once: true } option suggested in the issue does not fully address the leak either: in the common case where the document is already loaded when the span starts, the listener never fires and therefore is never auto-removed.

This change passes emitFinish directly as the handler and removes it as soon as the auto-finish signal is emitted. This covers both cases:

  • Document already loaded when the span starts → signal emitted synchronously, listener removed immediately.
  • Document still loading → listener fires on the next readystatechange, emits, and removes itself.

Since the framework integrations (Vue, Astro, Next.js, Remix, SvelteKit, Ember) all wrap the browser package's browserTracingIntegration, this single fix covers them too.

Root cause

optionalWindowDocument.addEventListener('readystatechange', () => { emitFinish(); }) added a fresh, unremovable listener for every pageload span without ever detaching it.

Fixes#21630

The auto-finish `readystatechange` listener registered for each pageload
span was never removed. Because it closes over the `idleSpan` (and the
rest of the route-span closure), every pageload leaked a listener and the
span it retained. The previous anonymous handler could not be removed, and
the suggested `{ once: true }` would not help in the common case where the
document is already loaded when the span starts, since the listener never
fires.
Pass `emitFinish` directly as the handler and remove it as soon as the
auto-finish signal is emitted, covering both the already-loaded and
load-later cases.
Fixes#21630
Co-Authored-By: Opus 4.8 <noreply@anthropic.com>
@andreiborza
andreiborza requested a review from a team as a code ownerJune 18, 2026 11:55
@andreiborza
andreiborza requested review from Lms24 and logaretm and removed request for a team, Lms24 and logaretmJune 18, 2026 11:55
@andreiborza
andreiborza marked this pull request as draft June 18, 2026 12:01
@github-actions

github-actionsBot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.45 kB--
@sentry/browser - with treeshaking flags25.88 kB--
@sentry/browser (incl. Tracing)45.89 kB+0.03%+12 B 🔺
@sentry/browser (incl. Tracing + Span Streaming)48.12 kB+0.03%+11 B 🔺
@sentry/browser (incl. Tracing, Profiling)50.67 kB+0.03%+14 B 🔺
@sentry/browser (incl. Tracing, Replay)85.08 kB+0.01%+4 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.69 kB+0.01%+2 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas)89.78 kB+0.01%+6 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback)102.45 kB+0.01%+7 B 🔺
@sentry/browser (incl. Feedback)44.62 kB--
@sentry/browser (incl. sendFeedback)32.25 kB--
@sentry/browser (incl. FeedbackAsync)37.38 kB--
@sentry/browser (incl. Metrics)28.52 kB--
@sentry/browser (incl. Logs)28.76 kB--
@sentry/browser (incl. Metrics & Logs)29.45 kB--
@sentry/react29.25 kB--
@sentry/react (incl. Tracing)48.18 kB+0.02%+5 B 🔺
@sentry/vue32.56 kB--
@sentry/vue (incl. Tracing)47.76 kB+0.03%+10 B 🔺
@sentry/svelte27.48 kB--
CDN Bundle29.86 kB--
CDN Bundle (incl. Tracing)48.29 kB+0.03%+11 B 🔺
CDN Bundle (incl. Logs, Metrics)31.4 kB--
CDN Bundle (incl. Tracing, Logs, Metrics)49.59 kB+0.03%+11 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics)70.71 kB--
CDN Bundle (incl. Tracing, Replay)85.62 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.88 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback)91.46 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.71 kB+0.01%+8 B 🔺
CDN Bundle - uncompressed88.8 kB--
CDN Bundle (incl. Tracing) - uncompressed146.08 kB+0.03%+38 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed93.5 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed150.06 kB+0.03%+38 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.33 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed264.95 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed268.91 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed278.65 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed282.6 kB+0.02%+38 B 🔺
@sentry/nextjs (client)50.58 kB+0.01%+2 B 🔺
@sentry/sveltekit (client)46.27 kB+0.02%+9 B 🔺
@sentry/core/server76.16 kB--
@sentry/core/browser63.31 kB--
@sentry/node-core61.87 kB-0.01%-1 B 🔽
@sentry/node127.69 kB--
@sentry/node - without tracing74.26 kB--
@sentry/aws-serverless85.35 kB--
@sentry/cloudflare (withSentry) - minified174.48 kB--
@sentry/cloudflare (withSentry)436.52 kB--

View base workflow run

optionalWindowDocument.addEventListener('readystatechange', () => {
emitFinish();
});
optionalWindowDocument.addEventListener('readystatechange', emitFinish);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't { once: true } suffice?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually never mind, you want to remove it once it is called down there as well.

@andreiborza
andreiborza marked this pull request as ready for review June 18, 2026 15:24
@andreiborza

Copy link
Copy Markdown
MemberAuthor

For the record: I'm not actually sure this fixes an actual leak in a real-world scenario, because it seems strange to me that we'd have more than one pageload span. That being said, cleaning up here seems like good form.

@andreiborza
andreiborza merged commit d64420f into developJun 18, 2026
368 of 372 checks passed
@andreiborza
andreiborza deleted the fix/browser-tracing-readystatechange-listener-leak branch June 18, 2026 15:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

memory leak in browserTracingIntegration.ts

3 participants

@andreiborza@logaretm@Lms24
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(browser): Clean up pageload readystatechange listener - #21632

Merged
andreiborza merged 2 commits into
developfrom
fix/browser-tracing-readystatechange-listener-leak
Jun 18, 2026
Merged

fix(browser): Clean up pageload readystatechange listener#21632
andreiborza merged 2 commits into
developfrom
fix/browser-tracing-readystatechange-listener-leak

Conversation

@andreiborza

Copy link
Copy Markdown
Member

The auto-finish readystatechange listener registered for each pageload span in browserTracingIntegration was never removed. Since the handler closes over the idleSpan (and the rest of the route-span closure), every pageload leaked a listener and kept the span it retained alive — the growing set of Sentry functions reported in #21630.

The previous handler was an anonymous arrow function, so it could not be removed via removeEventListener. The { once: true } option suggested in the issue does not fully address the leak either: in the common case where the document is already loaded when the span starts, the listener never fires and therefore is never auto-removed.

This change passes emitFinish directly as the handler and removes it as soon as the auto-finish signal is emitted. This covers both cases:

  • Document already loaded when the span starts → signal emitted synchronously, listener removed immediately.
  • Document still loading → listener fires on the next readystatechange, emits, and removes itself.

Since the framework integrations (Vue, Astro, Next.js, Remix, SvelteKit, Ember) all wrap the browser package's browserTracingIntegration, this single fix covers them too.

Root cause

optionalWindowDocument.addEventListener('readystatechange', () => { emitFinish(); }) added a fresh, unremovable listener for every pageload span without ever detaching it.

Fixes#21630

The auto-finish `readystatechange` listener registered for each pageload
span was never removed. Because it closes over the `idleSpan` (and the
rest of the route-span closure), every pageload leaked a listener and the
span it retained. The previous anonymous handler could not be removed, and
the suggested `{ once: true }` would not help in the common case where the
document is already loaded when the span starts, since the listener never
fires.
Pass `emitFinish` directly as the handler and remove it as soon as the
auto-finish signal is emitted, covering both the already-loaded and
load-later cases.
Fixes#21630
Co-Authored-By: Opus 4.8 <noreply@anthropic.com>
@andreiborza
andreiborza requested a review from a team as a code ownerJune 18, 2026 11:55
@andreiborza
andreiborza requested review from Lms24 and logaretm and removed request for a team, Lms24 and logaretmJune 18, 2026 11:55
@andreiborza
andreiborza marked this pull request as draft June 18, 2026 12:01
@github-actions

github-actionsBot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.45 kB--
@sentry/browser - with treeshaking flags25.88 kB--
@sentry/browser (incl. Tracing)45.89 kB+0.03%+12 B 🔺
@sentry/browser (incl. Tracing + Span Streaming)48.12 kB+0.03%+11 B 🔺
@sentry/browser (incl. Tracing, Profiling)50.67 kB+0.03%+14 B 🔺
@sentry/browser (incl. Tracing, Replay)85.08 kB+0.01%+4 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.69 kB+0.01%+2 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas)89.78 kB+0.01%+6 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback)102.45 kB+0.01%+7 B 🔺
@sentry/browser (incl. Feedback)44.62 kB--
@sentry/browser (incl. sendFeedback)32.25 kB--
@sentry/browser (incl. FeedbackAsync)37.38 kB--
@sentry/browser (incl. Metrics)28.52 kB--
@sentry/browser (incl. Logs)28.76 kB--
@sentry/browser (incl. Metrics & Logs)29.45 kB--
@sentry/react29.25 kB--
@sentry/react (incl. Tracing)48.18 kB+0.02%+5 B 🔺
@sentry/vue32.56 kB--
@sentry/vue (incl. Tracing)47.76 kB+0.03%+10 B 🔺
@sentry/svelte27.48 kB--
CDN Bundle29.86 kB--
CDN Bundle (incl. Tracing)48.29 kB+0.03%+11 B 🔺
CDN Bundle (incl. Logs, Metrics)31.4 kB--
CDN Bundle (incl. Tracing, Logs, Metrics)49.59 kB+0.03%+11 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics)70.71 kB--
CDN Bundle (incl. Tracing, Replay)85.62 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.88 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback)91.46 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.71 kB+0.01%+8 B 🔺
CDN Bundle - uncompressed88.8 kB--
CDN Bundle (incl. Tracing) - uncompressed146.08 kB+0.03%+38 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed93.5 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed150.06 kB+0.03%+38 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.33 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed264.95 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed268.91 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed278.65 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed282.6 kB+0.02%+38 B 🔺
@sentry/nextjs (client)50.58 kB+0.01%+2 B 🔺
@sentry/sveltekit (client)46.27 kB+0.02%+9 B 🔺
@sentry/core/server76.16 kB--
@sentry/core/browser63.31 kB--
@sentry/node-core61.87 kB-0.01%-1 B 🔽
@sentry/node127.69 kB--
@sentry/node - without tracing74.26 kB--
@sentry/aws-serverless85.35 kB--
@sentry/cloudflare (withSentry) - minified174.48 kB--
@sentry/cloudflare (withSentry)436.52 kB--

View base workflow run

optionalWindowDocument.addEventListener('readystatechange', () => {
emitFinish();
});
optionalWindowDocument.addEventListener('readystatechange', emitFinish);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't { once: true } suffice?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually never mind, you want to remove it once it is called down there as well.

@andreiborza
andreiborza marked this pull request as ready for review June 18, 2026 15:24
@andreiborza

Copy link
Copy Markdown
MemberAuthor

For the record: I'm not actually sure this fixes an actual leak in a real-world scenario, because it seems strange to me that we'd have more than one pageload span. That being said, cleaning up here seems like good form.

@andreiborza
andreiborza merged commit d64420f into developJun 18, 2026
368 of 372 checks passed
@andreiborza
andreiborza deleted the fix/browser-tracing-readystatechange-listener-leak branch June 18, 2026 15:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

memory leak in browserTracingIntegration.ts

3 participants

@andreiborza@logaretm@Lms24
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(browser): Clean up pageload readystatechange listener - #21632

Merged
andreiborza merged 2 commits into
developfrom
fix/browser-tracing-readystatechange-listener-leak
Jun 18, 2026
Merged

fix(browser): Clean up pageload readystatechange listener#21632
andreiborza merged 2 commits into
developfrom
fix/browser-tracing-readystatechange-listener-leak

Conversation

@andreiborza

Copy link
Copy Markdown
Member

The auto-finish readystatechange listener registered for each pageload span in browserTracingIntegration was never removed. Since the handler closes over the idleSpan (and the rest of the route-span closure), every pageload leaked a listener and kept the span it retained alive — the growing set of Sentry functions reported in #21630.

The previous handler was an anonymous arrow function, so it could not be removed via removeEventListener. The { once: true } option suggested in the issue does not fully address the leak either: in the common case where the document is already loaded when the span starts, the listener never fires and therefore is never auto-removed.

This change passes emitFinish directly as the handler and removes it as soon as the auto-finish signal is emitted. This covers both cases:

  • Document already loaded when the span starts → signal emitted synchronously, listener removed immediately.
  • Document still loading → listener fires on the next readystatechange, emits, and removes itself.

Since the framework integrations (Vue, Astro, Next.js, Remix, SvelteKit, Ember) all wrap the browser package's browserTracingIntegration, this single fix covers them too.

Root cause

optionalWindowDocument.addEventListener('readystatechange', () => { emitFinish(); }) added a fresh, unremovable listener for every pageload span without ever detaching it.

Fixes#21630

The auto-finish `readystatechange` listener registered for each pageload
span was never removed. Because it closes over the `idleSpan` (and the
rest of the route-span closure), every pageload leaked a listener and the
span it retained. The previous anonymous handler could not be removed, and
the suggested `{ once: true }` would not help in the common case where the
document is already loaded when the span starts, since the listener never
fires.
Pass `emitFinish` directly as the handler and remove it as soon as the
auto-finish signal is emitted, covering both the already-loaded and
load-later cases.
Fixes#21630
Co-Authored-By: Opus 4.8 <noreply@anthropic.com>
@andreiborza
andreiborza requested a review from a team as a code ownerJune 18, 2026 11:55
@andreiborza
andreiborza requested review from Lms24 and logaretm and removed request for a team, Lms24 and logaretmJune 18, 2026 11:55
@andreiborza
andreiborza marked this pull request as draft June 18, 2026 12:01
@github-actions

github-actionsBot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.45 kB--
@sentry/browser - with treeshaking flags25.88 kB--
@sentry/browser (incl. Tracing)45.89 kB+0.03%+12 B 🔺
@sentry/browser (incl. Tracing + Span Streaming)48.12 kB+0.03%+11 B 🔺
@sentry/browser (incl. Tracing, Profiling)50.67 kB+0.03%+14 B 🔺
@sentry/browser (incl. Tracing, Replay)85.08 kB+0.01%+4 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.69 kB+0.01%+2 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas)89.78 kB+0.01%+6 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback)102.45 kB+0.01%+7 B 🔺
@sentry/browser (incl. Feedback)44.62 kB--
@sentry/browser (incl. sendFeedback)32.25 kB--
@sentry/browser (incl. FeedbackAsync)37.38 kB--
@sentry/browser (incl. Metrics)28.52 kB--
@sentry/browser (incl. Logs)28.76 kB--
@sentry/browser (incl. Metrics & Logs)29.45 kB--
@sentry/react29.25 kB--
@sentry/react (incl. Tracing)48.18 kB+0.02%+5 B 🔺
@sentry/vue32.56 kB--
@sentry/vue (incl. Tracing)47.76 kB+0.03%+10 B 🔺
@sentry/svelte27.48 kB--
CDN Bundle29.86 kB--
CDN Bundle (incl. Tracing)48.29 kB+0.03%+11 B 🔺
CDN Bundle (incl. Logs, Metrics)31.4 kB--
CDN Bundle (incl. Tracing, Logs, Metrics)49.59 kB+0.03%+11 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics)70.71 kB--
CDN Bundle (incl. Tracing, Replay)85.62 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.88 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback)91.46 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.71 kB+0.01%+8 B 🔺
CDN Bundle - uncompressed88.8 kB--
CDN Bundle (incl. Tracing) - uncompressed146.08 kB+0.03%+38 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed93.5 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed150.06 kB+0.03%+38 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.33 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed264.95 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed268.91 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed278.65 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed282.6 kB+0.02%+38 B 🔺
@sentry/nextjs (client)50.58 kB+0.01%+2 B 🔺
@sentry/sveltekit (client)46.27 kB+0.02%+9 B 🔺
@sentry/core/server76.16 kB--
@sentry/core/browser63.31 kB--
@sentry/node-core61.87 kB-0.01%-1 B 🔽
@sentry/node127.69 kB--
@sentry/node - without tracing74.26 kB--
@sentry/aws-serverless85.35 kB--
@sentry/cloudflare (withSentry) - minified174.48 kB--
@sentry/cloudflare (withSentry)436.52 kB--

View base workflow run

optionalWindowDocument.addEventListener('readystatechange', () => {
emitFinish();
});
optionalWindowDocument.addEventListener('readystatechange', emitFinish);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't { once: true } suffice?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually never mind, you want to remove it once it is called down there as well.

@andreiborza
andreiborza marked this pull request as ready for review June 18, 2026 15:24
@andreiborza

Copy link
Copy Markdown
MemberAuthor

For the record: I'm not actually sure this fixes an actual leak in a real-world scenario, because it seems strange to me that we'd have more than one pageload span. That being said, cleaning up here seems like good form.

@andreiborza
andreiborza merged commit d64420f into developJun 18, 2026
368 of 372 checks passed
@andreiborza
andreiborza deleted the fix/browser-tracing-readystatechange-listener-leak branch June 18, 2026 15:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

memory leak in browserTracingIntegration.ts

3 participants

@andreiborza@logaretm@Lms24
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(browser): Clean up pageload readystatechange listener - #21632

Merged
andreiborza merged 2 commits into
developfrom
fix/browser-tracing-readystatechange-listener-leak
Jun 18, 2026
Merged

fix(browser): Clean up pageload readystatechange listener#21632
andreiborza merged 2 commits into
developfrom
fix/browser-tracing-readystatechange-listener-leak

Conversation

@andreiborza

Copy link
Copy Markdown
Member

The auto-finish readystatechange listener registered for each pageload span in browserTracingIntegration was never removed. Since the handler closes over the idleSpan (and the rest of the route-span closure), every pageload leaked a listener and kept the span it retained alive — the growing set of Sentry functions reported in #21630.

The previous handler was an anonymous arrow function, so it could not be removed via removeEventListener. The { once: true } option suggested in the issue does not fully address the leak either: in the common case where the document is already loaded when the span starts, the listener never fires and therefore is never auto-removed.

This change passes emitFinish directly as the handler and removes it as soon as the auto-finish signal is emitted. This covers both cases:

  • Document already loaded when the span starts → signal emitted synchronously, listener removed immediately.
  • Document still loading → listener fires on the next readystatechange, emits, and removes itself.

Since the framework integrations (Vue, Astro, Next.js, Remix, SvelteKit, Ember) all wrap the browser package's browserTracingIntegration, this single fix covers them too.

Root cause

optionalWindowDocument.addEventListener('readystatechange', () => { emitFinish(); }) added a fresh, unremovable listener for every pageload span without ever detaching it.

Fixes#21630

The auto-finish `readystatechange` listener registered for each pageload
span was never removed. Because it closes over the `idleSpan` (and the
rest of the route-span closure), every pageload leaked a listener and the
span it retained. The previous anonymous handler could not be removed, and
the suggested `{ once: true }` would not help in the common case where the
document is already loaded when the span starts, since the listener never
fires.
Pass `emitFinish` directly as the handler and remove it as soon as the
auto-finish signal is emitted, covering both the already-loaded and
load-later cases.
Fixes#21630
Co-Authored-By: Opus 4.8 <noreply@anthropic.com>
@andreiborza
andreiborza requested a review from a team as a code ownerJune 18, 2026 11:55
@andreiborza
andreiborza requested review from Lms24 and logaretm and removed request for a team, Lms24 and logaretmJune 18, 2026 11:55
@andreiborza
andreiborza marked this pull request as draft June 18, 2026 12:01
@github-actions

github-actionsBot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.45 kB--
@sentry/browser - with treeshaking flags25.88 kB--
@sentry/browser (incl. Tracing)45.89 kB+0.03%+12 B 🔺
@sentry/browser (incl. Tracing + Span Streaming)48.12 kB+0.03%+11 B 🔺
@sentry/browser (incl. Tracing, Profiling)50.67 kB+0.03%+14 B 🔺
@sentry/browser (incl. Tracing, Replay)85.08 kB+0.01%+4 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.69 kB+0.01%+2 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas)89.78 kB+0.01%+6 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback)102.45 kB+0.01%+7 B 🔺
@sentry/browser (incl. Feedback)44.62 kB--
@sentry/browser (incl. sendFeedback)32.25 kB--
@sentry/browser (incl. FeedbackAsync)37.38 kB--
@sentry/browser (incl. Metrics)28.52 kB--
@sentry/browser (incl. Logs)28.76 kB--
@sentry/browser (incl. Metrics & Logs)29.45 kB--
@sentry/react29.25 kB--
@sentry/react (incl. Tracing)48.18 kB+0.02%+5 B 🔺
@sentry/vue32.56 kB--
@sentry/vue (incl. Tracing)47.76 kB+0.03%+10 B 🔺
@sentry/svelte27.48 kB--
CDN Bundle29.86 kB--
CDN Bundle (incl. Tracing)48.29 kB+0.03%+11 B 🔺
CDN Bundle (incl. Logs, Metrics)31.4 kB--
CDN Bundle (incl. Tracing, Logs, Metrics)49.59 kB+0.03%+11 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics)70.71 kB--
CDN Bundle (incl. Tracing, Replay)85.62 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.88 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback)91.46 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.71 kB+0.01%+8 B 🔺
CDN Bundle - uncompressed88.8 kB--
CDN Bundle (incl. Tracing) - uncompressed146.08 kB+0.03%+38 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed93.5 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed150.06 kB+0.03%+38 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.33 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed264.95 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed268.91 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed278.65 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed282.6 kB+0.02%+38 B 🔺
@sentry/nextjs (client)50.58 kB+0.01%+2 B 🔺
@sentry/sveltekit (client)46.27 kB+0.02%+9 B 🔺
@sentry/core/server76.16 kB--
@sentry/core/browser63.31 kB--
@sentry/node-core61.87 kB-0.01%-1 B 🔽
@sentry/node127.69 kB--
@sentry/node - without tracing74.26 kB--
@sentry/aws-serverless85.35 kB--
@sentry/cloudflare (withSentry) - minified174.48 kB--
@sentry/cloudflare (withSentry)436.52 kB--

View base workflow run

optionalWindowDocument.addEventListener('readystatechange', () => {
emitFinish();
});
optionalWindowDocument.addEventListener('readystatechange', emitFinish);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't { once: true } suffice?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually never mind, you want to remove it once it is called down there as well.

@andreiborza
andreiborza marked this pull request as ready for review June 18, 2026 15:24
@andreiborza

Copy link
Copy Markdown
MemberAuthor

For the record: I'm not actually sure this fixes an actual leak in a real-world scenario, because it seems strange to me that we'd have more than one pageload span. That being said, cleaning up here seems like good form.

@andreiborza
andreiborza merged commit d64420f into developJun 18, 2026
368 of 372 checks passed
@andreiborza
andreiborza deleted the fix/browser-tracing-readystatechange-listener-leak branch June 18, 2026 15:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

memory leak in browserTracingIntegration.ts

3 participants

@andreiborza@logaretm@Lms24
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(browser): Clean up pageload readystatechange listener - #21632

Merged
andreiborza merged 2 commits into
developfrom
fix/browser-tracing-readystatechange-listener-leak
Jun 18, 2026
Merged

fix(browser): Clean up pageload readystatechange listener#21632
andreiborza merged 2 commits into
developfrom
fix/browser-tracing-readystatechange-listener-leak

Conversation

@andreiborza

Copy link
Copy Markdown
Member

The auto-finish readystatechange listener registered for each pageload span in browserTracingIntegration was never removed. Since the handler closes over the idleSpan (and the rest of the route-span closure), every pageload leaked a listener and kept the span it retained alive — the growing set of Sentry functions reported in #21630.

The previous handler was an anonymous arrow function, so it could not be removed via removeEventListener. The { once: true } option suggested in the issue does not fully address the leak either: in the common case where the document is already loaded when the span starts, the listener never fires and therefore is never auto-removed.

This change passes emitFinish directly as the handler and removes it as soon as the auto-finish signal is emitted. This covers both cases:

  • Document already loaded when the span starts → signal emitted synchronously, listener removed immediately.
  • Document still loading → listener fires on the next readystatechange, emits, and removes itself.

Since the framework integrations (Vue, Astro, Next.js, Remix, SvelteKit, Ember) all wrap the browser package's browserTracingIntegration, this single fix covers them too.

Root cause

optionalWindowDocument.addEventListener('readystatechange', () => { emitFinish(); }) added a fresh, unremovable listener for every pageload span without ever detaching it.

Fixes#21630

The auto-finish `readystatechange` listener registered for each pageload
span was never removed. Because it closes over the `idleSpan` (and the
rest of the route-span closure), every pageload leaked a listener and the
span it retained. The previous anonymous handler could not be removed, and
the suggested `{ once: true }` would not help in the common case where the
document is already loaded when the span starts, since the listener never
fires.
Pass `emitFinish` directly as the handler and remove it as soon as the
auto-finish signal is emitted, covering both the already-loaded and
load-later cases.
Fixes#21630
Co-Authored-By: Opus 4.8 <noreply@anthropic.com>
@andreiborza
andreiborza requested a review from a team as a code ownerJune 18, 2026 11:55
@andreiborza
andreiborza requested review from Lms24 and logaretm and removed request for a team, Lms24 and logaretmJune 18, 2026 11:55
@andreiborza
andreiborza marked this pull request as draft June 18, 2026 12:01
@github-actions

github-actionsBot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.45 kB--
@sentry/browser - with treeshaking flags25.88 kB--
@sentry/browser (incl. Tracing)45.89 kB+0.03%+12 B 🔺
@sentry/browser (incl. Tracing + Span Streaming)48.12 kB+0.03%+11 B 🔺
@sentry/browser (incl. Tracing, Profiling)50.67 kB+0.03%+14 B 🔺
@sentry/browser (incl. Tracing, Replay)85.08 kB+0.01%+4 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.69 kB+0.01%+2 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas)89.78 kB+0.01%+6 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback)102.45 kB+0.01%+7 B 🔺
@sentry/browser (incl. Feedback)44.62 kB--
@sentry/browser (incl. sendFeedback)32.25 kB--
@sentry/browser (incl. FeedbackAsync)37.38 kB--
@sentry/browser (incl. Metrics)28.52 kB--
@sentry/browser (incl. Logs)28.76 kB--
@sentry/browser (incl. Metrics & Logs)29.45 kB--
@sentry/react29.25 kB--
@sentry/react (incl. Tracing)48.18 kB+0.02%+5 B 🔺
@sentry/vue32.56 kB--
@sentry/vue (incl. Tracing)47.76 kB+0.03%+10 B 🔺
@sentry/svelte27.48 kB--
CDN Bundle29.86 kB--
CDN Bundle (incl. Tracing)48.29 kB+0.03%+11 B 🔺
CDN Bundle (incl. Logs, Metrics)31.4 kB--
CDN Bundle (incl. Tracing, Logs, Metrics)49.59 kB+0.03%+11 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics)70.71 kB--
CDN Bundle (incl. Tracing, Replay)85.62 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.88 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback)91.46 kB+0.01%+5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.71 kB+0.01%+8 B 🔺
CDN Bundle - uncompressed88.8 kB--
CDN Bundle (incl. Tracing) - uncompressed146.08 kB+0.03%+38 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed93.5 kB--
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed150.06 kB+0.03%+38 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed218.33 kB--
CDN Bundle (incl. Tracing, Replay) - uncompressed264.95 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed268.91 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed278.65 kB+0.02%+38 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed282.6 kB+0.02%+38 B 🔺
@sentry/nextjs (client)50.58 kB+0.01%+2 B 🔺
@sentry/sveltekit (client)46.27 kB+0.02%+9 B 🔺
@sentry/core/server76.16 kB--
@sentry/core/browser63.31 kB--
@sentry/node-core61.87 kB-0.01%-1 B 🔽
@sentry/node127.69 kB--
@sentry/node - without tracing74.26 kB--
@sentry/aws-serverless85.35 kB--
@sentry/cloudflare (withSentry) - minified174.48 kB--
@sentry/cloudflare (withSentry)436.52 kB--

View base workflow run

optionalWindowDocument.addEventListener('readystatechange', () => {
emitFinish();
});
optionalWindowDocument.addEventListener('readystatechange', emitFinish);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't { once: true } suffice?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually never mind, you want to remove it once it is called down there as well.

@andreiborza
andreiborza marked this pull request as ready for review June 18, 2026 15:24
@andreiborza

Copy link
Copy Markdown
MemberAuthor

For the record: I'm not actually sure this fixes an actual leak in a real-world scenario, because it seems strange to me that we'd have more than one pageload span. That being said, cleaning up here seems like good form.

@andreiborza
andreiborza merged commit d64420f into developJun 18, 2026
368 of 372 checks passed
@andreiborza
andreiborza deleted the fix/browser-tracing-readystatechange-listener-leak branch June 18, 2026 15:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

memory leak in browserTracingIntegration.ts

3 participants

@andreiborza@logaretm@Lms24