Skip to content
Merged
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
36 changes: 28 additions & 8 deletions packages/browser/src/tracing/browserTracingIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ import {
parseStringToURLObject,
propagationContextFromHeaders,
registerSpanErrorInstrumentation,
SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
spanIsSampled,
spanToJSON,
timestampInSeconds,
Expand All @@ -43,7 +41,14 @@ import { WEB_VITALS_INTEGRATION_NAME, webVitalsIntegration } from '../integratio
import { registerBackgroundTabDetection } from './backgroundtab';
import { linkTraces } from './linkedTraces';
import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request';
import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, URL_FULL, URL_PATH } from '@sentry/conventions/attributes';
import {
SENTRY_SEGMENT_NAME_SOURCE,
SENTRY_OP,
URL_FULL,
URL_PATH,
SENTRY_ORIGIN,
SENTRY_IDLE_SPAN_FINISH_REASON,
} from '@sentry/conventions/attributes';
import { NAVIGATION, NAVIGATION_REDIRECT, PAGELOAD } from '@sentry/conventions/op';

export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing';
Expand Down Expand Up @@ -427,7 +432,7 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption
`[Tracing] Finishing current active span with op: ${spanToJSON(activeSpan).attributes[SENTRY_OP]}`,
);
// If there's an open active span, we need to finish it before creating an new one.
activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, 'cancelled');
activeSpan.setAttribute(SENTRY_IDLE_SPAN_FINISH_REASON, 'cancelled');
activeSpan.end();
}
}
Expand Down Expand Up @@ -553,10 +558,26 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption

client.on('endPageloadSpan', () => {
if (enableReportPageLoaded && _pageloadSpan) {
_pageloadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON, 'reportPageLoaded');
_pageloadSpan.setAttribute(SENTRY_IDLE_SPAN_FINISH_REASON, 'reportPageLoaded');
_pageloadSpan.end();
}
});

// `pagehide` is the last moment a document leaving the page can still send. We cannot rely on
// `registerBackgroundTabDetection`, which ends the span on `visibilitychange`: the client's own
// `visibilitychange` flush is registered first and its deferring microtask runs after that
// listener but before background tab detection's, so the buffer is still empty when it drains.
// The segment span is buffered right after with nothing left to flush it, and the children have
// been streamed all along, which leaves a rootless trace. Ending and flushing together here
// keeps this self-contained instead of depending on that ordering.
WINDOW.addEventListener?.('pagehide', () => {
const activeSpan = getActiveIdleSpan(client);
if (activeSpan && !spanToJSON(activeSpan).end_timestamp) {
activeSpan.setAttribute(SENTRY_IDLE_SPAN_FINISH_REASON, 'documentHidden');
activeSpan.end();
}
void client.flush();
});
},

afterAllSetup(client) {
Expand Down Expand Up @@ -594,7 +615,7 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption
name: hasSpanStreamingEnabled(client) ? PAGELOAD_SPAN_NAME_FALLBACK : WINDOW.location.pathname,
attributes: {
[SENTRY_SEGMENT_NAME_SOURCE]: 'url',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser',
[SENTRY_ORIGIN]: 'auto.pageload.browser',
},
});
}
Expand Down Expand Up @@ -631,7 +652,7 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption
: parsed?.pathname || WINDOW.location.pathname,
attributes: {
[SENTRY_SEGMENT_NAME_SOURCE]: 'url',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser',
[SENTRY_ORIGIN]: 'auto.navigation.browser',
},
},
{ url: to, isRedirect: navigationIsRedirect },
Expand Down Expand Up @@ -748,7 +769,6 @@ export function getServerTiming(name: string): string | undefined {
// The cast is required for the declaration build (`build:types`), which resolves
// `getEntriesByType('navigation')` to `PerformanceEntry[]` (no `serverTiming`). It only reads as
// "unnecessary" to the type-aware linter, which runs with web-vitals' global augmentation applied.
// oxlint-disable-next-line typescript/no-unnecessary-type-assertion
const navigation = WINDOW.performance?.getEntriesByType?.('navigation')[0] as PerformanceNavigationTiming | undefined;
const entry = navigation?.serverTiming?.find(entry => entry.name === name);
return entry?.description;
Expand Down
61 changes: 61 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,67 @@ 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', () => {
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('flushes after ending the span, so the segment span is in the buffer when it drains', () => {
const client = new BrowserClient(
getDefaultBrowserClientOptions({
tracesSampleRate: 1,
integrations: [browserTracingIntegration()],
}),
);
setCurrentClient(client);
client.init();

const span = getActiveSpan()!;

let endTimestampWhenFlushed: number | undefined;
const flushSpy = vi.spyOn(client, 'flush').mockImplementation(() => {
endTimestampWhenFlushed = spanToJSON(span).end_timestamp;
return Promise.resolve(true);
});

WINDOW.dispatchEvent(new Event('pagehide'));

expect(flushSpy).toHaveBeenCalled();
expect(endTimestampWhenFlushed).toBeDefined();
});

it('ends no span 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