Skip to content

Commit a386262

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

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
@@ -96,7 +96,9 @@ export function startSpan<T>(options: StartSpanOptions, callback: (span: Span) =
9696

9797
/**
9898
* Similar to `Sentry.startSpan`. Wraps a function with a transaction/span, but does not finish the span
99-
* after the function is done automatically. You'll have to call `span.end()` manually.
99+
* after the function is done automatically. Use the `finish` function passed to the callback,
100+
* to finish the span manually and sett he previous span as the active span again. If you don't use `finish`
101+
* but `span.end()` instead, the span will be ended but remain as the active span.
100102
*
101103
* The created span is the active span and will be used as parent by other spans created inside the function
102104
* and can be accessed via `Sentry.getActiveSpan()`, as long as the function is executed while the scope is active.
@@ -111,7 +113,7 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S
111113
}
112114

113115
const spanArguments = parseSentrySpanArguments(options);
114-
const { forceTransaction, parentSpan: customParentSpan } = options;
116+
const { forceTransaction, parentSpan: customParentSpan, scope: customScope } = options;
115117

116118
return withScope(options.scope, () => {
117119
// If `options.parentSpan` is defined, we want to wrap the callback in `withActiveSpan`
@@ -131,10 +133,16 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S
131133
scope,
132134
});
133135

136+
const previousActiveSpanOnCustomScope = customScope && _getSpanForScope(customScope);
134137
_setSpanForScope(scope, activeSpan);
135138

136139
function finishAndSetSpan(): void {
137140
activeSpan.end();
141+
if (customScope) {
142+
// If a custom scope is passed, we don't fork the scope. Therefore, we need to reset the
143+
// active span on the scope to the previous active span (or to undefined if there was none)
144+
_setSpanForScope(customScope, previousActiveSpanOnCustomScope);
145+
}
138146
}
139147

140148
return handleCallbackErrors(

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

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

782-
it('allows to pass a scope', () => {
783-
const initialScope = getCurrentScope();
782+
describe('allows to pass a scope', () => {
783+
it('with parent span', () => {
784+
const initialScope = getCurrentScope();
784785

785-
const manualScope = initialScope.clone();
786-
const parentSpan = new SentrySpan({ spanId: 'parent-span-id', sampled: true });
787-
_setSpanForScope(manualScope, parentSpan);
786+
const manualScope = initialScope.clone();
787+
const parentSpan = new SentrySpan({ spanId: 'parent-span-id', sampled: true });
788+
_setSpanForScope(manualScope, parentSpan);
788789

789-
startSpanManual({ name: 'GET users/[id]', scope: manualScope }, span => {
790-
expect(getCurrentScope()).not.toBe(initialScope);
791-
expect(getCurrentScope()).toBe(manualScope);
792-
expect(getActiveSpan()).toBe(span);
793-
expect(spanToJSON(span).parent_span_id).toBe('parent-span-id');
790+
let span1: Span | undefined;
794791

795-
span.end();
792+
startSpanManual({ name: 'GET users/[id]', scope: manualScope }, span => {
793+
span1 = span;
794+
expect(getCurrentScope()).not.toBe(initialScope);
795+
expect(getCurrentScope()).toBe(manualScope);
796+
expect(getActiveSpan()).toBe(span);
797+
expect(spanToJSON(span).parent_span_id).toBe('parent-span-id');
796798

797-
// Is still the active span
798-
expect(getActiveSpan()).toBe(span);
799+
span.end();
800+
801+
// Is still the active span
802+
expect(getActiveSpan()).toBe(span);
803+
});
804+
805+
startSpanManual({ name: 'POST users/[id]', scope: manualScope }, (span, finish) => {
806+
expect(getCurrentScope()).not.toBe(initialScope);
807+
expect(getCurrentScope()).toBe(manualScope);
808+
expect(getActiveSpan()).toBe(span);
809+
expect(spanToJSON(span).parent_span_id).toBe(span1?.spanContext().spanId);
810+
811+
finish();
812+
813+
// using finish() resets the scope correctly
814+
expect(getActiveSpan()).toBe(span1);
815+
});
816+
817+
expect(getCurrentScope()).toBe(initialScope);
818+
expect(getActiveSpan()).toBe(undefined);
799819
});
800820

801-
expect(getCurrentScope()).toBe(initialScope);
802-
expect(getActiveSpan()).toBe(undefined);
821+
it('without parent span', () => {
822+
const initialScope = getCurrentScope();
823+
const manualScope = initialScope.clone();
824+
825+
startSpanManual({ name: 'GET users/[id]', scope: manualScope }, (span, finish) => {
826+
expect(getCurrentScope()).not.toBe(initialScope);
827+
expect(getCurrentScope()).toBe(manualScope);
828+
expect(getActiveSpan()).toBe(span);
829+
expect(spanToJSON(span).parent_span_id).toBe(undefined);
830+
831+
finish();
832+
833+
// Is still the active span
834+
expect(getActiveSpan()).toBe(undefined);
835+
});
836+
837+
startSpanManual({ name: 'GET users/[id]', scope: manualScope }, (span, finish) => {
838+
expect(getCurrentScope()).not.toBe(initialScope);
839+
expect(getCurrentScope()).toBe(manualScope);
840+
expect(getActiveSpan()).toBe(span);
841+
expect(spanToJSON(span).parent_span_id).toBe(undefined);
842+
843+
finish();
844+
845+
// using finish() resets the scope correctly
846+
expect(getActiveSpan()).toBe(undefined);
847+
});
848+
849+
expect(getCurrentScope()).toBe(initialScope);
850+
expect(getActiveSpan()).toBe(undefined);
851+
});
803852
});
804853

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

0 commit comments

Comments
 (0)