Skip to content

Commit bd3f4bc

Browse files
committed
fix(core): Restore previously active span when passing a custom scope to startSpanManual
1 parent c864c76 commit bd3f4bc

File tree

2 files changed

+74
-17
lines changed

2 files changed

+74
-17
lines changed

packages/core/src/tracing/trace.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ export function startSpan<T>(options: StartSpanOptions, callback: (span: Span) =
9292

9393
/**
9494
* Similar to `Sentry.startSpan`. Wraps a function with a transaction/span, but does not finish the span
95-
* after the function is done automatically. You'll have to call `span.end()` manually.
95+
* after the function is done automatically. Use the `finish` function passed to the callback,
96+
* to finish the span manually and sett he previous span as the active span again. If you don't use `finish`
97+
* but `span.end()` instead, the span will be ended but remain as the active span.
9698
*
9799
* The created span is the active span and will be used as parent by other spans created inside the function
98100
* and can be accessed via `Sentry.getActiveSpan()`, as long as the function is executed while the scope is active.
@@ -107,7 +109,7 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S
107109
}
108110

109111
const spanArguments = parseSentrySpanArguments(options);
110-
const { forceTransaction, parentSpan: customParentSpan } = options;
112+
const { forceTransaction, parentSpan: customParentSpan, scope: customScope } = options;
111113

112114
return withScope(options.scope, () => {
113115
// If `options.parentSpan` is defined, we want to wrap the callback in `withActiveSpan`
@@ -127,10 +129,16 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S
127129
scope,
128130
});
129131

132+
const previousActiveSpanOnCustomScope = customScope && _getSpanForScope(customScope);
130133
_setSpanForScope(scope, activeSpan);
131134

132135
function finishAndSetSpan(): void {
133136
activeSpan.end();
137+
if (customScope) {
138+
// If a custom scope is passed, we don't fork the scope. Therefore, we need to reset the
139+
// active span on the scope to the previous active span (or to undefined if there was none)
140+
_setSpanForScope(customScope, previousActiveSpanOnCustomScope);
141+
}
134142
}
135143

136144
return handleCallbackErrors(

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

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -732,27 +732,76 @@ describe('startSpanManual', () => {
732732
expect(getActiveSpan()).toBe(undefined);
733733
});
734734

735-
it('allows to pass a scope', () => {
736-
const initialScope = getCurrentScope();
735+
describe('allows to pass a scope', () => {
736+
it('with parent span', () => {
737+
const initialScope = getCurrentScope();
737738

738-
const manualScope = initialScope.clone();
739-
const parentSpan = new SentrySpan({ spanId: 'parent-span-id', sampled: true });
740-
_setSpanForScope(manualScope, parentSpan);
739+
const manualScope = initialScope.clone();
740+
const parentSpan = new SentrySpan({ spanId: 'parent-span-id', sampled: true });
741+
_setSpanForScope(manualScope, parentSpan);
741742

742-
startSpanManual({ name: 'GET users/[id]', scope: manualScope }, span => {
743-
expect(getCurrentScope()).not.toBe(initialScope);
744-
expect(getCurrentScope()).toBe(manualScope);
745-
expect(getActiveSpan()).toBe(span);
746-
expect(spanToJSON(span).parent_span_id).toBe('parent-span-id');
743+
let span1: Span | undefined;
747744

748-
span.end();
745+
startSpanManual({ name: 'GET users/[id]', scope: manualScope }, span => {
746+
span1 = span;
747+
expect(getCurrentScope()).not.toBe(initialScope);
748+
expect(getCurrentScope()).toBe(manualScope);
749+
expect(getActiveSpan()).toBe(span);
750+
expect(spanToJSON(span).parent_span_id).toBe('parent-span-id');
749751

750-
// Is still the active span
751-
expect(getActiveSpan()).toBe(span);
752+
span.end();
753+
754+
// Is still the active span
755+
expect(getActiveSpan()).toBe(span);
756+
});
757+
758+
startSpanManual({ name: 'POST users/[id]', scope: manualScope }, (span, finish) => {
759+
expect(getCurrentScope()).not.toBe(initialScope);
760+
expect(getCurrentScope()).toBe(manualScope);
761+
expect(getActiveSpan()).toBe(span);
762+
expect(spanToJSON(span).parent_span_id).toBe(span1?.spanContext().spanId);
763+
764+
finish();
765+
766+
// using finish() resets the scope correctly
767+
expect(getActiveSpan()).toBe(span1);
768+
});
769+
770+
expect(getCurrentScope()).toBe(initialScope);
771+
expect(getActiveSpan()).toBe(undefined);
752772
});
753773

754-
expect(getCurrentScope()).toBe(initialScope);
755-
expect(getActiveSpan()).toBe(undefined);
774+
it('without parent span', () => {
775+
const initialScope = getCurrentScope();
776+
const manualScope = initialScope.clone();
777+
778+
startSpanManual({ name: 'GET users/[id]', scope: manualScope }, (span, finish) => {
779+
expect(getCurrentScope()).not.toBe(initialScope);
780+
expect(getCurrentScope()).toBe(manualScope);
781+
expect(getActiveSpan()).toBe(span);
782+
expect(spanToJSON(span).parent_span_id).toBe(undefined);
783+
784+
finish();
785+
786+
// Is still the active span
787+
expect(getActiveSpan()).toBe(undefined);
788+
});
789+
790+
startSpanManual({ name: 'GET users/[id]', scope: manualScope }, (span, finish) => {
791+
expect(getCurrentScope()).not.toBe(initialScope);
792+
expect(getCurrentScope()).toBe(manualScope);
793+
expect(getActiveSpan()).toBe(span);
794+
expect(spanToJSON(span).parent_span_id).toBe(undefined);
795+
796+
finish();
797+
798+
// using finish() resets the scope correctly
799+
expect(getActiveSpan()).toBe(undefined);
800+
});
801+
802+
expect(getCurrentScope()).toBe(initialScope);
803+
expect(getActiveSpan()).toBe(undefined);
804+
});
756805
});
757806

758807
it('allows to pass a parentSpan', () => {

0 commit comments

Comments
 (0)