Skip to content

Commit 66ec822

Browse files
committed
feat(browser): Add previous_trace span links
1 parent 6efb6cb commit 66ec822

File tree

13 files changed

+525
-1
lines changed

13 files changed

+525
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { expect } from '@playwright/test';
2+
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE, type Event } from '@sentry/core';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import {
6+
envelopeRequestParser,
7+
getFirstSentryEnvelopeRequest,
8+
shouldSkipTracingTest,
9+
waitForTransactionRequest,
10+
} from '../../../../../utils/helpers';
11+
12+
sentryTest("navigation spans link back to previous trace's root span", async ({ getLocalTestUrl, page }) => {
13+
if (shouldSkipTracingTest()) {
14+
sentryTest.skip();
15+
}
16+
17+
const url = await getLocalTestUrl({ testDir: __dirname });
18+
19+
const pageloadRequest = await getFirstSentryEnvelopeRequest<Event>(page, url);
20+
const navigationRequest = await getFirstSentryEnvelopeRequest<Event>(page, `${url}#foo`);
21+
const navigation2Request = await getFirstSentryEnvelopeRequest<Event>(page, `${url}#bar`);
22+
23+
const pageloadTraceContext = pageloadRequest.contexts?.trace;
24+
const navigationTraceContext = navigationRequest.contexts?.trace;
25+
const navigation2TraceContext = navigation2Request.contexts?.trace;
26+
27+
const pageloadTraceId = pageloadTraceContext?.trace_id;
28+
const navigationTraceId = navigationTraceContext?.trace_id;
29+
const navigation2TraceId = navigation2TraceContext?.trace_id;
30+
31+
expect(pageloadTraceContext?.op).toBe('pageload');
32+
expect(navigationTraceContext?.op).toBe('navigation');
33+
expect(navigation2TraceContext?.op).toBe('navigation');
34+
35+
expect(pageloadTraceContext?.links).toBeUndefined();
36+
expect(navigationTraceContext?.links).toEqual([
37+
{
38+
trace_id: pageloadTraceId,
39+
span_id: pageloadTraceContext?.span_id,
40+
sampled: true,
41+
attributes: {
42+
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
43+
},
44+
},
45+
]);
46+
47+
expect(navigation2TraceContext?.links).toEqual([
48+
{
49+
trace_id: navigationTraceId,
50+
span_id: navigationTraceContext?.span_id,
51+
sampled: true,
52+
attributes: {
53+
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
54+
},
55+
},
56+
]);
57+
58+
expect(pageloadTraceId).not.toEqual(navigationTraceId);
59+
expect(navigationTraceId).not.toEqual(navigation2TraceId);
60+
expect(pageloadTraceId).not.toEqual(navigation2TraceId);
61+
});
62+
63+
sentryTest("doesn't link between hard page reloads by default", async ({ getLocalTestUrl, page }) => {
64+
if (shouldSkipTracingTest()) {
65+
sentryTest.skip();
66+
}
67+
68+
const url = await getLocalTestUrl({ testDir: __dirname });
69+
70+
const pageload1Event = await getFirstSentryEnvelopeRequest<Event>(page, url);
71+
72+
const pageload2RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');
73+
await page.reload();
74+
const pageload2Event = envelopeRequestParser(await pageload2RequestPromise);
75+
76+
expect(pageload1Event.contexts?.trace).toBeDefined();
77+
expect(pageload2Event.contexts?.trace).toBeDefined();
78+
expect(pageload1Event.contexts?.trace?.links).toBeUndefined();
79+
expect(pageload2Event.contexts?.trace?.links).toBeUndefined();
80+
});
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+
integrations: [Sentry.browserTracingIntegration()],
8+
tracesSampleRate: 1,
9+
});
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,46 @@
1+
import { expect } from '@playwright/test';
2+
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE, type Event } from '@sentry/core';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } 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 pageloadRequest = await getFirstSentryEnvelopeRequest<Event>(page, url);
17+
const navigationRequest = await getFirstSentryEnvelopeRequest<Event>(page, `${url}#foo`);
18+
19+
const pageloadTraceContext = pageloadRequest.contexts?.trace;
20+
const navigationTraceContext = navigationRequest.contexts?.trace;
21+
22+
const metaTagTraceId = '12345678901234567890123456789012';
23+
24+
const navigationTraceId = navigationTraceContext?.trace_id;
25+
26+
expect(pageloadTraceContext?.op).toBe('pageload');
27+
expect(navigationTraceContext?.op).toBe('navigation');
28+
29+
// sanity check
30+
expect(pageloadTraceContext?.trace_id).toBe(metaTagTraceId);
31+
expect(pageloadTraceContext?.links).toBeUndefined();
32+
33+
expect(navigationTraceContext?.links).toEqual([
34+
{
35+
trace_id: metaTagTraceId,
36+
span_id: pageloadTraceContext?.span_id,
37+
sampled: true,
38+
attributes: {
39+
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
40+
},
41+
},
42+
]);
43+
44+
expect(navigationTraceId).not.toEqual(metaTagTraceId);
45+
},
46+
);
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+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { expect } from '@playwright/test';
2+
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE, type Event } from '@sentry/core';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../../utils/helpers';
6+
7+
sentryTest('includes a span link to a previously negatively sampled span', async ({ getLocalTestUrl, page }) => {
8+
if (shouldSkipTracingTest()) {
9+
sentryTest.skip();
10+
}
11+
12+
const url = await getLocalTestUrl({ testDir: __dirname });
13+
14+
await page.goto(url);
15+
16+
const navigationRequest = await getFirstSentryEnvelopeRequest<Event>(page, `${url}#foo`);
17+
18+
const navigationTraceContext = navigationRequest.contexts?.trace;
19+
20+
const navigationTraceId = navigationTraceContext?.trace_id;
21+
22+
expect(navigationTraceContext?.op).toBe('navigation');
23+
24+
expect(navigationTraceContext?.links).toEqual([
25+
{
26+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
27+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
28+
sampled: false,
29+
attributes: {
30+
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
31+
},
32+
},
33+
]);
34+
35+
expect(navigationTraceId).not.toEqual(navigationTraceContext?.links![0].trace_id);
36+
});
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+
integrations: [Sentry.browserTracingIntegration({persistPreviousTrace: true})],
8+
tracesSampleRate: 1,
9+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { expect } from '@playwright/test';
2+
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE, type Event } from '@sentry/core';
3+
4+
import { sentryTest } from '../../../../../utils/fixtures';
5+
import {
6+
envelopeRequestParser,
7+
getFirstSentryEnvelopeRequest,
8+
shouldSkipTracingTest,
9+
waitForTransactionRequest,
10+
} from '../../../../../utils/helpers';
11+
12+
sentryTest(
13+
'adds link between hard page reload traces when opting into sessionStorage',
14+
async ({ getLocalTestUrl, page }) => {
15+
if (shouldSkipTracingTest()) {
16+
sentryTest.skip();
17+
}
18+
19+
const url = await getLocalTestUrl({ testDir: __dirname });
20+
21+
const pageload1Event = await getFirstSentryEnvelopeRequest<Event>(page, url);
22+
23+
const pageload2RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');
24+
await page.reload();
25+
const pageload2Event = envelopeRequestParser(await pageload2RequestPromise);
26+
27+
const pageload1TraceContext = pageload1Event.contexts?.trace;
28+
expect(pageload1TraceContext).toBeDefined();
29+
expect(pageload1TraceContext?.links).toBeUndefined();
30+
31+
expect(pageload2Event.contexts?.trace?.links).toEqual([
32+
{
33+
trace_id: pageload1TraceContext?.trace_id,
34+
span_id: pageload1TraceContext?.span_id,
35+
sampled: true,
36+
attributes: { [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace' },
37+
},
38+
]);
39+
40+
expect(pageload1TraceContext?.trace_id).not.toEqual(pageload2Event.contexts?.trace?.trace_id);
41+
},
42+
);

packages/browser/src/tracing/browserTracingIntegration.ts

+48
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ import { DEBUG_BUILD } from '../debug-build';
3535
import { WINDOW } from '../helpers';
3636
import { registerBackgroundTabDetection } from './backgroundtab';
3737
import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request';
38+
import type { PreviousTraceInfo } from './previousTrace';
39+
import {
40+
addPreviousTraceSpanLink,
41+
getPreviousTraceFromSessionStorage,
42+
storePreviousTraceInSessionStorage,
43+
} from './previousTrace';
3844

3945
export const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing';
4046

@@ -142,6 +148,25 @@ export interface BrowserTracingOptions {
142148
*/
143149
enableHTTPTimings: boolean;
144150

151+
/**
152+
* If enabled, previously started traces (e.g. pageload or navigation spans) will be linked
153+
* to the current trace. This lets you navigate across traces within a user journey in the
154+
* Sentry UI.
155+
*
156+
* Set `persistPreviousTrace` to `true` to connect traces across hard page reloads.
157+
*
158+
* @default true, this is turned on by default.
159+
*/
160+
enablePreviousTrace?: boolean;
161+
162+
/**
163+
* If set to true, the previous trace will be stored in `sessionStorage`, so that
164+
* traces can be linked across hard page refreshes.
165+
*
166+
* @default false, by default, previous trace data is only stored in-memory.
167+
*/
168+
persistPreviousTrace?: boolean;
169+
145170
/**
146171
* _experiments allows the user to send options to define how this integration works.
147172
*
@@ -175,6 +200,8 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = {
175200
enableLongTask: true,
176201
enableLongAnimationFrame: true,
177202
enableInp: true,
203+
enablePreviousTrace: true,
204+
persistPreviousTrace: false,
178205
_experiments: {},
179206
...defaultRequestInstrumentationOptions,
180207
};
@@ -214,6 +241,8 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
214241
enableHTTPTimings,
215242
instrumentPageLoad,
216243
instrumentNavigation,
244+
enablePreviousTrace,
245+
persistPreviousTrace,
217246
} = {
218247
...DEFAULT_BROWSER_TRACING_OPTIONS,
219248
..._options,
@@ -245,10 +274,19 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
245274
source: undefined,
246275
};
247276

277+
let previousTraceInfo: PreviousTraceInfo | undefined;
278+
if (enablePreviousTrace && persistPreviousTrace) {
279+
previousTraceInfo = getPreviousTraceFromSessionStorage();
280+
}
281+
248282
/** Create routing idle transaction. */
249283
function _createRouteSpan(client: Client, startSpanOptions: StartSpanOptions): void {
250284
const isPageloadTransaction = startSpanOptions.op === 'pageload';
251285

286+
if (enablePreviousTrace && previousTraceInfo) {
287+
previousTraceInfo = addPreviousTraceSpanLink(previousTraceInfo, startSpanOptions);
288+
}
289+
252290
const finalStartSpanOptions: StartSpanOptions = beforeStartSpan
253291
? beforeStartSpan(startSpanOptions)
254292
: startSpanOptions;
@@ -292,6 +330,16 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
292330
});
293331
setActiveIdleSpan(client, idleSpan);
294332

333+
if (enablePreviousTrace) {
334+
previousTraceInfo = {
335+
spanContext: idleSpan.spanContext(),
336+
startTimestamp: spanToJSON(idleSpan).start_timestamp,
337+
};
338+
if (persistPreviousTrace) {
339+
storePreviousTraceInSessionStorage(previousTraceInfo);
340+
}
341+
}
342+
295343
function emitFinish(): void {
296344
if (optionalWindowDocument && ['interactive', 'complete'].includes(optionalWindowDocument.readyState)) {
297345
client.emit('idleSpanEnableAutoFinish', idleSpan);

0 commit comments

Comments
 (0)