Skip to content

fix(browser): End the active idle span on pagehide - #23779

Open
logaretm wants to merge 3 commits into
developfrom
awad/pagehide-idle-span
Open

fix(browser): End the active idle span on pagehide#23779
logaretm wants to merge 3 commits into
developfrom
awad/pagehide-idle-span

Conversation

@logaretm

@logaretm logaretm commented Aug 31, 2026

Copy link
Copy Markdown
Member

Ends the active idle span on pagehide, so a navigation span that is still open when the user leaves the page doesn't lose its root.

@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 28.8 kB - -
@sentry/browser - with treeshaking flags 27.11 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27 kB - -
@sentry/browser (incl. Tracing) 49.25 kB +0.07% +30 B 🔺
@sentry/browser (incl. Tracing + Span Streaming) 49.26 kB +0.09% +44 B 🔺
@sentry/browser (incl. Tracing, Profiling) 52.19 kB +0.12% +62 B 🔺
@sentry/browser (incl. Tracing, Replay) 88.78 kB +0.03% +22 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 77.97 kB +0.02% +13 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 93.47 kB +0.04% +32 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 106.44 kB +0.06% +63 B 🔺
@sentry/browser (incl. Feedback) 46.3 kB - -
@sentry/browser (incl. sendFeedback) 33.86 kB - -
@sentry/browser (incl. FeedbackAsync) 38.97 kB - -
@sentry/browser (incl. Metrics) 29.82 kB - -
@sentry/browser (incl. Logs) 30.09 kB - -
@sentry/browser (incl. Metrics & Logs) 30.75 kB - -
@sentry/react 30.55 kB - -
@sentry/react (incl. Tracing) 51.6 kB +0.09% +45 B 🔺
@sentry/vue 36.05 kB - -
@sentry/vue (incl. Tracing) 51.52 kB +0.1% +47 B 🔺
@sentry/svelte 28.83 kB - -
CDN Bundle 30.55 kB - -
CDN Bundle (incl. Tracing) 49.8 kB +0.14% +65 B 🔺
CDN Bundle (incl. Logs, Metrics) 32.82 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 51.76 kB +0.11% +55 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 73.48 kB - -
CDN Bundle (incl. Tracing, Replay) 87.33 kB +0.05% +39 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 89.25 kB +0.08% +68 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 93.25 kB +0.04% +36 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 95.27 kB +0.07% +65 B 🔺
CDN Bundle - uncompressed 90.46 kB - -
CDN Bundle (incl. Tracing) - uncompressed 148.36 kB +0.13% +190 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 97.03 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 154.33 kB +0.13% +190 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 226.29 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 267.95 kB +0.08% +190 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 273.9 kB +0.07% +190 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 281.65 kB +0.07% +190 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 287.59 kB +0.07% +190 B 🔺
@sentry/nextjs (client) 54.06 kB +0.11% +55 B 🔺
@sentry/sveltekit (client) 49.7 kB +0.1% +45 B 🔺
@sentry/core/server 36.99 kB - -
@sentry/core/browser 13.55 kB - -
@sentry/node 127.86 kB +0.02% +21 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.61 kB - -
@sentry/node - without tracing 88.73 kB +0.03% +21 B 🔺
@sentry/node - without channel injection 107.11 kB +0.03% +22 B 🔺
@sentry/aws-serverless 97.12 kB +0.03% +26 B 🔺
@sentry/cloudflare (withSentry) - minified 201.98 kB - -
@sentry/cloudflare (withSentry) 502.68 kB - -

View base workflow run

@logaretm
logaretm marked this pull request as ready for review August 31, 2026 16:16
@logaretm
logaretm requested a review from a team as a code owner August 31, 2026 16:16
@logaretm
logaretm requested review from Lms24 and msonnb and removed request for a team August 31, 2026 16:16

@Lms24 Lms24 left a comment

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.

Looks reasonable to me! The interesting part is that the visibilityChange listener that also ends the span is gated by the markBackgroundSpan.

