Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/browser/src/tracing/browserTracingIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,18 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption
_pageloadSpan.end();
}
});

// `pagehide` is the last moment a document can still send. `registerBackgroundTabDetection`
// waits for `visibilitychange`, which on a same-tab navigation fires *after* `pagehide` has
// already frozen the page into the bfcache, so a root span ended there can never leave. Its
// children have been streamed all along, so missing this point produces a rootless trace.
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

activeSpan.end();
}
});
},

afterAllSetup(client) {
Expand Down
40 changes: 40 additions & 0 deletions packages/browser/test/tracing/browserTracingIntegration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading