Skip to content

Commit b061a23

Browse files
committed
fix(browser): Always start navigation as root span
1 parent 566929b commit b061a23

File tree

5 files changed

+77
-0
lines changed

5 files changed

+77
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
tracesSampleRate: 1,
8+
integrations: [
9+
Sentry.browserTracingIntegration({
10+
_experiments: { enableInteractions: true },
11+
}),
12+
],
13+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Clicking the navigate button will push a new history state, triggering navigation
2+
document.querySelector('[data-test-id=navigate-button]').addEventListener('click', () => {
3+
const loc = window.location;
4+
const url = loc.href.includes('#nav') ? loc.pathname : `${loc.pathname}#nav`;
5+
6+
history.pushState({}, '', url);
7+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<button data-test-id="navigate-button">Navigate</button>
8+
</body>
9+
</html>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event as SentryEvent } from '@sentry/core';
3+
import { sentryTest } from '../../../../utils/fixtures';
4+
import {
5+
envelopeRequestParser,
6+
getFirstSentryEnvelopeRequest,
7+
shouldSkipTracingTest,
8+
waitForTransactionRequest,
9+
} from '../../../../utils/helpers';
10+
11+
sentryTest(
12+
'click-triggered navigation should produce a root navigation transaction',
13+
async ({ getLocalTestUrl, page }) => {
14+
if (shouldSkipTracingTest()) {
15+
sentryTest.skip();
16+
}
17+
18+
const url = await getLocalTestUrl({ testDir: __dirname });
19+
20+
await page.goto(url);
21+
await getFirstSentryEnvelopeRequest<SentryEvent>(page);
22+
23+
const interactionRequestPromise = waitForTransactionRequest(
24+
page,
25+
evt => evt.contexts?.trace?.op === 'ui.action.click',
26+
);
27+
const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation');
28+
29+
await page.locator('[data-test-id=navigate-button]').click();
30+
31+
const interactionEvent = envelopeRequestParser(await interactionRequestPromise);
32+
const navigationEvent = envelopeRequestParser(await navigationRequestPromise);
33+
34+
// Navigation is root span, not a child span on the interaction
35+
expect(interactionEvent.contexts?.trace?.op).toBe('ui.action.click');
36+
expect(navigationEvent.contexts?.trace?.op).toBe('navigation');
37+
38+
expect(interactionEvent.contexts?.trace?.trace_id).not.toEqual(navigationEvent.contexts?.trace?.trace_id);
39+
40+
// does not contain a child navigation span
41+
const interactionSpans = interactionEvent.spans || [];
42+
const hasNavigationChild = interactionSpans.some(span => span.op === 'navigation' || span.op === 'http.server');
43+
expect(hasNavigationChild).toBeFalsy();
44+
},
45+
);

packages/browser/src/tracing/browserTracingIntegration.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,9 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
536536
_createRouteSpan(client, {
537537
op: 'navigation',
538538
...startSpanOptions,
539+
// Navigation starts a new trace and is NOT parented under any active interaction (e.g. ui.action.click)
540+
parentSpan: null,
541+
forceTransaction: true,
539542
});
540543
});
541544

0 commit comments

Comments
 (0)