-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(node-core): Extend onnhandledrejection with ignore errors option #17736
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
a69d4a2
a9bbc90
403893c
e4dab91
36ad33d
62bbdcd
d5352c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
const Sentry = require('@sentry/node'); | ||
|
||
Sentry.init({ | ||
dsn: 'https://[email protected]/1337', | ||
integrations: [ | ||
Sentry.onUnhandledRejectionIntegration({ | ||
// Use default mode: 'warn' - integration is active but should ignore CustomIgnoredError | ||
ignore: [{ name: 'CustomIgnoredError' }], | ||
}), | ||
], | ||
}); | ||
|
||
// Create a custom error that should be ignored | ||
class CustomIgnoredError extends Error { | ||
constructor(message) { | ||
super(message); | ||
this.name = 'CustomIgnoredError'; | ||
} | ||
} | ||
|
||
setTimeout(() => { | ||
process.stdout.write("I'm alive!"); | ||
process.exit(0); | ||
}, 500); | ||
|
||
// This should be ignored by the custom ignore matcher and not produce a warning | ||
Promise.reject(new CustomIgnoredError('This error should be ignored')); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
const Sentry = require('@sentry/node'); | ||
|
||
const IGNORE_SYMBOL = Symbol('ignore'); | ||
|
||
Sentry.init({ | ||
dsn: 'https://[email protected]/1337', | ||
integrations: [ | ||
Sentry.onUnhandledRejectionIntegration({ | ||
// Use default mode: 'warn' - integration is active but should ignore errors with the symbol | ||
ignore: [{ symbol: IGNORE_SYMBOL }], | ||
}), | ||
], | ||
}); | ||
|
||
// Create an error with the ignore symbol | ||
class CustomError extends Error { | ||
constructor(message) { | ||
super(message); | ||
this.name = 'CustomError'; | ||
this[IGNORE_SYMBOL] = true; | ||
} | ||
} | ||
|
||
setTimeout(() => { | ||
process.stdout.write("I'm alive!"); | ||
process.exit(0); | ||
}, 500); | ||
|
||
// This should be ignored by the symbol matcher and not produce a warning | ||
Promise.reject(new CustomError('This error should be ignored by symbol')); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
const Sentry = require('@sentry/node'); | ||
|
||
Sentry.init({ | ||
dsn: 'https://[email protected]/1337', | ||
// Use default mode: 'warn' - integration is active but should ignore AI_NoOutputGeneratedError | ||
}); | ||
|
||
// Create an error with the name that should be ignored by default | ||
class AI_NoOutputGeneratedError extends Error { | ||
constructor(message) { | ||
super(message); | ||
this.name = 'AI_NoOutputGeneratedError'; | ||
} | ||
} | ||
|
||
setTimeout(() => { | ||
process.stdout.write("I'm alive!"); | ||
process.exit(0); | ||
}, 500); | ||
|
||
// This should be ignored by default and not produce a warning | ||
Promise.reject(new AI_NoOutputGeneratedError('Stream aborted')); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,21 +4,31 @@ import { logAndExitProcess } from '../utils/errorhandling'; | |
|
||
type UnhandledRejectionMode = 'none' | 'warn' | 'strict'; | ||
|
||
type IgnoreMatcher = { symbol: symbol } | { name?: string | RegExp; message?: string | RegExp }; | ||
|
||
interface OnUnhandledRejectionOptions { | ||
/** | ||
* Option deciding what to do after capturing unhandledRejection, | ||
* that mimicks behavior of node's --unhandled-rejection flag. | ||
*/ | ||
mode: UnhandledRejectionMode; | ||
/** Rejection Errors to ignore (don't capture or warn). */ | ||
ignore?: IgnoreMatcher[]; | ||
} | ||
|
||
const INTEGRATION_NAME = 'OnUnhandledRejection'; | ||
|
||
const DEFAULT_IGNORES: IgnoreMatcher[] = [ | ||
{ | ||
name: 'AI_NoOutputGeneratedError', // When stream aborts in Vercel AI SDK, Vercel flush() fails with an error | ||
}, | ||
]; | ||
|
||
const _onUnhandledRejectionIntegration = ((options: Partial<OnUnhandledRejectionOptions> = {}) => { | ||
const opts = { | ||
mode: 'warn', | ||
...options, | ||
} satisfies OnUnhandledRejectionOptions; | ||
const opts: OnUnhandledRejectionOptions = { | ||
mode: options.mode ?? 'warn', | ||
ignore: [...DEFAULT_IGNORES, ...(options.ignore ?? [])], | ||
}; | ||
|
||
return { | ||
name: INTEGRATION_NAME, | ||
|
@@ -28,26 +38,63 @@ const _onUnhandledRejectionIntegration = ((options: Partial<OnUnhandledRejection | |
}; | ||
}) satisfies IntegrationFn; | ||
|
||
/** | ||
* Add a global promise rejection handler. | ||
*/ | ||
export const onUnhandledRejectionIntegration = defineIntegration(_onUnhandledRejectionIntegration); | ||
|
||
/** | ||
* Send an exception with reason | ||
* @param reason string | ||
* @param promise promise | ||
* | ||
* Exported only for tests. | ||
*/ | ||
/** Extract error info safely */ | ||
function extractErrorInfo(reason: unknown): { name: string; message: string; isObject: boolean } { | ||
const isObject = reason !== null && typeof reason === 'object'; | ||
if (!isObject) { | ||
|
||
return { name: '', message: String(reason ?? ''), isObject }; | ||
} | ||
|
||
const errorLike = reason as Record<string, unknown>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. using |
||
const name = typeof errorLike.name === 'string' ? errorLike.name : ''; | ||
const message = typeof errorLike.message === 'string' ? errorLike.message : String(reason); | ||
|
||
return { name, message, isObject }; | ||
} | ||
|
||
/** Check if a matcher matches the reason */ | ||
function isMatchingReason( | ||
matcher: IgnoreMatcher, | ||
reason: unknown, | ||
errorInfo: ReturnType<typeof extractErrorInfo>, | ||
): boolean { | ||
if ('symbol' in matcher) { | ||
return errorInfo.isObject && matcher.symbol in (reason as object); | ||
} | ||
|
||
// name/message matcher | ||
const nameMatches = | ||
matcher.name === undefined || | ||
(typeof matcher.name === 'string' ? errorInfo.name === matcher.name : matcher.name.test(errorInfo.name)); | ||
|
||
const messageMatches = | ||
matcher.message === undefined || | ||
(typeof matcher.message === 'string' | ||
? errorInfo.message.includes(matcher.message) | ||
: matcher.message.test(errorInfo.message)); | ||
|
||
return nameMatches && messageMatches; | ||
|
||
} | ||
|
||
/** Match helper */ | ||
function matchesIgnore(reason: unknown, list: IgnoreMatcher[]): boolean { | ||
const errorInfo = extractErrorInfo(reason); | ||
return list.some(matcher => isMatchingReason(matcher, reason, errorInfo)); | ||
} | ||
|
||
/** Core handler */ | ||
export function makeUnhandledPromiseHandler( | ||
client: Client, | ||
options: OnUnhandledRejectionOptions, | ||
): (reason: unknown, promise: unknown) => void { | ||
return function sendUnhandledPromise(reason: unknown, promise: unknown): void { | ||
if (getClient() !== client) { | ||
return; | ||
} | ||
// Only handle for the active client | ||
if (getClient() !== client) return; | ||
|
||
// Skip if configured to ignore | ||
if (matchesIgnore(reason, options.ignore ?? [])) return; | ||
RulaKhaled marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
const level: SeverityLevel = options.mode === 'strict' ? 'fatal' : 'error'; | ||
|
||
|
Uh oh!
There was an error while loading. Please reload this page.