Which tbh, I'm a bit torn if we should keep supporting long-term because to me it seems like we'd always want to cancel and end a span when going to background. It's also a bit weird that the visibilityChange logic sets an error status. But this decision is out of scope of this PR I'd say.

So I'd suggest, for now we also gate on markBackgroundSpan, wdyt?

WINDOW.addEventListener?.('pagehide', () => {
const activeSpan = getActiveIdleSpan(client);
if (activeSpan && !spanToJSON(activeSpan).end_timestamp) {
activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, 'documentHidden');

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.

super-l: we can use the @sentry/conventions constant here instead

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

👋 @msonnb — Please review this PR when you get a chance!

1 similar comment
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

👋 @msonnb — Please review this PR when you get a chance!

@logaretm
logaretm force-pushed the awad/pagehide-idle-span branch from 53b3d45 to b97f2ae Compare September 8, 2026 14:56
A navigation span whose idle timeout has not elapsed when the document
goes away was reaching Sentry as a rootless trace: its children had been
streamed while the page was alive, but the root span never arrived.

`registerBackgroundTabDetection` is meant to be the safety net, but it
listens for `visibilitychange`. Measured on a same-tab cross-document
navigation in Chrome 152, the order is:

    pagehide (persisted=true)  ->  visibilitychange (hidden)

`pagehide` comes first and freezes the document into the bfcache, so a
root span ended on `visibilitychange` is ended on a page that can no
longer send, and it is stranded on its own. That is also why the
cancellation never appears in the debug log: it is logged from a frozen
document.

`pagehide` is the last point at which a document can still send, so the
idle span is ended there instead, which also puts the root in the same
final batch as the remaining children. The trace then arrives whole or
not at all, rather than headless.

Verified against a real project: rootless traces appear without this and
consistently do not with it. Trace delivery volume is unchanged (7-8 of
10 traces over three runs each way), which is expected - this changes
whether the root travels with its children, not how much gets through.

The traces that go missing entirely are a separate problem: four envelope
requests per run are killed with `net::ERR_ABORTED` during unload, which
points at the `keepalive` budget in `fetch.ts` rather than at span
lifecycle.
@logaretm
logaretm force-pushed the awad/pagehide-idle-span branch from b97f2ae to a5823c1 Compare September 8, 2026 14:57
@logaretm

logaretm commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

I'm a bit torn if we should keep supporting long-term because to me it seems like we'd always want to cancel and end a span when going to background [...]

I'm torn as well because partly if it reports accurate span timings then I would like to keep it... we can tackle this in detail later. I need to dig more into this area tho 🤔

So I'd suggest, for now we also gate on markBackgroundSpan, wdyt?

Gating would drop the root entirely in the case of leaving the page while the span is still open. It's not the same as switching the tab away since the page would be still alive then so spans have a chance to end naturally and flushed, with pagehide they won't.

IMO, the pagehide is a clear signal that the page will not have a chance to do anything new, so it's probably safe to end there always. WDYT?

The client's `visibilitychange` flush defers via `queueMicrotask` so that
background tab detection ends the segment span first. For a browser-fired
event the microtask checkpoint runs after each listener callback rather than
after all of them, so the flush drains an empty buffer and the span is
buffered right after with nothing left to send it.

Ending and flushing together in the `pagehide` handler keeps this
self-contained instead of depending on that ordering.
@logaretm
logaretm force-pushed the awad/pagehide-idle-span branch from 4cf2788 to 310e74e Compare September 8, 2026 15:48
@logaretm
logaretm requested a review from Lms24 September 8, 2026 15:53

@Lms24 Lms24 left a comment

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.

Thanks for clarifying. Admittedly, I misunderstood pagehide and thought it would also fire when switching tabs. Which is why I conflated this with the background tab logic.

Ending the span on the actual semantics of pagehide makes total sense IMHO.

Sign up for free to 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.

2 participants