Skip to content

Commit b65741e

Browse files
committed
Preserve continued trace DSC for onlyIfParent placeholders
1 parent 8b31869 commit b65741e

3 files changed

Lines changed: 49 additions & 13 deletions

File tree

packages/core/src/tracing/dynamicSamplingContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export function getDynamicSamplingContextFromSpan(span: Span): Readonly<Partial<
107107
}
108108

109109
// 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
110+
// span; the scope is the source of truth. Resolve it from the span's captured scope: continued
111111
// traces keep the incoming DSC, new traces derive it from the client (without a local transaction).
112112
if (spanIsNonRecordingSpan(rootSpan)) {
113113
const capturedScope = getCapturedScopesOnSpan(rootSpan).scope;

packages/core/src/tracing/trace.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,14 @@ export function startSpan<T>(options: StartSpanOptions, callback: (span: Span) =
7171

7272
const missingRequiredParent = options.onlyIfParent && !parentSpan;
7373
const activeSpan = missingRequiredParent
74-
? new SentryNonRecordingSpan({ traceId: scope.getPropagationContext().traceId })
74+
? startMissingRequiredParentSpan(scope, client)
7575
: createChildOrRootSpan({
7676
parentSpan,
7777
spanArguments,
7878
forceTransaction,
7979
scope,
8080
});
8181

82-
if (missingRequiredParent) {
83-
client?.recordDroppedEvent('no_parent_span', 'span');
84-
}
85-
8682
// Ignored root spans still need to be set on scope so that `getActiveSpan()` returns them
8783
// and descendants are also non-recording. Ignored child spans don't need this because
8884
// the parent span is already on scope.
@@ -138,18 +134,14 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S
138134

139135
const missingRequiredParent = options.onlyIfParent && !parentSpan;
140136
const activeSpan = missingRequiredParent
141-
? new SentryNonRecordingSpan({ traceId: scope.getPropagationContext().traceId })
137+
? startMissingRequiredParentSpan(scope, getClient())
142138
: createChildOrRootSpan({
143139
parentSpan,
144140
spanArguments,
145141
forceTransaction,
146142
scope,
147143
});
148144

149-
if (missingRequiredParent) {
150-
getClient()?.recordDroppedEvent('no_parent_span', 'span');
151-
}
152-
153145
// We don't set ignored child spans onto the scope because there likely is an active,
154146
// unignored span on the scope already.
155147
if (!_isIgnoredSpan(activeSpan) || !parentSpan) {
@@ -208,8 +200,7 @@ export function startInactiveSpan(options: StartSpanOptions): Span {
208200
const missingRequiredParent = options.onlyIfParent && !parentSpan;
209201

210202
if (missingRequiredParent) {
211-
client?.recordDroppedEvent('no_parent_span', 'span');
212-
return new SentryNonRecordingSpan({ traceId: scope.getPropagationContext().traceId });
203+
return startMissingRequiredParentSpan(scope, client);
213204
}
214205

215206
return createChildOrRootSpan({
@@ -332,6 +323,19 @@ export function startNewTrace<T>(callback: () => T): T {
332323
});
333324
}
334325

326+
/**
327+
* The placeholder returned from `startSpan*` when `onlyIfParent` is set but there is no parent span.
328+
* It carries the current trace id and captured scopes so the trace data it propagates (and any nested
329+
* span that resolves it as its root via `getRootSpan`) reads its DSC from the scope, preserving a
330+
* continued trace's DSC instead of fabricating a fresh client one. Also records the dropped-span outcome.
331+
*/
332+
function startMissingRequiredParentSpan(scope: Scope, client: Client | undefined): SentryNonRecordingSpan {
333+
client?.recordDroppedEvent('no_parent_span', 'span');
334+
const span = new SentryNonRecordingSpan({ traceId: scope.getPropagationContext().traceId });
335+
setCapturedScopesOnSpan(span, scope, getIsolationScope());
336+
return span;
337+
}
338+
335339
function createChildOrRootSpan({
336340
parentSpan,
337341
spanArguments,

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,38 @@ describe('getTraceData', () => {
181181
});
182182
});
183183

184+
it('preserves the continued-trace DSC under (and nested within) an onlyIfParent placeholder', () => {
185+
// The placeholder captures the scope, so it (and any nested span that resolves it as its root
186+
// via `getRootSpan`) reads the continued trace's DSC from the scope instead of fabricating a
187+
// fresh client one.
188+
setupClient({ tracesSampleRate: undefined });
189+
190+
getCurrentScope().setPropagationContext({
191+
traceId: '12345678901234567890123456789012',
192+
sampleRand: 0.42,
193+
sampled: true,
194+
dsc: {
195+
environment: 'production',
196+
public_key: '123',
197+
trace_id: '12345678901234567890123456789012',
198+
sampled: 'true',
199+
sample_rate: '0.5',
200+
transaction: 'continued-root-txn',
201+
},
202+
});
203+
204+
startSpan({ name: 'parent', onlyIfParent: true }, () => {
205+
expect(getTraceData().baggage).toContain('sentry-transaction=continued-root-txn');
206+
207+
startSpan({ name: 'nested' }, () => {
208+
const baggage = getTraceData().baggage;
209+
expect(baggage).toContain('sentry-transaction=continued-root-txn');
210+
expect(baggage).toContain('sentry-sample_rate=0.5');
211+
expect(baggage).toContain('sentry-sampled=true');
212+
});
213+
});
214+
});
215+
184216
it('keeps an explicit negative sampling decision for an active unsampled span', () => {
185217
setupClient({ tracesSampleRate: 0 });
186218

0 commit comments

Comments
 (0)