Skip to content

Commit d1f3d97

Browse files
committed
rework implementation to fork scope instead of restoring previously active span
1 parent a386262 commit d1f3d97

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
@@ -779,39 +779,57 @@ describe('startSpanManual', () => {
779779
expect(getActiveSpan()).toBe(undefined);
780780
});
781781

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

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

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

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

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

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

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

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

813831
// using finish() resets the scope correctly
814-
expect(getActiveSpan()).toBe(span1);
832+
expect(getActiveSpan()).toBe(span);
815833
});
816834

817835
expect(getCurrentScope()).toBe(initialScope);
@@ -823,27 +841,33 @@ describe('startSpanManual', () => {
823841
const manualScope = initialScope.clone();
824842

825843
startSpanManual({ name: 'GET users/[id]', scope: manualScope }, (span, finish) => {
844+
// current scope is forked from the customScope
826845
expect(getCurrentScope()).not.toBe(initialScope);
827-
expect(getCurrentScope()).toBe(manualScope);
846+
expect(getCurrentScope()).not.toBe(manualScope);
847+
expect(getCurrentScope()).toEqual(manualScope);
848+
849+
// span is active span and a root span
828850
expect(getActiveSpan()).toBe(span);
829-
expect(spanToJSON(span).parent_span_id).toBe(undefined);
851+
expect(getRootSpan(span)).toBe(span);
830852

831-
finish();
853+
span.end();
832854

833-
// Is still the active span
834-
expect(getActiveSpan()).toBe(undefined);
855+
expect(getActiveSpan()).toBe(span);
835856
});
836857

837-
startSpanManual({ name: 'GET users/[id]', scope: manualScope }, (span, finish) => {
858+
startSpanManual({ name: 'POST users/[id]', scope: manualScope }, (span, finish) => {
838859
expect(getCurrentScope()).not.toBe(initialScope);
839-
expect(getCurrentScope()).toBe(manualScope);
860+
expect(getCurrentScope()).not.toBe(manualScope);
861+
expect(getCurrentScope()).toEqual(manualScope);
862+
863+
// second span is active span and its own root span
840864
expect(getActiveSpan()).toBe(span);
841-
expect(spanToJSON(span).parent_span_id).toBe(undefined);
865+
expect(getRootSpan(span)).toBe(span);
842866

843867
finish();
844868

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

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

0 commit comments

Comments
 (0)