Skip to content

Commit 1a4fa0c

Browse files
authored
feat(browser): Add previous_trace span links (#15569)
This PR adds logic to set the `previous_trace ` span link on root spans (via `browserTracingIntegration`). - added `linkPreviousTrace` integration option to control the trace linking behaviour: - everything is implemented within `browserTracingIntegration`, meaning there's no bundle size hit for error-only users or users who only send manual spans (the latter is a tradeoff but I think it's a fair one) - added unit and integration tests for a bunch of scenarios closes #14992 UPDATE: I rewrote the public API options from having two options (`enablePreviousTrace` and `persistPreviousTrace`) to only one which controls both aspects.
1 parent 267ebe0 commit 1a4fa0c

File tree

24 files changed

+1010
-4
lines changed

24 files changed

+1010
-4
lines changed

.size-limit.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ module.exports = [
4747
path: 'packages/browser/build/npm/esm/index.js',
4848
import: createImport('init', 'browserTracingIntegration', 'replayIntegration'),
4949
gzip: true,
50-
limit: '75.5 KB',
50+
limit: '76 KB',
5151
},
5252
{
5353
name: '@sentry/browser (incl. Tracing, Replay) - with treeshaking flags',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const btn1 = document.getElementById('btn1');
2+
const btn2 = document.getElementById('btn2');
3+
4+
btn1.addEventListener('click', () => {
5+
Sentry.startNewTrace(() => {
6+
Sentry.startSpan({name: 'custom root span 1', op: 'custom'}, () => {});
7+
});
8+
});
9+
10+
11+
btn2.addEventListener('click', () => {
12+
Sentry.startNewTrace(() => {
13+
Sentry.startSpan({name: 'custom root span 2', op: 'custom'}, () => {});
14+
});
15+
});
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+
<button id="btn1">
7+
<button id="btn2">
8+
</button>
9+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { expect } from '@playwright/test';
2+
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers';
6+
7+
sentryTest('manually started custom traces are linked correctly in the chain', async ({ getLocalTestUrl, page }) => {
8+
if (shouldSkipTracingTest()) {
9+
sentryTest.skip();
10+
}
11+
12+
const url = await getLocalTestUrl({ testDir: __dirname });
13+
14+
const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => {
15+
const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');
16+
await page.goto(url);
17+
const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise);
18+
return pageloadRequest.contexts?.trace;
19+
});
20+
21+
const customTrace1Context = await sentryTest.step('Custom trace', async () => {
22+
const customTrace1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom');
23+
await page.locator('#btn1').click();
24+
const customTrace1Event = envelopeRequestParser(await customTrace1RequestPromise);
25+
26+
const customTraceCtx = customTrace1Event.contexts?.trace;
27+
28+
expect(customTraceCtx?.trace_id).not.toEqual(pageloadTraceContext?.trace_id);
29+
expect(customTraceCtx?.links).toEqual([
30+
{
31+
trace_id: pageloadTraceContext?.trace_id,
32+
span_id: pageloadTraceContext?.span_id,
33+
sampled: true,
34+
attributes: {
35+
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
36+
},
37+
},
38+
]);
39+
40+
return customTraceCtx;
41+
});
42+
43+
await sentryTest.step('Navigation', async () => {
44+
const navigation1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation');
45+
await page.goto(`${url}#foo`);
46+
const navigationEvent = envelopeRequestParser(await navigation1RequestPromise);
47+
const navTraceContext = navigationEvent.contexts?.trace;
48+
49+
expect(navTraceContext?.trace_id).not.toEqual(customTrace1Context?.trace_id);
50+
expect(navTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id);
51+
52+
expect(navTraceContext?.links).toEqual([
53+
{
54+
trace_id: customTrace1Context?.trace_id,
55+
span_id: customTrace1Context?.span_id,
56+
sampled: true,
57+
attributes: {
58+
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
59+
},
60+
},
61+
]);
62+
});
63+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { expect } from '@playwright/test';
2+
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers';
6+
7+
sentryTest("navigation spans link back to previous trace's root span", async ({ getLocalTestUrl, page }) => {
8+
if (shouldSkipTracingTest()) {
9+
sentryTest.skip();
10+
}
11+
12+
const url = await getLocalTestUrl({ testDir: __dirname });
13+
14+
const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => {
15+
const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');
16+
await page.goto(url);
17+
const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise);
18+
return pageloadRequest.contexts?.trace;
19+
});
20+
21+
const navigation1TraceContext = await sentryTest.step('First navigation', async () => {
22+
const navigation1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation');
23+
await page.goto(`${url}#foo`);
24+
const navigation1Request = envelopeRequestParser(await navigation1RequestPromise);
25+
return navigation1Request.contexts?.trace;
26+
});
27+
28+
const navigation2TraceContext = await sentryTest.step('Second navigation', async () => {
29+
const navigation2RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation');
30+
await page.goto(`${url}#bar`);
31+
const navigation2Request = envelopeRequestParser(await navigation2RequestPromise);
32+
return navigation2Request.contexts?.trace;
33+
});
34+
35+
const pageloadTraceId = pageloadTraceContext?.trace_id;
36+
const navigation1TraceId = navigation1TraceContext?.trace_id;
37+
const navigation2TraceId = navigation2TraceContext?.trace_id;
38+
39+
expect(pageloadTraceContext?.links).toBeUndefined();
40+
41+
expect(navigation1TraceContext?.links).toEqual([
42+
{
43+
trace_id: pageloadTraceId,
44+
span_id: pageloadTraceContext?.span_id,
45+
sampled: true,
46+
attributes: {
47+
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
48+
},
49+
},
50+
]);
51+
52+
expect(navigation2TraceContext?.links).toEqual([
53+
{
54+
trace_id: navigation1TraceId,
55+
span_id: navigation1TraceContext?.span_id,
56+
sampled: true,
57+
attributes: {
58+
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
59+
},
60+
},
61+
]);
62+
63+
expect(pageloadTraceId).not.toEqual(navigation1TraceId);
64+
expect(navigation1TraceId).not.toEqual(navigation2TraceId);
65+
expect(pageloadTraceId).not.toEqual(navigation2TraceId);
66+
});
67+
68+
sentryTest("doesn't link between hard page reloads by default", async ({ getLocalTestUrl, page }) => {
69+
if (shouldSkipTracingTest()) {
70+
sentryTest.skip();
71+
}
72+
73+
const url = await getLocalTestUrl({ testDir: __dirname });
74+
75+
await sentryTest.step('First pageload', async () => {
76+
const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');
77+
await page.goto(url);
78+
const pageload1Event = envelopeRequestParser(await pageloadRequestPromise);
79+
80+
expect(pageload1Event.contexts?.trace).toBeDefined();
81+
expect(pageload1Event.contexts?.trace?.links).toBeUndefined();
82+
});
83+
84+
await sentryTest.step('Second pageload', async () => {
85+
const pageload2RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');
86+
await page.reload();
87+
const pageload2Event = envelopeRequestParser(await pageload2RequestPromise);
88+
89+
expect(pageload2Event.contexts?.trace).toBeDefined();
90+
expect(pageload2Event.contexts?.trace?.links).toBeUndefined();
91+
});
92+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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: [Sentry.browserTracingIntegration({_experiments: {enableInteractions: true}})],
9+
});
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="btn">
7+
</button>
8+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { expect } from '@playwright/test';
2+
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers';
6+
7+
/*
8+
This is quite peculiar behavior but it's a result of the route-based trace lifetime.
9+
Once we shortened trace lifetime, this whole scenario will change as the interaction
10+
spans will be their own trace. So most likely, we can replace this test with a new one
11+
that covers the new default behavior.
12+
*/
13+
sentryTest(
14+
'only the first root spans in the trace link back to the previous trace',
15+
async ({ getLocalTestUrl, page }) => {
16+
if (shouldSkipTracingTest()) {
17+
sentryTest.skip();
18+
}
19+
20+
const url = await getLocalTestUrl({ testDir: __dirname });
21+
22+
const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => {
23+
const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');
24+
await page.goto(url);
25+
26+
const pageloadEvent = envelopeRequestParser(await pageloadRequestPromise);
27+
const traceContext = pageloadEvent.contexts?.trace;
28+
29+
expect(traceContext).toBeDefined();
30+
expect(traceContext?.links).toBeUndefined();
31+
32+
return traceContext;
33+
});
34+
35+
await sentryTest.step('Click Before navigation', async () => {
36+
const interactionRequestPromise = waitForTransactionRequest(page, evt => {
37+
return evt.contexts?.trace?.op === 'ui.action.click';
38+
});
39+
await page.click('#btn');
40+
41+
const interactionEvent = envelopeRequestParser(await interactionRequestPromise);
42+
const interactionTraceContext = interactionEvent.contexts?.trace;
43+
44+
// sanity check: route-based trace lifetime means the trace_id should be the same
45+
expect(interactionTraceContext?.trace_id).toBe(pageloadTraceContext?.trace_id);
46+
47+
// no links yet as previous root span belonged to same trace
48+
expect(interactionTraceContext?.links).toBeUndefined();
49+
});
50+
51+
const navigationTraceContext = await sentryTest.step('Navigation', async () => {
52+
const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation');
53+
await page.goto(`${url}#foo`);
54+
const navigationEvent = envelopeRequestParser(await navigationRequestPromise);
55+
56+
const traceContext = navigationEvent.contexts?.trace;
57+
58+
expect(traceContext?.op).toBe('navigation');
59+
expect(traceContext?.links).toEqual([
60+
{
61+
trace_id: pageloadTraceContext?.trace_id,
62+
span_id: pageloadTraceContext?.span_id,
63+
sampled: true,
64+
attributes: {
65+
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
66+
},
67+
},
68+
]);
69+
70+
expect(traceContext?.trace_id).not.toEqual(traceContext?.links![0].trace_id);
71+
return traceContext;
72+
});
73+
74+
await sentryTest.step('Click After navigation', async () => {
75+
const interactionRequestPromise = waitForTransactionRequest(page, evt => {
76+
return evt.contexts?.trace?.op === 'ui.action.click';
77+
});
78+
await page.click('#btn');
79+
const interactionEvent = envelopeRequestParser(await interactionRequestPromise);
80+
81+
const interactionTraceContext = interactionEvent.contexts?.trace;
82+
83+
// sanity check: route-based trace lifetime means the trace_id should be the same
84+
expect(interactionTraceContext?.trace_id).toBe(navigationTraceContext?.trace_id);
85+
86+
// since this is the second root span in the trace, it doesn't link back to the previous trace
87+
expect(interactionTraceContext?.links).toBeUndefined();
88+
});
89+
},
90+
);
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+
<meta name="sentry-trace" content="12345678901234567890123456789012-1234567890123456-1" />
6+
<meta name="baggage"
7+
content="sentry-trace_id=12345678901234567890123456789012,sentry-sample_rate=0.2,sentry-sampled=true,sentry-transaction=my-transaction,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=prod,sentry-sample_rand=0.42"/>
8+
</head>
9+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { expect } from '@playwright/test';
2+
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers';
6+
7+
sentryTest(
8+
"links back to previous trace's local root span if continued from meta tags",
9+
async ({ getLocalTestUrl, page }) => {
10+
if (shouldSkipTracingTest()) {
11+
sentryTest.skip();
12+
}
13+
14+
const url = await getLocalTestUrl({ testDir: __dirname });
15+
16+
const metaTagTraceId = '12345678901234567890123456789012';
17+
18+
const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => {
19+
const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');
20+
await page.goto(url);
21+
const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise);
22+
23+
const traceContext = pageloadRequest.contexts?.trace;
24+
25+
// sanity check
26+
expect(traceContext?.trace_id).toBe(metaTagTraceId);
27+
28+
expect(traceContext?.links).toBeUndefined();
29+
30+
return traceContext;
31+
});
32+
33+
const navigationTraceContext = await sentryTest.step('Navigation', async () => {
34+
const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation');
35+
await page.goto(`${url}#foo`);
36+
const navigationRequest = envelopeRequestParser(await navigationRequestPromise);
37+
return navigationRequest.contexts?.trace;
38+
});
39+
40+
const navigationTraceId = navigationTraceContext?.trace_id;
41+
42+
expect(navigationTraceContext?.links).toEqual([
43+
{
44+
trace_id: metaTagTraceId,
45+
span_id: pageloadTraceContext?.span_id,
46+
sampled: true,
47+
attributes: {
48+
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
49+
},
50+
},
51+
]);
52+
53+
expect(navigationTraceId).not.toEqual(metaTagTraceId);
54+
},
55+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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+
tracesSampler: (ctx) => {
9+
if (ctx.attributes['sentry.origin'] === 'auto.pageload.browser') {
10+
return 0;
11+
}
12+
return 1;
13+
}
14+
});

0 commit comments

Comments
 (0)