Skip to content

Commit 559d3bf

Browse files
authored
fix(core): Fork scope if custom scope is passed to startSpanManual (#14901)
Follow-up on #14900 for `startSpanManual`
1 parent b831f36 commit 559d3bf

File tree

2 files changed

+98
-23
lines changed

2 files changed

+98
-23
lines changed

packages/core/src/tracing/trace.ts

+10-8
Original file line numberDiff line numberDiff line change
@@ -96,7 +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. You'll have to call `span.end()` manually.
99+
* after the function is done automatically. Use `span.end()` to end the span.
100100
*
101101
* The created span is the active span and will be used as parent by other spans created inside the function
102102
* and can be accessed via `Sentry.getActiveSpan()`, as long as the function is executed while the scope is active.
@@ -111,9 +111,11 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S
111111
}
112112

113113
const spanArguments = parseSentrySpanArguments(options);
114-
const { forceTransaction, parentSpan: customParentSpan } = options;
114+
const { forceTransaction, parentSpan: customParentSpan, scope: customScope } = options;
115+
116+
const customForkedScope = customScope?.clone();
115117

116-
return withScope(options.scope, () => {
118+
return withScope(customForkedScope, () => {
117119
// If `options.parentSpan` is defined, we want to wrap the callback in `withActiveSpan`
118120
const wrapper = getActiveSpanWrapper<T>(customParentSpan);
119121

@@ -133,12 +135,12 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S
133135

134136
_setSpanForScope(scope, activeSpan);
135137

136-
function finishAndSetSpan(): void {
137-
activeSpan.end();
138-
}
139-
140138
return handleCallbackErrors(
141-
() => 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()),
142144
() => {
143145
// Only update the span status if it hasn't been changed yet, and the span is not yet finished
144146
const { status } = spanToJSON(activeSpan);

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

+88-15
Original file line numberDiff line numberDiff line change
@@ -782,27 +782,100 @@ describe('startSpanManual', () => {
782782
expect(getActiveSpan()).toBe(undefined);
783783
});
784784

785-
it('allows to pass a scope', () => {
786-
const initialScope = getCurrentScope();
785+
describe('starts a span on the fork of a custom scope if passed', () => {
786+
it('with parent span', () => {
787+
const initialScope = getCurrentScope();
787788

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

792-
startSpanManual({ name: 'GET users/[id]', scope: manualScope }, span => {
793-
expect(getCurrentScope()).not.toBe(initialScope);
794-
expect(getCurrentScope()).toBe(manualScope);
795-
expect(getActiveSpan()).toBe(span);
796-
expect(spanToJSON(span).parent_span_id).toBe('parent-span-id');
792+
const parentSpan = new SentrySpan({ spanId: 'parent-span-id', sampled: true });
793+
_setSpanForScope(customScope, parentSpan);
797794

798-
span.end();
795+
startSpanManual({ name: 'GET users/[id]', scope: customScope }, span => {
796+
// current scope is forked from the customScope
797+
expect(getCurrentScope()).not.toBe(initialScope);
798+
expect(getCurrentScope()).not.toBe(customScope);
799+
expect(spanToJSON(span).parent_span_id).toBe('parent-span-id');
799800

800-
// Is still the active span
801-
expect(getActiveSpan()).toBe(span);
801+
// span is active span
802+
expect(getActiveSpan()).toBe(span);
803+
804+
span.end();
805+
806+
// span is still the active span (weird but it is what it is)
807+
expect(getActiveSpan()).toBe(span);
808+
809+
getCurrentScope().setTag('cats', 'great');
810+
customScope.setTag('bears', 'great');
811+
812+
expect(getCurrentScope().getScopeData().tags).toEqual({ dogs: 'great', cats: 'great' });
813+
expect(customScope.getScopeData().tags).toEqual({ dogs: 'great', bears: 'great' });
814+
});
815+
816+
expect(getCurrentScope()).toBe(initialScope);
817+
expect(getActiveSpan()).toBe(undefined);
818+
819+
startSpanManual({ name: 'POST users/[id]', scope: customScope }, (span, finish) => {
820+
// current scope is forked from the customScope
821+
expect(getCurrentScope()).not.toBe(initialScope);
822+
expect(getCurrentScope()).not.toBe(customScope);
823+
expect(spanToJSON(span).parent_span_id).toBe('parent-span-id');
824+
825+
// scope data modification from customScope in previous callback is persisted
826+
expect(getCurrentScope().getScopeData().tags).toEqual({ dogs: 'great', bears: 'great' });
827+
828+
// span is active span
829+
expect(getActiveSpan()).toBe(span);
830+
831+
// calling finish() or span.end() has the same effect
832+
finish();
833+
834+
// using finish() resets the scope correctly
835+
expect(getActiveSpan()).toBe(span);
836+
});
837+
838+
expect(getCurrentScope()).toBe(initialScope);
839+
expect(getActiveSpan()).toBe(undefined);
802840
});
803841

804-
expect(getCurrentScope()).toBe(initialScope);
805-
expect(getActiveSpan()).toBe(undefined);
842+
it('without parent span', () => {
843+
const initialScope = getCurrentScope();
844+
const manualScope = initialScope.clone();
845+
846+
startSpanManual({ name: 'GET users/[id]', scope: manualScope }, span => {
847+
// current scope is forked from the customScope
848+
expect(getCurrentScope()).not.toBe(initialScope);
849+
expect(getCurrentScope()).not.toBe(manualScope);
850+
expect(getCurrentScope()).toEqual(manualScope);
851+
852+
// span is active span and a root span
853+
expect(getActiveSpan()).toBe(span);
854+
expect(getRootSpan(span)).toBe(span);
855+
856+
span.end();
857+
858+
expect(getActiveSpan()).toBe(span);
859+
});
860+
861+
startSpanManual({ name: 'POST users/[id]', scope: manualScope }, (span, finish) => {
862+
expect(getCurrentScope()).not.toBe(initialScope);
863+
expect(getCurrentScope()).not.toBe(manualScope);
864+
expect(getCurrentScope()).toEqual(manualScope);
865+
866+
// second span is active span and its own root span
867+
expect(getActiveSpan()).toBe(span);
868+
expect(getRootSpan(span)).toBe(span);
869+
870+
finish();
871+
872+
// calling finish() or span.end() has the same effect
873+
expect(getActiveSpan()).toBe(span);
874+
});
875+
876+
expect(getCurrentScope()).toBe(initialScope);
877+
expect(getActiveSpan()).toBe(undefined);
878+
});
806879
});
807880

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

0 commit comments

Comments
 (0)