Skip to content

Commit 707afd6

Browse files
authored
fix(tracing): Ensure you can pass null as parentSpan in startSpan* (#12928)
Noticed this while writing docs, this makes this a bit harder to understand. With this change you can say that `parentSpan` behaves the same and accepts the same as `withActiveSpan`. See getsentry/sentry-docs#10729
1 parent 475d66f commit 707afd6

File tree

5 files changed

+64
-6
lines changed

5 files changed

+64
-6
lines changed

packages/core/src/tracing/trace.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export function startInactiveSpan(options: StartSpanOptions): Span {
157157
// If `options.parentSpan` is defined, we want to wrap the callback in `withActiveSpan`
158158
const wrapper = options.scope
159159
? (callback: () => Span) => withScope(options.scope, callback)
160-
: customParentSpan
160+
: customParentSpan !== undefined
161161
? (callback: () => Span) => withActiveSpan(customParentSpan, callback)
162162
: (callback: () => Span) => callback();
163163

@@ -445,8 +445,8 @@ function getParentSpan(scope: Scope): SentrySpan | undefined {
445445
return span;
446446
}
447447

448-
function getActiveSpanWrapper<T>(parentSpan?: Span): (callback: () => T) => T {
449-
return parentSpan
448+
function getActiveSpanWrapper<T>(parentSpan: Span | undefined | null): (callback: () => T) => T {
449+
return parentSpan !== undefined
450450
? (callback: () => T) => {
451451
return withActiveSpan(parentSpan, callback);
452452
}

packages/core/test/lib/tracing/trace.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,14 @@ describe('startSpan', () => {
282282
expect(getActiveSpan()).toBe(undefined);
283283
});
284284

285+
it('allows to pass parentSpan=null', () => {
286+
startSpan({ name: 'GET users/[id]' }, () => {
287+
startSpan({ name: 'GET users/[id]', parentSpan: null }, span => {
288+
expect(spanToJSON(span).parent_span_id).toBe(undefined);
289+
});
290+
});
291+
});
292+
285293
it('allows to force a transaction with forceTransaction=true', async () => {
286294
const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 });
287295
client = new TestClient(options);
@@ -693,6 +701,15 @@ describe('startSpanManual', () => {
693701
expect(getActiveSpan()).toBe(undefined);
694702
});
695703

704+
it('allows to pass parentSpan=null', () => {
705+
startSpan({ name: 'GET users/[id]' }, () => {
706+
startSpanManual({ name: 'child', parentSpan: null }, span => {
707+
expect(spanToJSON(span).parent_span_id).toBe(undefined);
708+
span.end();
709+
});
710+
});
711+
});
712+
696713
it('allows to force a transaction with forceTransaction=true', async () => {
697714
const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 });
698715
client = new TestClient(options);
@@ -1014,6 +1031,14 @@ describe('startInactiveSpan', () => {
10141031
expect(getActiveSpan()).toBeUndefined();
10151032
});
10161033

1034+
it('allows to pass parentSpan=null', () => {
1035+
startSpan({ name: 'outer' }, () => {
1036+
const span = startInactiveSpan({ name: 'GET users/[id]', parentSpan: null });
1037+
expect(spanToJSON(span).parent_span_id).toBe(undefined);
1038+
span.end();
1039+
});
1040+
});
1041+
10171042
it('allows to force a transaction with forceTransaction=true', async () => {
10181043
const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 });
10191044
client = new TestClient(options);

packages/opentelemetry/src/trace.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,8 @@ export function continueTrace<T>(options: Parameters<typeof baseContinueTrace>[0
286286
});
287287
}
288288

289-
function getActiveSpanWrapper<T>(parentSpan?: Span | SentrySpan): (callback: () => T) => T {
290-
return parentSpan
289+
function getActiveSpanWrapper<T>(parentSpan: Span | SentrySpan | undefined | null): (callback: () => T) => T {
290+
return parentSpan !== undefined
291291
? (callback: () => T) => {
292292
// We cast this, because the OTEL Span has a few more methods than our Span interface
293293
// TODO: Add these missing methods to the Span interface

packages/opentelemetry/test/trace.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,16 @@ describe('trace', () => {
325325
expect(getActiveSpan()).toBe(undefined);
326326
});
327327

328+
it('allows to pass parentSpan=null', () => {
329+
startSpan({ name: 'GET users/[id' }, () => {
330+
startSpan({ name: 'child', parentSpan: null }, span => {
331+
// Due to the way we propagate the scope in OTEL,
332+
// the parent_span_id is not actually undefined here, but comes from the propagation context
333+
expect(spanToJSON(span).parent_span_id).toBe(getCurrentScope().getPropagationContext().spanId);
334+
});
335+
});
336+
});
337+
328338
it('allows to force a transaction with forceTransaction=true', async () => {
329339
const client = getClient()!;
330340
const transactionEvents: Event[] = [];
@@ -577,6 +587,17 @@ describe('trace', () => {
577587
expect(getActiveSpan()).toBe(undefined);
578588
});
579589

590+
it('allows to pass parentSpan=null', () => {
591+
startSpan({ name: 'outer' }, () => {
592+
const span = startInactiveSpan({ name: 'test span', parentSpan: null });
593+
594+
// Due to the way we propagate the scope in OTEL,
595+
// the parent_span_id is not actually undefined here, but comes from the propagation context
596+
expect(spanToJSON(span).parent_span_id).toBe(getCurrentScope().getPropagationContext().spanId);
597+
span.end();
598+
});
599+
});
600+
580601
it('allows to force a transaction with forceTransaction=true', async () => {
581602
const client = getClient()!;
582603
const transactionEvents: Event[] = [];
@@ -856,6 +877,17 @@ describe('trace', () => {
856877
expect(getActiveSpan()).toBe(undefined);
857878
});
858879

880+
it('allows to pass parentSpan=null', () => {
881+
startSpan({ name: 'outer' }, () => {
882+
startSpanManual({ name: 'GET users/[id]', parentSpan: null }, span => {
883+
// Due to the way we propagate the scope in OTEL,
884+
// the parent_span_id is not actually undefined here, but comes from the propagation context
885+
expect(spanToJSON(span).parent_span_id).toBe(getCurrentScope().getPropagationContext().spanId);
886+
span.end();
887+
});
888+
});
889+
});
890+
859891
it('allows to force a transaction with forceTransaction=true', async () => {
860892
const client = getClient()!;
861893
const transactionEvents: Event[] = [];

packages/types/src/startSpanOptions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ export interface StartSpanOptions {
2020
/**
2121
* If provided, make the new span a child of this span.
2222
* If this is not provided, the new span will be a child of the currently active span.
23+
* If this is set to `null`, the new span will have no parent span.
2324
*/
24-
parentSpan?: Span;
25+
parentSpan?: Span | null;
2526

2627
/**
2728
* If set to true, this span will be forced to be treated as a transaction in the Sentry UI, if possible and applicable.

0 commit comments

Comments
 (0)