Skip to content

Commit a696aef

Browse files
mydealforst
andauthored
feat(core): Deprecate trace in favor of startSpan (#10012)
Also add a `handleCallbackErrors` utility to replace this. We've been using this in a few places, and since it has a bit of a different usage than `startSpan` I had to add a new utility to properly make this work. The upside is that we now can ensure to use the same implementation for this "maybe promise" handling everywhere! --------- Co-authored-by: Luca Forstner <[email protected]>
1 parent 3acb7e4 commit a696aef

File tree

29 files changed

+418
-298
lines changed

29 files changed

+418
-298
lines changed

packages/astro/src/index.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export {
4141
setTags,
4242
setUser,
4343
spanStatusfromHttpCode,
44+
// eslint-disable-next-line deprecation/deprecation
4445
trace,
4546
withScope,
4647
autoDiscoverNodePerformanceMonitoringIntegrations,

packages/browser/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export {
4848
extractTraceparentData,
4949
getActiveTransaction,
5050
spanStatusfromHttpCode,
51+
// eslint-disable-next-line deprecation/deprecation
5152
trace,
5253
makeMultiplexedTransport,
5354
ModuleMetadata,

packages/bun/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export {
6060
setTags,
6161
setUser,
6262
spanStatusfromHttpCode,
63+
// eslint-disable-next-line deprecation/deprecation
6364
trace,
6465
withScope,
6566
captureCheckIn,

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export { prepareEvent } from './utils/prepareEvent';
6969
export { createCheckInEnvelope } from './checkin';
7070
export { hasTracingEnabled } from './utils/hasTracingEnabled';
7171
export { isSentryRequestUrl } from './utils/isSentryRequestUrl';
72+
export { handleCallbackErrors } from './utils/handleCallbackErrors';
7273
export { spanToTraceHeader } from './utils/spanUtils';
7374
export { DEFAULT_ENVIRONMENT } from './constants';
7475
export { ModuleMetadata } from './integrations/metadata';

packages/core/src/tracing/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export { extractTraceparentData, getActiveTransaction } from './utils';
99
export { SpanStatus } from './spanstatus';
1010
export type { SpanStatusType } from './span';
1111
export {
12+
// eslint-disable-next-line deprecation/deprecation
1213
trace,
1314
getActiveSpan,
1415
startSpan,

packages/core/src/tracing/trace.ts

Lines changed: 35 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { Span, TransactionContext } from '@sentry/types';
2-
import { dropUndefinedKeys, isThenable, logger, tracingContextFromHeaders } from '@sentry/utils';
2+
import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils';
33

44
import { DEBUG_BUILD } from '../debug-build';
55
import { getCurrentScope, withScope } from '../exports';
66
import type { Hub } from '../hub';
77
import { getCurrentHub } from '../hub';
8+
import { handleCallbackErrors } from '../utils/handleCallbackErrors';
89
import { hasTracingEnabled } from '../utils/hasTracingEnabled';
910

1011
/**
@@ -18,6 +19,8 @@ import { hasTracingEnabled } from '../utils/hasTracingEnabled';
1819
*
1920
* @internal
2021
* @private
22+
*
23+
* @deprecated Use `startSpan` instead.
2124
*/
2225
export function trace<T>(
2326
context: TransactionContext,
@@ -37,43 +40,18 @@ export function trace<T>(
3740

3841
scope.setSpan(activeSpan);
3942

40-
function finishAndSetSpan(): void {
41-
activeSpan && activeSpan.end();
42-
scope.setSpan(parentSpan);
43-
}
44-
45-
let maybePromiseResult: T;
46-
try {
47-
maybePromiseResult = callback(activeSpan);
48-
} catch (e) {
49-
activeSpan && activeSpan.setStatus('internal_error');
50-
onError(e, activeSpan);
51-
finishAndSetSpan();
52-
afterFinish();
53-
throw e;
54-
}
55-
56-
if (isThenable(maybePromiseResult)) {
57-
// @ts-expect-error - the isThenable check returns the "wrong" type here
58-
return maybePromiseResult.then(
59-
res => {
60-
finishAndSetSpan();
61-
afterFinish();
62-
return res;
63-
},
64-
e => {
65-
activeSpan && activeSpan.setStatus('internal_error');
66-
onError(e, activeSpan);
67-
finishAndSetSpan();
68-
afterFinish();
69-
throw e;
70-
},
71-
);
72-
}
73-
74-
finishAndSetSpan();
75-
afterFinish();
76-
return maybePromiseResult;
43+
return handleCallbackErrors(
44+
() => callback(activeSpan),
45+
error => {
46+
activeSpan && activeSpan.setStatus('internal_error');
47+
onError(error, activeSpan);
48+
},
49+
() => {
50+
activeSpan && activeSpan.end();
51+
scope.setSpan(parentSpan);
52+
afterFinish();
53+
},
54+
);
7755
}
7856

7957
/**
@@ -90,43 +68,23 @@ export function trace<T>(
9068
export function startSpan<T>(context: TransactionContext, callback: (span: Span | undefined) => T): T {
9169
const ctx = normalizeContext(context);
9270

93-
// @ts-expect-error - isThenable returns the wrong type
9471
return withScope(scope => {
9572
const hub = getCurrentHub();
9673
const parentSpan = scope.getSpan();
9774

9875
const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx);
9976
scope.setSpan(activeSpan);
10077

101-
function finishAndSetSpan(): void {
102-
activeSpan && activeSpan.end();
103-
}
104-
105-
let maybePromiseResult: T;
106-
try {
107-
maybePromiseResult = callback(activeSpan);
108-
} catch (e) {
109-
activeSpan && activeSpan.setStatus('internal_error');
110-
finishAndSetSpan();
111-
throw e;
112-
}
113-
114-
if (isThenable(maybePromiseResult)) {
115-
return maybePromiseResult.then(
116-
res => {
117-
finishAndSetSpan();
118-
return res;
119-
},
120-
e => {
121-
activeSpan && activeSpan.setStatus('internal_error');
122-
finishAndSetSpan();
123-
throw e;
124-
},
125-
);
126-
}
127-
128-
finishAndSetSpan();
129-
return maybePromiseResult;
78+
return handleCallbackErrors(
79+
() => callback(activeSpan),
80+
() => {
81+
// Only update the span status if it hasn't been changed yet
82+
if (activeSpan && (!activeSpan.status || activeSpan.status === 'ok')) {
83+
activeSpan.setStatus('internal_error');
84+
}
85+
},
86+
() => activeSpan && activeSpan.end(),
87+
);
13088
});
13189
}
13290

@@ -152,7 +110,6 @@ export function startSpanManual<T>(
152110
): T {
153111
const ctx = normalizeContext(context);
154112

155-
// @ts-expect-error - isThenable returns the wrong type
156113
return withScope(scope => {
157114
const hub = getCurrentHub();
158115
const parentSpan = scope.getSpan();
@@ -164,25 +121,15 @@ export function startSpanManual<T>(
164121
activeSpan && activeSpan.end();
165122
}
166123

167-
let maybePromiseResult: T;
168-
try {
169-
maybePromiseResult = callback(activeSpan, finishAndSetSpan);
170-
} catch (e) {
171-
activeSpan && activeSpan.setStatus('internal_error');
172-
throw e;
173-
}
174-
175-
if (isThenable(maybePromiseResult)) {
176-
return maybePromiseResult.then(
177-
res => res,
178-
e => {
179-
activeSpan && activeSpan.setStatus('internal_error');
180-
throw e;
181-
},
182-
);
183-
}
184-
185-
return maybePromiseResult;
124+
return handleCallbackErrors(
125+
() => callback(activeSpan, finishAndSetSpan),
126+
() => {
127+
// Only update the span status if it hasn't been changed yet, and the span is not yet finished
128+
if (activeSpan && !activeSpan.endTimestamp && (!activeSpan.status || activeSpan.status === 'ok')) {
129+
activeSpan.setStatus('internal_error');
130+
}
131+
},
132+
);
186133
});
187134
}
188135

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { isThenable } from '@sentry/utils';
2+
3+
/**
4+
* Wrap a callback function with error handling.
5+
* If an error is thrown, it will be passed to the `onError` callback and re-thrown.
6+
*
7+
* If the return value of the function is a promise, it will be handled with `maybeHandlePromiseRejection`.
8+
*
9+
* If an `onFinally` callback is provided, this will be called when the callback has finished
10+
* - so if it returns a promise, once the promise resolved/rejected,
11+
* else once the callback has finished executing.
12+
* The `onFinally` callback will _always_ be called, no matter if an error was thrown or not.
13+
*/
14+
export function handleCallbackErrors<
15+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
16+
Fn extends () => any,
17+
>(
18+
fn: Fn,
19+
onError: (error: unknown) => void,
20+
// eslint-disable-next-line @typescript-eslint/no-empty-function
21+
onFinally: () => void = () => {},
22+
): ReturnType<Fn> {
23+
let maybePromiseResult: ReturnType<Fn>;
24+
try {
25+
maybePromiseResult = fn();
26+
} catch (e) {
27+
onError(e);
28+
onFinally();
29+
throw e;
30+
}
31+
32+
return maybeHandlePromiseRejection(maybePromiseResult, onError, onFinally);
33+
}
34+
35+
/**
36+
* Maybe handle a promise rejection.
37+
* This expects to be given a value that _may_ be a promise, or any other value.
38+
* If it is a promise, and it rejects, it will call the `onError` callback.
39+
* Other than this, it will generally return the given value as-is.
40+
*/
41+
function maybeHandlePromiseRejection<MaybePromise>(
42+
value: MaybePromise,
43+
onError: (error: unknown) => void,
44+
onFinally: () => void,
45+
): MaybePromise {
46+
if (isThenable(value)) {
47+
// @ts-expect-error - the isThenable check returns the "wrong" type here
48+
return value.then(
49+
res => {
50+
onFinally();
51+
return res;
52+
},
53+
e => {
54+
onError(e);
55+
onFinally();
56+
throw e;
57+
},
58+
);
59+
}
60+
61+
onFinally();
62+
return value;
63+
}

0 commit comments

Comments
 (0)