Skip to content

Commit 3db988f

Browse files
authored
feat(core): Introduce startSpanManual (#8913)
Based on #8911 and convos in slack, it was brought up that we might need to expose a method that works similar to `startSpan`, but that does not automatically finish the span at the end of the callback. This is necessary when you have event emitters (`res.once`) or similar. ```ts Sentry.startSpanManual(ctx, (span, finish) => { // do something with span // when you're done, call finish() finish(); }); ```
1 parent 595e4e2 commit 3db988f

File tree

6 files changed

+77
-28
lines changed

6 files changed

+77
-28
lines changed

packages/browser/src/exports.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export {
4040
getActiveSpan,
4141
startSpan,
4242
startInactiveSpan,
43+
startSpanManual,
4344
SDK_VERSION,
4445
setContext,
4546
setExtra,

packages/core/src/tracing/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ export { extractTraceparentData, getActiveTransaction } from './utils';
77
export { SpanStatus } from './spanstatus';
88
export type { SpanStatusType } from './span';
99
// eslint-disable-next-line deprecation/deprecation
10-
export { trace, getActiveSpan, startSpan, startInactiveSpan, startActiveSpan } from './trace';
10+
export { trace, getActiveSpan, startSpan, startInactiveSpan, startActiveSpan, startSpanManual } from './trace';
1111
export { getDynamicSamplingContextFromClient } from './dynamicSamplingContext';
1212
export { setMeasurement } from './measurement';

packages/core/src/tracing/trace.ts

Lines changed: 72 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { TransactionContext } from '@sentry/types';
22
import { isThenable } from '@sentry/utils';
33

4+
import type { Hub } from '../hub';
45
import { getCurrentHub } from '../hub';
56
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
67
import type { Span } from './span';
@@ -23,25 +24,14 @@ export function trace<T>(
2324
// eslint-disable-next-line @typescript-eslint/no-empty-function
2425
onError: (error: unknown) => void = () => {},
2526
): T {
26-
const ctx = { ...context };
27-
// If a name is set and a description is not, set the description to the name.
28-
if (ctx.name !== undefined && ctx.description === undefined) {
29-
ctx.description = ctx.name;
30-
}
27+
const ctx = normalizeContext(context);
3128

3229
const hub = getCurrentHub();
3330
const scope = hub.getScope();
34-
3531
const parentSpan = scope.getSpan();
3632

37-
function createChildSpanOrTransaction(): Span | undefined {
38-
if (!hasTracingEnabled()) {
39-
return undefined;
40-
}
41-
return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx);
42-
}
33+
const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx);
4334

44-
const activeSpan = createChildSpanOrTransaction();
4535
scope.setSpan(activeSpan);
4636

4737
function finishAndSetSpan(): void {
@@ -89,25 +79,13 @@ export function trace<T>(
8979
* and the `span` returned from the callback will be undefined.
9080
*/
9181
export function startSpan<T>(context: TransactionContext, callback: (span: Span | undefined) => T): T {
92-
const ctx = { ...context };
93-
// If a name is set and a description is not, set the description to the name.
94-
if (ctx.name !== undefined && ctx.description === undefined) {
95-
ctx.description = ctx.name;
96-
}
82+
const ctx = normalizeContext(context);
9783

9884
const hub = getCurrentHub();
9985
const scope = hub.getScope();
100-
10186
const parentSpan = scope.getSpan();
10287

103-
function createChildSpanOrTransaction(): Span | undefined {
104-
if (!hasTracingEnabled()) {
105-
return undefined;
106-
}
107-
return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx);
108-
}
109-
110-
const activeSpan = createChildSpanOrTransaction();
88+
const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx);
11189
scope.setSpan(activeSpan);
11290

11391
function finishAndSetSpan(): void {
@@ -146,6 +124,52 @@ export function startSpan<T>(context: TransactionContext, callback: (span: Span
146124
*/
147125
export const startActiveSpan = startSpan;
148126

127+
/**
128+
* Similar to `Sentry.startSpan`. Wraps a function with a transaction/span, but does not finish the span
129+
* after the function is done automatically.
130+
*
131+
* The created span is the active span and will be used as parent by other spans created inside the function
132+
* and can be accessed via `Sentry.getActiveSpan()`, as long as the function is executed while the scope is active.
133+
*
134+
* Note that if you have not enabled tracing extensions via `addTracingExtensions`
135+
* or you didn't set `tracesSampleRate`, this function will not generate spans
136+
* and the `span` returned from the callback will be undefined.
137+
*/
138+
export function startSpanManual<T>(
139+
context: TransactionContext,
140+
callback: (span: Span | undefined, finish: () => void) => T,
141+
): T {
142+
const ctx = normalizeContext(context);
143+
144+
const hub = getCurrentHub();
145+
const scope = hub.getScope();
146+
const parentSpan = scope.getSpan();
147+
148+
const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx);
149+
scope.setSpan(activeSpan);
150+
151+
function finishAndSetSpan(): void {
152+
activeSpan && activeSpan.finish();
153+
hub.getScope().setSpan(parentSpan);
154+
}
155+
156+
let maybePromiseResult: T;
157+
try {
158+
maybePromiseResult = callback(activeSpan, finishAndSetSpan);
159+
} catch (e) {
160+
activeSpan && activeSpan.setStatus('internal_error');
161+
throw e;
162+
}
163+
164+
if (isThenable(maybePromiseResult)) {
165+
Promise.resolve(maybePromiseResult).then(undefined, () => {
166+
activeSpan && activeSpan.setStatus('internal_error');
167+
});
168+
}
169+
170+
return maybePromiseResult;
171+
}
172+
149173
/**
150174
* Creates a span. This span is not set as active, so will not get automatic instrumentation spans
151175
* as children or be able to be accessed via `Sentry.getSpan()`.
@@ -178,3 +202,24 @@ export function startInactiveSpan(context: TransactionContext): Span | undefined
178202
export function getActiveSpan(): Span | undefined {
179203
return getCurrentHub().getScope().getSpan();
180204
}
205+
206+
function createChildSpanOrTransaction(
207+
hub: Hub,
208+
parentSpan: Span | undefined,
209+
ctx: TransactionContext,
210+
): Span | undefined {
211+
if (!hasTracingEnabled()) {
212+
return undefined;
213+
}
214+
return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx);
215+
}
216+
217+
function normalizeContext(context: TransactionContext): TransactionContext {
218+
const ctx = { ...context };
219+
// If a name is set and a description is not, set the description to the name.
220+
if (ctx.name !== undefined && ctx.description === undefined) {
221+
ctx.description = ctx.name;
222+
}
223+
224+
return ctx;
225+
}

packages/node/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export {
6060
// eslint-disable-next-line deprecation/deprecation
6161
startActiveSpan,
6262
startInactiveSpan,
63+
startSpanManual,
6364
} from '@sentry/core';
6465
export type { SpanStatusType } from '@sentry/core';
6566
export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing';

packages/serverless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,5 @@ export {
5555
// eslint-disable-next-line deprecation/deprecation
5656
startActiveSpan,
5757
startInactiveSpan,
58+
startSpanManual,
5859
} from '@sentry/node';

packages/sveltekit/src/server/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export {
5050
// eslint-disable-next-line deprecation/deprecation
5151
startActiveSpan,
5252
startInactiveSpan,
53+
startSpanManual,
5354
} from '@sentry/node';
5455

5556
// We can still leave this for the carrier init and type exports

0 commit comments

Comments
 (0)