fix(browser): End the active idle span on pagehide - #23779
Conversation
size-limit report 📦
|
Lms24
left a comment
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
super-l: we can use the @sentry/conventions constant here instead
|
👋 @msonnb — Please review this PR when you get a chance! |
1 similar comment
|
👋 @msonnb — Please review this PR when you get a chance! |
53b3d45 to
b97f2ae
Compare
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.
b97f2ae to
a5823c1
Compare
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 🤔
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 IMO, the |
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.
4cf2788 to
310e74e
Compare
Lms24
left a comment
There was a problem hiding this comment.
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.
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.