Skip to content

Commit a9bbc90

Browse files
committed
feat(node-core): Extend onnhandledrejection with ignore errors options
1 parent a69d4a2 commit a9bbc90

File tree

2 files changed

+65
-54
lines changed

2 files changed

+65
-54
lines changed

packages/node-core/src/integrations/onunhandledrejection.ts

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,31 @@ import { logAndExitProcess } from '../utils/errorhandling';
44

55
type UnhandledRejectionMode = 'none' | 'warn' | 'strict';
66

7+
type IgnoreMatcher = { symbol: symbol } | { name?: string | RegExp; message?: string | RegExp };
8+
79
interface OnUnhandledRejectionOptions {
810
/**
911
* Option deciding what to do after capturing unhandledRejection,
1012
* that mimicks behavior of node's --unhandled-rejection flag.
1113
*/
1214
mode: UnhandledRejectionMode;
15+
/** Rejection Errors to ignore (don't capture or warn). */
16+
ignore?: IgnoreMatcher[];
1317
}
1418

1519
const INTEGRATION_NAME = 'OnUnhandledRejection';
1620

21+
const DEFAULT_IGNORES: IgnoreMatcher[] = [
22+
{
23+
name: 'AI_NoOutputGeneratedError', // When stream aborts in Vercel AI SDK, flush() fails with an error
24+
},
25+
];
26+
1727
const _onUnhandledRejectionIntegration = ((options: Partial<OnUnhandledRejectionOptions> = {}) => {
18-
const opts = {
19-
mode: 'warn',
20-
...options,
21-
} satisfies OnUnhandledRejectionOptions;
28+
const opts: OnUnhandledRejectionOptions = {
29+
mode: options.mode ?? 'warn',
30+
ignore: [...DEFAULT_IGNORES, ...(options.ignore ?? [])],
31+
};
2232

2333
return {
2434
name: INTEGRATION_NAME,
@@ -28,31 +38,67 @@ const _onUnhandledRejectionIntegration = ((options: Partial<OnUnhandledRejection
2838
};
2939
}) satisfies IntegrationFn;
3040

31-
/**
32-
* Add a global promise rejection handler.
33-
*/
3441
export const onUnhandledRejectionIntegration = defineIntegration(_onUnhandledRejectionIntegration);
3542

36-
/**
37-
* Send an exception with reason
38-
* @param reason string
39-
* @param promise promise
40-
*
41-
* Exported only for tests.
42-
*/
43+
/** Extract error info safely */
44+
function extractErrorInfo(reason: unknown): { name: string; message: string; isObject: boolean } {
45+
const isObject = reason !== null && typeof reason === 'object';
46+
if (!isObject) {
47+
return { name: '', message: String(reason ?? ''), isObject };
48+
}
49+
50+
const errorLike = reason as Record<string, unknown>;
51+
const name = typeof errorLike.name === 'string' ? errorLike.name : '';
52+
const message = typeof errorLike.message === 'string' ? errorLike.message : String(reason);
53+
54+
return { name, message, isObject };
55+
}
56+
57+
/** Check if a matcher matches the reason */
58+
function checkMatcher(
59+
matcher: IgnoreMatcher,
60+
reason: unknown,
61+
errorInfo: ReturnType<typeof extractErrorInfo>,
62+
): boolean {
63+
if ('symbol' in matcher) {
64+
return errorInfo.isObject && matcher.symbol in (reason as object);
65+
}
66+
67+
// name/message matcher
68+
const nameMatches =
69+
matcher.name === undefined ||
70+
(typeof matcher.name === 'string' ? errorInfo.name === matcher.name : matcher.name.test(errorInfo.name));
71+
72+
const messageMatches =
73+
matcher.message === undefined ||
74+
(typeof matcher.message === 'string'
75+
? errorInfo.message.includes(matcher.message)
76+
: matcher.message.test(errorInfo.message));
77+
78+
return nameMatches && messageMatches;
79+
}
80+
81+
/** Match helper */
82+
function matchesIgnore(reason: unknown, list: IgnoreMatcher[]): boolean {
83+
const errorInfo = extractErrorInfo(reason);
84+
return list.some(matcher => checkMatcher(matcher, reason, errorInfo));
85+
}
86+
87+
/** Core handler */
4388
export function makeUnhandledPromiseHandler(
4489
client: Client,
4590
options: OnUnhandledRejectionOptions,
4691
): (reason: unknown, promise: unknown) => void {
4792
return function sendUnhandledPromise(reason: unknown, promise: unknown): void {
48-
if (getClient() !== client) {
49-
return;
50-
}
93+
// Only handle for the active client
94+
if (getClient() !== client) return;
95+
96+
// Skip if configured to ignore
97+
if (matchesIgnore(reason, options.ignore ?? [])) return;
5198

5299
const level: SeverityLevel = options.mode === 'strict' ? 'fatal' : 'error';
53100

54-
// this can be set in places where we cannot reliably get access to the active span/error
55-
// when the error bubbles up to this handler, we can use this to set the active span
101+
// If upstream code stored an active span on the error, use it for linking.
56102
const activeSpanForError =
57103
reason && typeof reason === 'object' ? (reason as { _sentry_active_span?: Span })._sentry_active_span : undefined;
58104

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

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -202,48 +202,13 @@ export class SentryVercelAiInstrumentation extends InstrumentationBase {
202202
}
203203
}
204204

205-
/**
206-
* Sets up global error handling for Vercel AI stream processing errors
207-
*/
208-
private _setupGlobalErrorHandling(): void {
209-
// Add a global unhandled rejection handler specifically for Vercel AI errors
210-
const originalHandler = process.listeners('unhandledRejection');
211-
const aiErrorHandler = (reason: unknown, promise: Promise<unknown>): void => {
212-
// Check if this is a Vercel AI error
213-
if (reason && typeof reason === 'object' && reason !== null && Symbol.for('vercel.ai.error') in reason) {
214-
// Add Sentry context to the error
215-
if (reason && typeof reason === 'object') {
216-
addNonEnumerableProperty(reason, '_sentry_active_span', getActiveSpan());
217-
}
218-
219-
// Don't re-throw the error to prevent it from becoming unhandled
220-
return;
221-
}
222-
223-
// For non-AI errors, let the original handler deal with it
224-
if (originalHandler.length > 0) {
225-
originalHandler.forEach(handler => {
226-
if (typeof handler === 'function') {
227-
handler(reason, promise);
228-
}
229-
});
230-
}
231-
};
232-
233-
// Remove any existing unhandled rejection handlers and add our AI-specific one
234-
process.removeAllListeners('unhandledRejection');
235-
process.on('unhandledRejection', aiErrorHandler);
236-
}
237205

238206
/**
239207
* Patches module exports to enable Vercel AI telemetry.
240208
*/
241209
private _patch(moduleExports: PatchedModuleExports): unknown {
242210
this._isPatched = true;
243211

244-
// Set up global error handling for Vercel AI stream processing errors
245-
this._setupGlobalErrorHandling();
246-
247212
this._callbacks.forEach(callback => callback());
248213
this._callbacks = [];
249214

0 commit comments

Comments
 (0)