diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 49e6abf319c5..03c400598688 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -566,6 +566,18 @@ export const browserTracingIntegration = ((options: Partial { + const activeSpan = getActiveIdleSpan(client); + if (activeSpan && !spanToJSON(activeSpan).end_timestamp) { + activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, 'documentHidden'); + activeSpan.end(); + } + }); }, afterAllSetup(client) { diff --git a/packages/browser/test/tracing/browserTracingIntegration.test.ts b/packages/browser/test/tracing/browserTracingIntegration.test.ts index 5ef5172f16d4..a41ceb7bcc8c 100644 --- a/packages/browser/test/tracing/browserTracingIntegration.test.ts +++ b/packages/browser/test/tracing/browserTracingIntegration.test.ts @@ -770,6 +770,46 @@ describe('browserTracingIntegration', () => { expect(spanToJSON(pageloadSpan!).attributes[SENTRY_SEGMENT_NAME_SOURCE]).toBe('custom'); }); + describe('pagehide', () => { + it('ends the active idle span so its root is not stranded on a frozen page', () => { + // `registerBackgroundTabDetection` waits for `visibilitychange`, which on a same-tab + // navigation fires after `pagehide` has already frozen the document into the bfcache. A root + // ended there can never be sent, while its children have been streaming all along. + const client = new BrowserClient( + getDefaultBrowserClientOptions({ + tracesSampleRate: 1, + integrations: [browserTracingIntegration()], + }), + ); + setCurrentClient(client); + client.init(); + + const span = getActiveSpan()!; + expect(span).toBeDefined(); + expect(spanToJSON(span).end_timestamp).toBeUndefined(); + + WINDOW.dispatchEvent(new Event('pagehide')); + + const json = spanToJSON(span); + expect(json.end_timestamp).toBeDefined(); + expect(json.attributes?.['sentry.idle_span_finish_reason']).toBe('documentHidden'); + }); + + it('does nothing when there is no active idle span', () => { + const client = new BrowserClient( + getDefaultBrowserClientOptions({ + tracesSampleRate: 1, + integrations: [browserTracingIntegration({ instrumentPageLoad: false })], + }), + ); + setCurrentClient(client); + client.init(); + + expect(() => WINDOW.dispatchEvent(new Event('pagehide'))).not.toThrow(); + expect(getActiveSpan()).toBeUndefined(); + }); + }); + describe('startBrowserTracingNavigationSpan', () => { it('works without integration setup', () => { const client = new BrowserClient(