Skip to content

Commit 6f24b8f

Browse files
committed
rework implementation to fork scope instead of restoring previously active span
1 parent 79b8f1f commit 6f24b8f

File tree

2 files changed

+57
-39
lines changed

2 files changed

+57
-39
lines changed

packages/core/src/tracing/trace.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,7 @@ 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. 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.
99+
* after the function is done automatically. Use `span.end()` to end the span.
102100
*
103101
* The created span is the active span and will be used as parent by other spans created inside the function
104102
* and can be accessed via `Sentry.getActiveSpan()`, as long as the function is executed while the scope is active.
@@ -115,7 +113,9 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S
115113
const spanArguments = parseSentrySpanArguments(options);
116114
const { forceTransaction, parentSpan: customParentSpan, scope: customScope } = options;
117115

118-
return withScope(options.scope, () => {
116+
const customForkedScope = customScope?.clone();
117+
118+
return withScope(customForkedScope, () => {
119119
// If `options.parentSpan` is defined, we want to wrap the callback in `withActiveSpan`
120120
const wrapper = getActiveSpanWrapper<T>(customParentSpan);
121121

@@ -133,20 +133,14 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S
133133
scope,
134134
});
135135

136-
const previousActiveSpanOnCustomScope = customScope && _getSpanForScope(customScope);
137136
_setSpanForScope(scope, activeSpan);
138137

139-
function finishAndSetSpan(): void {
140-
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-
}
146-
}
147-
148138
return handleCallbackErrors(
149-
() => callback(activeSpan, finishAndSetSpan),
139+
// We pass the `finish` function to the callback, so the user can finish the span manually
140+
// this is mainly here for historic purposes because previously, we instructed users to call
141+
// `finish` instead of `span.end()` to also clean up the scope. Nowadays, calling `span.end()`
142+
// or `finish` has the same effect and we simply leave it here to avoid breaking user code.
143+
() => callback(activeSpan, () => activeSpan.end()),
150144
() => {
151145
// Only update the span status if it hasn't been changed yet, and the span is not yet finished
152146
const { status } = spanToJSON(activeSpan);

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

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -780,39 +780,57 @@ describe('startSpanManual', () => {
780780
expect(getActiveSpan()).toBe(undefined);
781781
});
782782

783-
describe('allows to pass a scope', () => {
783+
describe('starts a span on the fork of a custom scope if passed', () => {
784784
it('with parent span', () => {
785785
const initialScope = getCurrentScope();
786786

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

791-
let span1: Span | undefined;
790+
const parentSpan = new SentrySpan({ spanId: 'parent-span-id', sampled: true });
791+
_setSpanForScope(customScope, parentSpan);
792792

793-
startSpanManual({ name: 'GET users/[id]', scope: manualScope }, span => {
794-
span1 = span;
793+
startSpanManual({ name: 'GET users/[id]', scope: customScope }, span => {
794+
// current scope is forked from the customScope
795795
expect(getCurrentScope()).not.toBe(initialScope);
796-
expect(getCurrentScope()).toBe(manualScope);
797-
expect(getActiveSpan()).toBe(span);
796+
expect(getCurrentScope()).not.toBe(customScope);
798797
expect(spanToJSON(span).parent_span_id).toBe('parent-span-id');
799798

799+
// span is active span
800+
expect(getActiveSpan()).toBe(span);
801+
800802
span.end();
801803

802-
// Is still the active span
804+
// span is still the active span (weird but it is what it is)
803805
expect(getActiveSpan()).toBe(span);
806+
807+
getCurrentScope().setTag('cats', 'great');
808+
customScope.setTag('bears', 'great');
809+
810+
expect(getCurrentScope().getScopeData().tags).toEqual({ dogs: 'great', cats: 'great' });
811+
expect(customScope.getScopeData().tags).toEqual({ dogs: 'great', bears: 'great' });
804812
});
805813

806-
startSpanManual({ name: 'POST users/[id]', scope: manualScope }, (span, finish) => {
814+
expect(getCurrentScope()).toBe(initialScope);
815+
expect(getActiveSpan()).toBe(undefined);
816+
817+
startSpanManual({ name: 'POST users/[id]', scope: customScope }, (span, finish) => {
818+
// current scope is forked from the customScope
807819
expect(getCurrentScope()).not.toBe(initialScope);
808-
expect(getCurrentScope()).toBe(manualScope);
820+
expect(getCurrentScope()).not.toBe(customScope);
821+
expect(spanToJSON(span).parent_span_id).toBe('parent-span-id');
822+
823+
// scope data modification from customScope in previous callback is persisted
824+
expect(getCurrentScope().getScopeData().tags).toEqual({ dogs: 'great', bears: 'great' });
825+
826+
// span is active span
809827
expect(getActiveSpan()).toBe(span);
810-
expect(spanToJSON(span).parent_span_id).toBe(span1?.spanContext().spanId);
811828

829+
// calling finish() or span.end() has the same effect
812830
finish();
813831

814832
// using finish() resets the scope correctly
815-
expect(getActiveSpan()).toBe(span1);
833+
expect(getActiveSpan()).toBe(span);
816834
});
817835

818836
expect(getCurrentScope()).toBe(initialScope);
@@ -824,27 +842,33 @@ describe('startSpanManual', () => {
824842
const manualScope = initialScope.clone();
825843

826844
startSpanManual({ name: 'GET users/[id]', scope: manualScope }, (span, finish) => {
845+
// current scope is forked from the customScope
827846
expect(getCurrentScope()).not.toBe(initialScope);
828-
expect(getCurrentScope()).toBe(manualScope);
847+
expect(getCurrentScope()).not.toBe(manualScope);
848+
expect(getCurrentScope()).toEqual(manualScope);
849+
850+
// span is active span and a root span
829851
expect(getActiveSpan()).toBe(span);
830-
expect(spanToJSON(span).parent_span_id).toBe(undefined);
852+
expect(getRootSpan(span)).toBe(span);
831853

832-
finish();
854+
span.end();
833855

834-
// Is still the active span
835-
expect(getActiveSpan()).toBe(undefined);
856+
expect(getActiveSpan()).toBe(span);
836857
});
837858

838-
startSpanManual({ name: 'GET users/[id]', scope: manualScope }, (span, finish) => {
859+
startSpanManual({ name: 'POST users/[id]', scope: manualScope }, (span, finish) => {
839860
expect(getCurrentScope()).not.toBe(initialScope);
840-
expect(getCurrentScope()).toBe(manualScope);
861+
expect(getCurrentScope()).not.toBe(manualScope);
862+
expect(getCurrentScope()).toEqual(manualScope);
863+
864+
// second span is active span and its own root span
841865
expect(getActiveSpan()).toBe(span);
842-
expect(spanToJSON(span).parent_span_id).toBe(undefined);
866+
expect(getRootSpan(span)).toBe(span);
843867

844868
finish();
845869

846-
// using finish() resets the scope correctly
847-
expect(getActiveSpan()).toBe(undefined);
870+
// calling finish() or span.end() has the same effect
871+
expect(getActiveSpan()).toBe(span);
848872
});
849873

850874
expect(getCurrentScope()).toBe(initialScope);

0 commit comments

Comments
 (0)