Skip to content

Commit 7965770

Browse files
committed
Align gating of DSC in getDynamicSamplingContextFromSpan with getTraceData
1 parent b65741e commit 7965770

2 files changed

Lines changed: 43 additions & 5 deletions

File tree

packages/core/src/tracing/dynamicSamplingContext.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,20 @@ export function getDynamicSamplingContextFromSpan(span: Span): Readonly<Partial<
106106
return applyLocalSampleRateToDsc(frozenDsc);
107107
}
108108

109-
// For a non-recording placeholder (Tracing without Performance), the DSC is not carried on the
110-
// span; the scope is the source of truth. Resolve it from the span's captured scope: continued
111-
// traces keep the incoming DSC, new traces derive it from the client (without a local transaction).
112-
if (spanIsNonRecordingSpan(rootSpan)) {
109+
// For a non-recording placeholder in Tracing without Performance (TwP) mode, the DSC is not
110+
// carried on the span; the scope is the source of truth. Resolve it from the span's captured
111+
// scope: continued traces keep the incoming DSC, new traces derive it from the client.
112+
//
113+
// We gate this on `!hasSpansEnabled()` so it mirrors the `sentry-trace` source in `getTraceData`:
114+
// with tracing enabled, a non-recording span (e.g. an `onlyIfParent` placeholder) keeps deriving
115+
// its DSC from the span/client so the baggage agrees with the `-0` decision that `spanToTraceHeader`
116+
// encodes for `sentry-trace`. Without this guard the two headers can disagree.
117+
//
118+
// We spread into a new object so applying the local sample rate can't mutate the scope's DSC.
119+
if (spanIsNonRecordingSpan(rootSpan) && !hasSpansEnabled(client.getOptions())) {
113120
const capturedScope = getCapturedScopesOnSpan(rootSpan).scope;
114121
if (capturedScope) {
115-
return applyLocalSampleRateToDsc(getDynamicSamplingContextFromScope(client, capturedScope));
122+
return applyLocalSampleRateToDsc({ ...getDynamicSamplingContextFromScope(client, capturedScope) });
116123
}
117124
}
118125

packages/core/test/lib/utils/traceData.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,37 @@ describe('getTraceData', () => {
213213
});
214214
});
215215

216+
it('does not defer DSC to the scope for an onlyIfParent placeholder in tracing mode', () => {
217+
// With tracing enabled, an `onlyIfParent` placeholder without a parent is a non-recording span.
218+
// Its `sentry-trace` is read from the span (`-0`), so unlike a TwP placeholder its DSC must not
219+
// defer to the scope's continued decision (`sampled=true`) — otherwise the two headers disagree.
220+
setupClient({ tracesSampleRate: 1 });
221+
222+
getCurrentScope().setPropagationContext({
223+
traceId: '12345678901234567890123456789012',
224+
sampleRand: 0.42,
225+
sampled: true,
226+
dsc: {
227+
environment: 'production',
228+
public_key: '123',
229+
trace_id: '12345678901234567890123456789012',
230+
sampled: 'true',
231+
transaction: 'continued-root-txn',
232+
},
233+
});
234+
235+
startSpan({ name: 'child', onlyIfParent: true }, () => {
236+
const data = getTraceData();
237+
238+
expect(data['sentry-trace']).toMatch(/^12345678901234567890123456789012-[a-f0-9]{16}-0$/);
239+
// The baggage agrees with the `-0` decision instead of carrying the upstream `sampled=true`.
240+
expect(data.baggage).toContain('sentry-sampled=false');
241+
expect(data.baggage).not.toContain('sentry-sampled=true');
242+
expect(data.baggage).not.toContain('sentry-transaction=continued-root-txn');
243+
expect(data.baggage).toContain('sentry-trace_id=12345678901234567890123456789012');
244+
});
245+
});
246+
216247
it('keeps an explicit negative sampling decision for an active unsampled span', () => {
217248
setupClient({ tracesSampleRate: 0 });
218249

0 commit comments

Comments
 (0)