Skip to content

Commit cfcd226

Browse files
authored
feat(browser): Update propagationContext on spanEnd to keep trace consistent (#11631)
As spec'd out in #11599 and agreed upon in internal discussions, a trace should to stay consistent over the entire time span of one route. Therefore, when the initial pageload or navigation span ends, we update the scope's propagation context to keep span-specific attributes like the `sampled` decision and the dynamic sampling context on the scope's propagation context, even after the transaction has ended. This ensures that the trace data is consistent for the entire duration of the route. Subsequent navigations will reset the propagation context (see #11377).
1 parent daf2edf commit cfcd226

File tree

6 files changed

+79
-15
lines changed

6 files changed

+79
-15
lines changed

dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,11 @@ sentryTest(
130130
const request = await requestPromise;
131131
const headers = request.headers();
132132

133-
// sampling decision is deferred b/c of no active span at the time of request
133+
// sampling decision and DSC are continued from navigation span, even after it ended
134134
const navigationTraceId = navigationTraceContext?.trace_id;
135-
expect(headers['sentry-trace']).toMatch(new RegExp(`^${navigationTraceId}-[0-9a-f]{16}$`));
135+
expect(headers['sentry-trace']).toMatch(new RegExp(`^${navigationTraceId}-[0-9a-f]{16}-1$`));
136136
expect(headers['baggage']).toEqual(
137-
`sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId}`,
137+
`sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sample_rate=1,sentry-sampled=true`,
138138
);
139139
},
140140
);
@@ -203,11 +203,11 @@ sentryTest(
203203
const request = await xhrPromise;
204204
const headers = request.headers();
205205

206-
// sampling decision is deferred b/c of no active span at the time of request
206+
// sampling decision and DSC are continued from navigation span, even after it ended
207207
const navigationTraceId = navigationTraceContext?.trace_id;
208-
expect(headers['sentry-trace']).toMatch(new RegExp(`^${navigationTraceId}-[0-9a-f]{16}$`));
208+
expect(headers['sentry-trace']).toMatch(new RegExp(`^${navigationTraceId}-[0-9a-f]{16}-1$`));
209209
expect(headers['baggage']).toEqual(
210-
`sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId}`,
210+
`sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sample_rate=1,sentry-sampled=true`,
211211
);
212212
},
213213
);

dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/template.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="utf-8" />
55
<meta name="sentry-trace" content="12345678901234567890123456789012-1234567890123456-1" />
66
<meta name="baggage"
7-
content="sentry-trace_id=12345678901234567890123456789012,sentry-sample_rate=0.2,sentry-transaction=my-transaction,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=prod"/>
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"/>
88
</head>
99
<body>
1010
<button id="errorBtn">Throw Error</button>

dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
const META_TAG_TRACE_ID = '12345678901234567890123456789012';
1111
const META_TAG_PARENT_SPAN_ID = '1234567890123456';
1212
const META_TAG_BAGGAGE =
13-
'sentry-trace_id=12345678901234567890123456789012,sentry-sample_rate=0.2,sentry-transaction=my-transaction,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=prod';
13+
'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';
1414

1515
sentryTest(
1616
'create a new trace for a navigation after the <meta> tag pageload trace',

dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,11 @@ sentryTest(
124124
const request = await requestPromise;
125125
const headers = request.headers();
126126

127-
// sampling decision is deferred b/c of no active span at the time of request
127+
// sampling decision and DSC are continued from the pageload span even after it ended
128128
const pageloadTraceId = pageloadTraceContext?.trace_id;
129-
expect(headers['sentry-trace']).toMatch(new RegExp(`^${pageloadTraceId}-[0-9a-f]{16}$`));
129+
expect(headers['sentry-trace']).toMatch(new RegExp(`^${pageloadTraceId}-[0-9a-f]{16}-1$`));
130130
expect(headers['baggage']).toEqual(
131-
`sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId}`,
131+
`sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sample_rate=1,sentry-sampled=true`,
132132
);
133133
},
134134
);
@@ -191,11 +191,11 @@ sentryTest(
191191
const request = await requestPromise;
192192
const headers = request.headers();
193193

194-
// sampling decision is deferred b/c of no active span at the time of request
194+
// sampling decision and DSC are continued from the pageload span even after it ended
195195
const pageloadTraceId = pageloadTraceContext?.trace_id;
196-
expect(headers['sentry-trace']).toMatch(new RegExp(`^${pageloadTraceId}-[0-9a-f]{16}$`));
196+
expect(headers['sentry-trace']).toMatch(new RegExp(`^${pageloadTraceId}-[0-9a-f]{16}-1$`));
197197
expect(headers['baggage']).toEqual(
198-
`sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId}`,
198+
`sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sample_rate=1,sentry-sampled=true`,
199199
);
200200
},
201201
);

