Skip to content

Commit 735c1d8

Browse files
authored
fix(browser): Improve navigation vs. redirect detection (#17275)
- Increase the threshold of inactivity between potential navigations from 300ms to 1.5s - Change how we treat interactions: - Previously, we'd take interactions happening _before_ the last root span was started into account - Now, we reset the interaction timestamp when we start a new navigation _root_ span. So from that moment on, we'll consider every new navigation span a redirect iff: - no interaction happened within 1.5s (from root span start) - the new span was started within 1.5s - there is still an active root span
1 parent b4aad49 commit 735c1d8

File tree

30 files changed

+310
-84
lines changed

30 files changed

+310
-84
lines changed

dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-aborting-pageload/init.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ window.Sentry = Sentry;
44

55
Sentry.init({
66
dsn: 'https://[email protected]/1337',
7-
integrations: [Sentry.browserTracingIntegration({ idleTimeout: 2000 })],
7+
integrations: [Sentry.browserTracingIntegration({ idleTimeout: 2000, detectRedirects: false })],
88
tracesSampleRate: 1,
99
});
1010

dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/keypress-early/init.js

Lines changed: 0 additions & 23 deletions
This file was deleted.

dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-redirect/keypress-early/test.ts

Lines changed: 0 additions & 42 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
integrations: [Sentry.browserTracingIntegration()],
8+
tracesSampleRate: 1,
9+
});
10+
11+
window.history.pushState({}, '', '/sub-page-redirect-1');
12+
13+
setTimeout(() => {
14+
window.history.pushState({}, '', '/sub-page-redirect-2');
15+
}, 400);
16+
17+
setTimeout(() => {
18+
window.history.pushState({}, '', '/sub-page-redirect-3');
19+
}, 800);
20+
21+
document.getElementById('btn1').addEventListener('click', () => {
22+
window.history.pushState({}, '', '/next-page');
23+
});
24+
25+
setTimeout(() => {
26+
document.getElementById('btn1').click();
27+
// 1s is still within the 1.5s time window, but the click should trigger a new navigation root span
28+
}, 1000);
29+
30+
setTimeout(() => {
31+
window.history.pushState({}, '', '/next-page-redirect-1');
32+
}, 1100);
33+
34+
setTimeout(() => {
35+
window.history.pushState({}, '', '/next-page-redirect-2');
36+
}, 2000);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { expect } from '@playwright/test';
2+
import {
3+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
4+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
5+
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
6+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
7+
} from '@sentry/core';
8+
import { sentryTest } from '../../../../../utils/fixtures';
9+
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers';
10+
11+
sentryTest(
12+
'creates a pageload and navigation root spans each with multiple navigation.redirect childspans',
13+
async ({ getLocalTestUrl, page }) => {
14+
if (shouldSkipTracingTest()) {
15+
sentryTest.skip();
16+
}
17+
18+
const url = await getLocalTestUrl({ testDir: __dirname });
19+
20+
const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload');
21+
const navigationRequestPromise = waitForTransactionRequest(
22+
page,
23+
event => event.contexts?.trace?.op === 'navigation' && event.transaction === '/next-page',
24+
);
25+
26+
await page.goto(url);
27+
28+
const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise);
29+
const navigationRequest = envelopeRequestParser(await navigationRequestPromise);
30+
31+
expect(pageloadRequest.contexts?.trace?.op).toBe('pageload');
32+
33+
expect(pageloadRequest.contexts?.trace?.data).toMatchObject({
34+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser',
35+
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1,
36+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
37+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
38+
['sentry.idle_span_finish_reason']: 'cancelled',
39+
});
40+
41+
expect(pageloadRequest.request).toEqual({
42+
headers: {
43+
'User-Agent': expect.any(String),
44+
},
45+
url: 'http://sentry-test.io/index.html',
46+
});
47+
48+
const spans = pageloadRequest.spans || [];
49+
50+
const redirectSpans = spans.filter(span => span.op === 'navigation.redirect');
51+
expect(redirectSpans).toHaveLength(3);
52+
53+
redirectSpans.forEach(redirectSpan => {
54+
expect(redirectSpan?.timestamp).toEqual(redirectSpan?.start_timestamp);
55+
expect(redirectSpan).toEqual({
56+
data: {
57+
'sentry.op': 'navigation.redirect',
58+
'sentry.origin': 'auto.navigation.browser',
59+
'sentry.source': 'url',
60+
},
61+
description: expect.stringContaining('/sub-page-redirect-'),
62+
op: 'navigation.redirect',
63+
origin: 'auto.navigation.browser',
64+
parent_span_id: pageloadRequest.contexts!.trace!.span_id,
65+
span_id: expect.any(String),
66+
start_timestamp: expect.any(Number),
67+
timestamp: expect.any(Number),
68+
trace_id: expect.any(String),
69+
});
70+
});
71+
72+
expect(navigationRequest.contexts?.trace?.op).toBe('navigation');
73+
expect(navigationRequest.transaction).toEqual('/next-page');
74+
75+
// 2 subsequent redirects belonging to the navigation root span
76+
expect(navigationRequest.spans?.filter(span => span.op === 'navigation.redirect')).toHaveLength(2);
77+
},
78+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
integrations: [Sentry.browserTracingIntegration()],
8+
tracesSampleRate: 1,
9+
debug: true,
10+
});
11+
12+
document.getElementById('btn1').addEventListener('click', () => {
13+
window.history.pushState({}, '', '/sub-page');
14+
15+
// then trigger redirect inside of this navigation, which should not be detected as a redirect
16+
// because the last click was less than 1.5s ago
17+
setTimeout(() => {
18+
document.getElementById('btn2').click();
19+
}, 100);
20+
});
21+
22+
document.getElementById('btn2').addEventListener('click', () => {
23+
setTimeout(() => {
24+
// navigation happens ~1100ms after the last navigation
25+
window.history.pushState({}, '', '/sub-page-2');
26+
}, 1000);
27+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<button id="btn1">Trigger navigation</button>
7+
<button id="btn2">Trigger navigation 2</button>
8+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../../utils/fixtures';
3+
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers';
4+
5+
sentryTest(
6+
'creates navigation root span if click happened within 1.5s of the last navigation',
7+
async ({ getLocalTestUrl, page }) => {
8+
if (shouldSkipTracingTest()) {
9+
sentryTest.skip();
10+
}
11+
12+
const url = await getLocalTestUrl({ testDir: __dirname });
13+
14+
const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload');
15+
const navigationRequestPromise = waitForTransactionRequest(
16+
page,
17+
event => event.contexts?.trace?.op === 'navigation' && event.transaction === '/sub-page',
18+
);
19+
const navigation2RequestPromise = waitForTransactionRequest(
20+
page,
21+
event => event.contexts?.trace?.op === 'navigation' && event.transaction === '/sub-page-2',
22+
);
23+
24+
await page.goto(url);
25+
26+
await pageloadRequestPromise;
27+
28+
// Now trigger navigation (since no span is active), and then a redirect in the navigation, with
29+
await page.click('#btn1');
30+
31+
const navigationRequest = envelopeRequestParser(await navigationRequestPromise);
32+
const navigation2Request = envelopeRequestParser(await navigation2RequestPromise);
33+
34+
expect(navigationRequest.contexts?.trace?.op).toBe('navigation');
35+
expect(navigationRequest.transaction).toEqual('/sub-page');
36+
37+
const spans = (navigationRequest.spans || []).filter(s => s.op === 'navigation.redirect');
38+
39+
expect(spans).toHaveLength(0);
40+
41+
expect(navigation2Request.contexts?.trace?.op).toBe('navigation');
42+
expect(navigation2Request.transaction).toEqual('/sub-page-2');
43+
44+
const spans2 = (navigation2Request.spans || []).filter(s => s.op === 'navigation.redirect');
45+
expect(spans2).toHaveLength(0);
46+
},
47+
);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,10 @@ document.getElementById('btn1').addEventListener('click', () => {
2121
}, 100);
2222
}, 400);
2323
});
24+
25+
document.getElementById('btn2').addEventListener('click', () => {
26+
// Trigger navigation later than click, so the last click is more than 300ms ago
27+
setTimeout(() => {
28+
window.history.pushState({}, '', '/sub-page-2');
29+
}, 400);
30+
});

0 commit comments

Comments
 (0)