Skip to content

Commit 79b8f1f

Browse files
committed
fix(core): Restore previously active span when passing a custom scope to startSpanManual
1 parent 7fb7632 commit 79b8f1f

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
@@ -780,27 +780,76 @@ describe('startSpanManual', () => {
780780
expect(getActiveSpan()).toBe(undefined);
781781
});
782782

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

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

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

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

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

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

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

0 commit comments

Comments
 (0)