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

Lines changed: 1 addition & 1 deletion
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://public@dsn.ingest.sentry.io/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://public@dsn.ingest.sentry.io/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>

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy