Skip to content

Commit b6a7cef

Browse files
authored
feat(core): Ensure startSpan & startSpanManual fork scope (#9955)
@lforst noticed that currently `startSpan()` and `startSpanManual` do not fork the scope, which is also a slightly different behavior than it is in OTEL. This adjusts this to better align, so that we always fork a scope. We also always leave the span on the forked scope, even after it was finished. In a follow up, we can thus also get rid of the `finish` callback arg for `startSpanManual`, as users can/should simply call `span.end()` themselves then.
1 parent 1413568 commit b6a7cef

File tree

2 files changed

+137
-58
lines changed

2 files changed

+137
-58
lines changed

packages/core/src/tracing/trace.ts

Lines changed: 56 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { TransactionContext } from '@sentry/types';
22
import { dropUndefinedKeys, isThenable, logger, tracingContextFromHeaders } from '@sentry/utils';
33

44
import { DEBUG_BUILD } from '../debug-build';
5-
import { getCurrentScope } from '../exports';
5+
import { getCurrentScope, withScope } from '../exports';
66
import type { Hub } from '../hub';
77
import { getCurrentHub } from '../hub';
88
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
@@ -89,42 +89,42 @@ export function trace<T>(
8989
export function startSpan<T>(context: TransactionContext, callback: (span: Span | undefined) => T): T {
9090
const ctx = normalizeContext(context);
9191

92-
const hub = getCurrentHub();
93-
const scope = getCurrentScope();
94-
const parentSpan = scope.getSpan();
92+
return withScope(scope => {
93+
const hub = getCurrentHub();
94+
const parentSpan = scope.getSpan();
9595

96-
const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx);
97-
scope.setSpan(activeSpan);
98-
99-
function finishAndSetSpan(): void {
100-
activeSpan && activeSpan.end();
101-
scope.setSpan(parentSpan);
102-
}
96+
const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx);
97+
scope.setSpan(activeSpan);
10398

104-
let maybePromiseResult: T;
105-
try {
106-
maybePromiseResult = callback(activeSpan);
107-
} catch (e) {
108-
activeSpan && activeSpan.setStatus('internal_error');
109-
finishAndSetSpan();
110-
throw e;
111-
}
99+
function finishAndSetSpan(): void {
100+
activeSpan && activeSpan.end();
101+
}
112102

113-
if (isThenable(maybePromiseResult)) {
114-
Promise.resolve(maybePromiseResult).then(
115-
() => {
116-
finishAndSetSpan();
117-
},
118-
() => {
119-
activeSpan && activeSpan.setStatus('internal_error');
120-
finishAndSetSpan();
121-
},
122-
);
123-
} else {
124-
finishAndSetSpan();
125-
}
126-
127-
return maybePromiseResult;
103+
let maybePromiseResult: T;
104+
try {
105+
maybePromiseResult = callback(activeSpan);
106+
} catch (e) {
107+
activeSpan && activeSpan.setStatus('internal_error');
108+
finishAndSetSpan();
109+
throw e;
110+
}
111+
112+
if (isThenable(maybePromiseResult)) {
113+
Promise.resolve(maybePromiseResult).then(
114+
() => {
115+
finishAndSetSpan();
116+
},
117+
() => {
118+
activeSpan && activeSpan.setStatus('internal_error');
119+
finishAndSetSpan();
120+
},
121+
);
122+
} else {
123+
finishAndSetSpan();
124+
}
125+
126+
return maybePromiseResult;
127+
});
128128
}
129129

130130
/**
@@ -149,33 +149,33 @@ export function startSpanManual<T>(
149149
): T {
150150
const ctx = normalizeContext(context);
151151

152-
const hub = getCurrentHub();
153-
const scope = getCurrentScope();
154-
const parentSpan = scope.getSpan();
152+
return withScope(scope => {
153+
const hub = getCurrentHub();
154+
const parentSpan = scope.getSpan();
155155

156-
const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx);
157-
scope.setSpan(activeSpan);
156+
const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx);
157+
scope.setSpan(activeSpan);
158158

159-
function finishAndSetSpan(): void {
160-
activeSpan && activeSpan.end();
161-
scope.setSpan(parentSpan);
162-
}
159+
function finishAndSetSpan(): void {
160+
activeSpan && activeSpan.end();
161+
}
163162

164-
let maybePromiseResult: T;
165-
try {
166-
maybePromiseResult = callback(activeSpan, finishAndSetSpan);
167-
} catch (e) {
168-
activeSpan && activeSpan.setStatus('internal_error');
169-
throw e;
170-
}
171-
172-
if (isThenable(maybePromiseResult)) {
173-
Promise.resolve(maybePromiseResult).then(undefined, () => {
163+
let maybePromiseResult: T;
164+
try {
165+
maybePromiseResult = callback(activeSpan, finishAndSetSpan);
166+
} catch (e) {
174167
activeSpan && activeSpan.setStatus('internal_error');
175-
});
176-
}
168+
throw e;
169+
}
177170

178-
return maybePromiseResult;
171+
if (isThenable(maybePromiseResult)) {
172+
Promise.resolve(maybePromiseResult).then(undefined, () => {
173+
activeSpan && activeSpan.setStatus('internal_error');
174+
});
175+
}
176+
177+
return maybePromiseResult;
178+
});
179179
}
180180

181181
/**

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

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { Hub, addTracingExtensions, makeMain } from '../../../src';
2-
import { continueTrace, startSpan } from '../../../src/tracing';
1+
import type { Span } from '@sentry/types';
2+
import { Hub, addTracingExtensions, getCurrentScope, makeMain } from '../../../src';
3+
import { continueTrace, startInactiveSpan, startSpan, startSpanManual } from '../../../src/tracing';
34
import { TestClient, getDefaultTestClientOptions } from '../../mocks/client';
45

56
beforeAll(() => {
@@ -80,6 +81,18 @@ describe('startSpan', () => {
8081
expect(ref.status).toEqual(isError ? 'internal_error' : undefined);
8182
});
8283

84+
it('creates & finishes span', async () => {
85+
let _span: Span | undefined;
86+
startSpan({ name: 'GET users/[id]' }, span => {
87+
expect(span).toBeDefined();
88+
expect(span?.endTimestamp).toBeUndefined();
89+
_span = span;
90+
});
91+
92+
expect(_span).toBeDefined();
93+
expect(_span?.endTimestamp).toBeDefined();
94+
});
95+
8396
it('allows traceparent information to be overriden', async () => {
8497
let ref: any = undefined;
8598
client.on('finishTransaction', transaction => {
@@ -168,6 +181,72 @@ describe('startSpan', () => {
168181
expect(ref.spanRecorder.spans).toHaveLength(2);
169182
expect(ref.spanRecorder.spans[1].op).toEqual('db.query');
170183
});
184+
185+
it('forks the scope', () => {
186+
const initialScope = getCurrentScope();
187+
188+
startSpan({ name: 'GET users/[id]' }, span => {
189+
expect(getCurrentScope()).not.toBe(initialScope);
190+
expect(getCurrentScope().getSpan()).toBe(span);
191+
});
192+
193+
expect(getCurrentScope()).toBe(initialScope);
194+
expect(initialScope.getSpan()).toBe(undefined);
195+
});
196+
});
197+
});
198+
199+
describe('startSpanManual', () => {
200+
it('creates & finishes span', async () => {
201+
startSpanManual({ name: 'GET users/[id]' }, (span, finish) => {
202+
expect(span).toBeDefined();
203+
expect(span?.endTimestamp).toBeUndefined();
204+
finish();
205+
expect(span?.endTimestamp).toBeDefined();
206+
});
207+
});
208+
209+
it('forks the scope automatically', () => {
210+
const initialScope = getCurrentScope();
211+
212+
startSpanManual({ name: 'GET users/[id]' }, (span, finish) => {
213+
expect(getCurrentScope()).not.toBe(initialScope);
214+
expect(getCurrentScope().getSpan()).toBe(span);
215+
216+
finish();
217+
218+
// Is still the active span
219+
expect(getCurrentScope().getSpan()).toBe(span);
220+
});
221+
222+
expect(getCurrentScope()).toBe(initialScope);
223+
expect(initialScope.getSpan()).toBe(undefined);
224+
});
225+
});
226+
227+
describe('startInactiveSpan', () => {
228+
it('creates & finishes span', async () => {
229+
const span = startInactiveSpan({ name: 'GET users/[id]' });
230+
231+
expect(span).toBeDefined();
232+
expect(span?.endTimestamp).toBeUndefined();
233+
234+
span?.end();
235+
236+
expect(span?.endTimestamp).toBeDefined();
237+
});
238+
239+
it('does not set span on scope', () => {
240+
const initialScope = getCurrentScope();
241+
242+
const span = startInactiveSpan({ name: 'GET users/[id]' });
243+
244+
expect(span).toBeDefined();
245+
expect(initialScope.getSpan()).toBeUndefined();
246+
247+
span?.end();
248+
249+
expect(initialScope.getSpan()).toBeUndefined();
171250
});
172251
});
173252

0 commit comments

Comments
 (0)