Skip to content

Commit 0e61da1

Browse files
committed
feat(core): Allow to pass onSuccess to handleCallbackErrors
I've seen a few places where we are wrapping things that could be sync or async, and we want to do something with the return value. By adding an onSuccess handler to `handelCallbackErrors` we can handle this more generically in the future. revert and fix vendor type in better awaited??
1 parent 3410a08 commit 0e61da1

File tree

4 files changed

+152
-36
lines changed

4 files changed

+152
-36
lines changed

packages/core/src/utils/anthropic-ai/index.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
GEN_AI_SYSTEM_ATTRIBUTE,
2525
} from '../ai/gen-ai-attributes';
2626
import { buildMethodPath, getFinalOperationName, getSpanOperation, setTokenUsageAttributes } from '../ai/utils';
27+
import { handleCallbackErrors } from '../handleCallbackErrors';
2728
import { ANTHROPIC_AI_INTEGRATION_NAME } from './constants';
2829
import { instrumentStream } from './streaming';
2930
import type {
@@ -238,7 +239,7 @@ function instrumentMethod<T extends unknown[], R>(
238239
op: getSpanOperation(methodPath),
239240
attributes: requestAttributes as Record<string, SpanAttributeValue>,
240241
},
241-
async (span: Span) => {
242+
async span => {
242243
try {
243244
if (finalOptions.recordInputs && params) {
244245
addPrivateRequestAttributes(span, params);
@@ -274,27 +275,27 @@ function instrumentMethod<T extends unknown[], R>(
274275
op: getSpanOperation(methodPath),
275276
attributes: requestAttributes as Record<string, SpanAttributeValue>,
276277
},
277-
async (span: Span) => {
278-
try {
279-
if (finalOptions.recordInputs && args[0] && typeof args[0] === 'object') {
280-
addPrivateRequestAttributes(span, args[0] as Record<string, unknown>);
281-
}
278+
span => {
279+
if (finalOptions.recordInputs && params) {
280+
addPrivateRequestAttributes(span, params);
281+
}
282282

283-
const result = await originalMethod.apply(context, args);
284-
addResponseAttributes(span, result, finalOptions.recordOutputs);
285-
return result;
286-
} catch (error) {
287-
captureException(error, {
288-
mechanism: {
289-
handled: false,
290-
type: 'auto.ai.anthropic',
291-
data: {
292-
function: methodPath,
283+
return handleCallbackErrors(
284+
() => originalMethod.apply(context, args),
285+
error => {
286+
captureException(error, {
287+
mechanism: {
288+
handled: false,
289+
type: 'auto.ai.anthropic',
290+
data: {
291+
function: methodPath,
292+
},
293293
},
294-
},
295-
});
296-
throw error;
297-
}
294+
});
295+
},
296+
() => {},
297+
result => addResponseAttributes(span, result as AnthropicAiResponse, finalOptions.recordOutputs),
298+
);
298299
},
299300
);
300301
};

packages/core/src/utils/handleCallbackErrors.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
import { isThenable } from '../utils/is';
22

3+
/* eslint-disable */
4+
// Vendor "Awaited" in to be TS 3.8 compatible
5+
type AwaitedPromise<T> = T extends null | undefined
6+
? T // special case for `null | undefined` when not in `--strictNullChecks` mode
7+
: T extends object & { then(onfulfilled: infer F, ...args: infer _): any } // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
8+
? F extends (value: infer V, ...args: infer _) => any // if the argument to `then` is callable, extracts the first argument
9+
? V // normally this would recursively unwrap, but this is not possible in TS3.8
10+
: never // the argument to `then` was not callable
11+
: T; // non-object or non-thenable
12+
/* eslint-enable */
13+
14+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
15+
export function handleCallbackErrors<Fn extends () => Promise<any>, PromiseValue = AwaitedPromise<ReturnType<Fn>>>(
16+
fn: Fn,
17+
onError: (error: unknown) => void,
18+
onFinally?: () => void,
19+
onSuccess?: (result: PromiseValue) => void,
20+
): ReturnType<Fn>;
21+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
22+
export function handleCallbackErrors<Fn extends () => any>(
23+
fn: Fn,
24+
onError: (error: unknown) => void,
25+
onFinally?: () => void,
26+
onSuccess?: (result: ReturnType<Fn>) => void,
27+
): ReturnType<Fn>;
328
/**
429
* Wrap a callback function with error handling.
530
* If an error is thrown, it will be passed to the `onError` callback and re-thrown.
@@ -14,7 +39,13 @@ import { isThenable } from '../utils/is';
1439
export function handleCallbackErrors<
1540
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1641
Fn extends () => any,
17-
>(fn: Fn, onError: (error: unknown) => void, onFinally: () => void = () => {}): ReturnType<Fn> {
42+
ValueType = ReturnType<Fn>,
43+
>(
44+
fn: Fn,
45+
onError: (error: unknown) => void,
46+
onFinally: () => void = () => {},
47+
onSuccess: (result: ValueType | AwaitedPromise<ValueType>) => void = () => {},
48+
): ValueType {
1849
let maybePromiseResult: ReturnType<Fn>;
1950
try {
2051
maybePromiseResult = fn();
@@ -24,7 +55,7 @@ export function handleCallbackErrors<
2455
throw e;
2556
}
2657

27-
return maybeHandlePromiseRejection(maybePromiseResult, onError, onFinally);
58+
return maybeHandlePromiseRejection(maybePromiseResult, onError, onFinally, onSuccess);
2859
}
2960

3061
/**
@@ -37,12 +68,14 @@ function maybeHandlePromiseRejection<MaybePromise>(
3768
value: MaybePromise,
3869
onError: (error: unknown) => void,
3970
onFinally: () => void,
71+
onSuccess: (result: MaybePromise | AwaitedPromise<MaybePromise>) => void,
4072
): MaybePromise {
4173
if (isThenable(value)) {
4274
// @ts-expect-error - the isThenable check returns the "wrong" type here
4375
return value.then(
4476
res => {
4577
onFinally();
78+
onSuccess(res);
4679
return res;
4780
},
4881
e => {
@@ -54,5 +87,6 @@ function maybeHandlePromiseRejection<MaybePromise>(
5487
}
5588

5689
onFinally();
90+
onSuccess(value);
5791
return value;
5892
}

packages/core/test/lib/utils/handleCallbackErrors.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,94 @@ describe('handleCallbackErrors', () => {
148148
expect(onFinally).toHaveBeenCalledTimes(1);
149149
});
150150
});
151+
152+
describe('onSuccess', () => {
153+
it('triggers after successful sync callback', () => {
154+
const onError = vi.fn();
155+
const onFinally = vi.fn();
156+
const onSuccess = vi.fn();
157+
158+
const fn = vi.fn(() => 'aa');
159+
160+
const res = handleCallbackErrors(fn, onError, onFinally, onSuccess);
161+
162+
expect(res).toBe('aa');
163+
expect(fn).toHaveBeenCalledTimes(1);
164+
expect(onError).not.toHaveBeenCalled();
165+
expect(onFinally).toHaveBeenCalledTimes(1);
166+
expect(onSuccess).toHaveBeenCalledTimes(1);
167+
expect(onSuccess).toHaveBeenCalledWith('aa');
168+
});
169+
170+
it('does not trigger onSuccess after error in sync callback', () => {
171+
const error = new Error('test error');
172+
173+
const onError = vi.fn();
174+
const onFinally = vi.fn();
175+
const onSuccess = vi.fn();
176+
177+
const fn = vi.fn(() => {
178+
throw error;
179+
});
180+
181+
expect(() => handleCallbackErrors(fn, onError, onFinally, onSuccess)).toThrow(error);
182+
183+
expect(fn).toHaveBeenCalledTimes(1);
184+
expect(onError).toHaveBeenCalledTimes(1);
185+
expect(onError).toHaveBeenCalledWith(error);
186+
expect(onFinally).toHaveBeenCalledTimes(1);
187+
expect(onSuccess).not.toHaveBeenCalled();
188+
});
189+
190+
it('triggers after successful async callback', async () => {
191+
const onError = vi.fn();
192+
const onFinally = vi.fn();
193+
const onSuccess = vi.fn();
194+
195+
const fn = vi.fn(async () => 'aa');
196+
197+
const res = handleCallbackErrors(fn, onError, onFinally, onSuccess);
198+
199+
expect(res).toBeInstanceOf(Promise);
200+
201+
expect(fn).toHaveBeenCalledTimes(1);
202+
expect(onError).not.toHaveBeenCalled();
203+
expect(onFinally).not.toHaveBeenCalled();
204+
205+
const value = await res;
206+
expect(value).toBe('aa');
207+
208+
expect(onFinally).toHaveBeenCalledTimes(1);
209+
expect(onSuccess).toHaveBeenCalled();
210+
expect(onSuccess).toHaveBeenCalledWith('aa');
211+
});
212+
213+
it('does not trigger onSuccess after error in async callback', async () => {
214+
const onError = vi.fn();
215+
const onFinally = vi.fn();
216+
const onSuccess = vi.fn();
217+
218+
const error = new Error('test error');
219+
220+
const fn = vi.fn(async () => {
221+
await new Promise(resolve => setTimeout(resolve, 10));
222+
throw error;
223+
});
224+
225+
const res = handleCallbackErrors(fn, onError, onFinally, onSuccess);
226+
227+
expect(res).toBeInstanceOf(Promise);
228+
229+
expect(fn).toHaveBeenCalledTimes(1);
230+
expect(onError).not.toHaveBeenCalled();
231+
expect(onFinally).not.toHaveBeenCalled();
232+
233+
await expect(res).rejects.toThrow(error);
234+
235+
expect(onError).toHaveBeenCalledTimes(1);
236+
expect(onError).toHaveBeenCalledWith(error);
237+
expect(onFinally).toHaveBeenCalledTimes(1);
238+
expect(onSuccess).not.toHaveBeenCalled();
239+
});
240+
});
151241
});

packages/node/src/integrations/tracing/vercelai/instrumentation.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
getActiveSpan,
1010
getClient,
1111
handleCallbackErrors,
12-
isThenable,
1312
SDK_VERSION,
1413
withScope,
1514
} from '@sentry/core';
@@ -238,19 +237,7 @@ export class SentryVercelAiInstrumentation extends InstrumentationBase {
238237
};
239238

240239
return handleCallbackErrors(
241-
() => {
242-
const result = Reflect.apply(target, thisArg, args);
243-
244-
if (isThenable(result)) {
245-
// check for tool errors when the promise resolves, keep the original promise identity
246-
result.then(checkResultForToolErrors, () => {});
247-
return result;
248-
}
249-
250-
// check for tool errors when the result is synchronous
251-
checkResultForToolErrors(result);
252-
return result;
253-
},
240+
() => Reflect.apply(target, thisArg, args),
254241
error => {
255242
// This error bubbles up to unhandledrejection handler (if not handled before),
256243
// where we do not know the active span anymore
@@ -260,6 +247,10 @@ export class SentryVercelAiInstrumentation extends InstrumentationBase {
260247
addNonEnumerableProperty(error, '_sentry_active_span', getActiveSpan());
261248
}
262249
},
250+
() => {},
251+
result => {
252+
checkResultForToolErrors(result);
253+
},
263254
);
264255
},
265256
});

0 commit comments

Comments
 (0)