packages/browser/src/tracing/browserTracingIntegration.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import {
1414
getActiveSpan,
1515
getClient,
1616
getCurrentScope,
17+
getDynamicSamplingContextFromSpan,
1718
getIsolationScope,
1819
getRootSpan,
1920
registerSpanErrorInstrumentation,
21+
spanIsSampled,
2022
spanToJSON,
2123
startIdleSpan,
2224
withScope,
@@ -282,6 +284,29 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
282284
});
283285
});
284286

287+
// A trace should to stay the consistent over the entire time span of one route.
288+
// Therefore, when the initial pageload or navigation transaction ends, we update the
289+
// scope's propagation context to keep span-specific attributes like the `sampled` decision and
290+
// the dynamic sampling context valid, even after the transaction has ended.
291+
// This ensures that the trace data is consistent for the entire duration of the route.
292+
client.on('spanEnd', span => {
293+
const op = spanToJSON(span).op;
294+
if (span !== getRootSpan(span) || (op !== 'navigation' && op !== 'pageload')) {
295+
return;
296+
}
297+
298+
const scope = getCurrentScope();
299+
const oldPropagationContext = scope.getPropagationContext();
300+
301+
const newPropagationContext = {
302+
...oldPropagationContext,
303+
sampled: oldPropagationContext.sampled !== undefined ? oldPropagationContext.sampled : spanIsSampled(span),
304+
dsc: oldPropagationContext.dsc || getDynamicSamplingContextFromSpan(span),
305+
};
306+
307+
scope.setPropagationContext(newPropagationContext);
308+
});
309+
285310
if (options.instrumentPageLoad && WINDOW.location) {
286311
const startSpanOptions: StartSpanOptions = {
287312
name: WINDOW.location.pathname,

packages/browser/test/unit/tracing/browserTracingIntegration.test.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,7 @@ describe('browserTracingIntegration', () => {
638638
expect(getCurrentScope().getScopeData().transactionName).toBe('test navigation span');
639639
});
640640

641-
it("resets the scopes' propagationContexts", () => {
641+
it("updates the scopes' propagationContexts on a navigation", () => {
642642
const client = new BrowserClient(
643643
getDefaultBrowserClientOptions({
644644
integrations: [browserTracingIntegration()],
@@ -675,6 +675,45 @@ describe('browserTracingIntegration', () => {
675675
expect(newIsolationScopePropCtx?.traceId).not.toEqual(oldIsolationScopePropCtx?.traceId);
676676
expect(newCurrentScopePropCtx?.traceId).not.toEqual(oldCurrentScopePropCtx?.traceId);
677677
});
678+
679+
it("saves the span's sampling decision and its DSC on the propagationContext when the span finishes", () => {
680+
const client = new BrowserClient(
681+
getDefaultBrowserClientOptions({
682+
tracesSampleRate: 1,
683+
integrations: [browserTracingIntegration({ instrumentPageLoad: false })],
684+
}),
685+
);
686+
setCurrentClient(client);
687+
client.init();
688+
689+
const navigationSpan = startBrowserTracingNavigationSpan(client, {
690+
name: 'mySpan',
691+
attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route' },
692+
});
693+
694+
const propCtxBeforeEnd = getCurrentScope().getPropagationContext();
695+
expect(propCtxBeforeEnd).toStrictEqual({
696+
spanId: expect.stringMatching(/[a-f0-9]{16}/),
697+
traceId: expect.stringMatching(/[a-f0-9]{32}/),
698+
});
699+
700+
navigationSpan!.end();
701+
702+
const propCtxAfterEnd = getCurrentScope().getPropagationContext();
703+
expect(propCtxAfterEnd).toStrictEqual({
704+
spanId: propCtxBeforeEnd?.spanId,
705+
traceId: propCtxBeforeEnd?.traceId,
706+
sampled: true,
707+
dsc: {
708+
environment: 'production',
709+
public_key: 'examplePublicKey',
710+
sample_rate: '1',
711+
sampled: 'true',
712+
transaction: 'mySpan',
713+
trace_id: propCtxBeforeEnd?.traceId,
714+
},
715+
});
716+
});
678717
});
679718

680719
describe('using the <meta> tag data', () => {

0 commit comments

Comments
 (0)