Skip to content

Commit 985873e

Browse files
authored
feat(node-core): Extend onnhandledrejection with ignore errors option (#17736)
I’m introducing support for selectively suppressing specific errors. This is done by adding a new option that allows users to override the default logging modes. By passing this option down through the configuration, users will have finer control over which errors are logged versus ignored. Also added a default array for the errors, addressing an issue where Vercel’s flush is called during abort, causing unnecessary error logs.
1 parent 0295e69 commit 985873e

File tree

4 files changed

+136
-15
lines changed

4 files changed

+136
-15
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const Sentry = require('@sentry/node');
2+
3+
Sentry.init({
4+
dsn: 'https://[email protected]/1337',
5+
integrations: [
6+
Sentry.onUnhandledRejectionIntegration({
7+
// Use default mode: 'warn' - integration is active but should ignore CustomIgnoredError
8+
ignore: [{ name: 'CustomIgnoredError' }],
9+
}),
10+
],
11+
});
12+
13+
// Create a custom error that should be ignored
14+
class CustomIgnoredError extends Error {
15+
constructor(message) {
16+
super(message);
17+
this.name = 'CustomIgnoredError';
18+
}
19+
}
20+
21+
setTimeout(() => {
22+
process.stdout.write("I'm alive!");
23+
process.exit(0);
24+
}, 500);
25+
26+
// This should be ignored by the custom ignore matcher and not produce a warning
27+
Promise.reject(new CustomIgnoredError('This error should be ignored'));
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const Sentry = require('@sentry/node');
2+
3+
Sentry.init({
4+
dsn: 'https://[email protected]/1337',
5+
// Use default mode: 'warn' - integration is active but should ignore AI_NoOutputGeneratedError
6+
});
7+
8+
// Create an error with the name that should be ignored by default
9+
class AI_NoOutputGeneratedError extends Error {
10+
constructor(message) {
11+
super(message);
12+
this.name = 'AI_NoOutputGeneratedError';
13+
}
14+
}
15+
16+
setTimeout(() => {
17+
process.stdout.write("I'm alive!");
18+
process.exit(0);
19+
}, 500);
20+
21+
// This should be ignored by default and not produce a warning
22+
Promise.reject(new AI_NoOutputGeneratedError('Stream aborted'));

dev-packages/node-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,32 @@ test rejection`);
178178
expect(transactionEvent!.contexts!.trace!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id);
179179
expect(transactionEvent!.contexts!.trace!.span_id).toBe(errorEvent!.contexts!.trace!.span_id);
180180
});
181+
182+
test('should not warn when AI_NoOutputGeneratedError is rejected (default ignore)', () =>
183+
new Promise<void>(done => {
184+
expect.assertions(3);
185+
186+
const testScriptPath = path.resolve(__dirname, 'ignore-default.js');
187+
188+
childProcess.execFile('node', [testScriptPath], { encoding: 'utf8' }, (err, stdout, stderr) => {
189+
expect(err).toBeNull();
190+
expect(stdout).toBe("I'm alive!");
191+
expect(stderr).toBe(''); // No warning should be shown
192+
done();
193+
});
194+
}));
195+
196+
test('should not warn when custom ignored error by name is rejected', () =>
197+
new Promise<void>(done => {
198+
expect.assertions(3);
199+
200+
const testScriptPath = path.resolve(__dirname, 'ignore-custom-name.js');
201+
202+
childProcess.execFile('node', [testScriptPath], { encoding: 'utf8' }, (err, stdout, stderr) => {
203+
expect(err).toBeNull();
204+
expect(stdout).toBe("I'm alive!");
205+
expect(stderr).toBe(''); // No warning should be shown
206+
done();
207+
});
208+
}));
181209
});

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

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,41 @@
11
import type { Client, IntegrationFn, SeverityLevel, Span } from '@sentry/core';
2-
import { captureException, consoleSandbox, defineIntegration, getClient, withActiveSpan } from '@sentry/core';
2+
import {
3+
captureException,
4+
consoleSandbox,
5+
defineIntegration,
6+
getClient,
7+
isMatchingPattern,
8+
withActiveSpan,
9+
} from '@sentry/core';
310
import { logAndExitProcess } from '../utils/errorhandling';
411

512
type UnhandledRejectionMode = 'none' | 'warn' | 'strict';
613

14+
type IgnoreMatcher = { name?: string | RegExp; message?: string | RegExp };
15+
716
interface OnUnhandledRejectionOptions {
817
/**
918
* Option deciding what to do after capturing unhandledRejection,
1019
* that mimicks behavior of node's --unhandled-rejection flag.
1120
*/
1221
mode: UnhandledRejectionMode;
22+
/** Rejection Errors to ignore (don't capture or warn). */
23+
ignore?: IgnoreMatcher[];
1324
}
1425

1526
const INTEGRATION_NAME = 'OnUnhandledRejection';
1627

28+
const DEFAULT_IGNORES: IgnoreMatcher[] = [
29+
{
30+
name: 'AI_NoOutputGeneratedError', // When stream aborts in Vercel AI SDK, Vercel flush() fails with an error
31+
},
32+
];
33+
1734
const _onUnhandledRejectionIntegration = ((options: Partial<OnUnhandledRejectionOptions> = {}) => {
18-
const opts = {
19-
mode: 'warn',
20-
...options,
21-
} satisfies OnUnhandledRejectionOptions;
35+
const opts: OnUnhandledRejectionOptions = {
36+
mode: options.mode ?? 'warn',
37+
ignore: [...DEFAULT_IGNORES, ...(options.ignore ?? [])],
38+
};
2239

2340
return {
2441
name: INTEGRATION_NAME,
@@ -28,27 +45,54 @@ const _onUnhandledRejectionIntegration = ((options: Partial<OnUnhandledRejection
2845
};
2946
}) satisfies IntegrationFn;
3047

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

36-
/**
37-
* Send an exception with reason
38-
* @param reason string
39-
* @param promise promise
40-
*
41-
* Exported only for tests.
42-
*/
50+
/** Extract error info safely */
51+
function extractErrorInfo(reason: unknown): { name: string; message: string } {
52+
// Check if reason is an object (including Error instances, not just plain objects)
53+
if (typeof reason !== 'object' || reason === null) {
54+
return { name: '', message: String(reason ?? '') };
55+
}
56+
57+
const errorLike = reason as Record<string, unknown>;
58+
const name = typeof errorLike.name === 'string' ? errorLike.name : '';
59+
const message = typeof errorLike.message === 'string' ? errorLike.message : String(reason);
60+
61+
return { name, message };
62+
}
63+
64+
/** Check if a matcher matches the reason */
65+
function isMatchingReason(matcher: IgnoreMatcher, errorInfo: ReturnType<typeof extractErrorInfo>): boolean {
66+
// name/message matcher
67+
const nameMatches = matcher.name === undefined || isMatchingPattern(errorInfo.name, matcher.name, true);
68+
69+
const messageMatches = matcher.message === undefined || isMatchingPattern(errorInfo.message, matcher.message);
70+
71+
return nameMatches && messageMatches;
72+
}
73+
74+
/** Match helper */
75+
function matchesIgnore(list: IgnoreMatcher[], reason: unknown): boolean {
76+
const errorInfo = extractErrorInfo(reason);
77+
return list.some(matcher => isMatchingReason(matcher, errorInfo));
78+
}
79+
80+
/** Core handler */
4381
export function makeUnhandledPromiseHandler(
4482
client: Client,
4583
options: OnUnhandledRejectionOptions,
4684
): (reason: unknown, promise: unknown) => void {
4785
return function sendUnhandledPromise(reason: unknown, promise: unknown): void {
86+
// Only handle for the active client
4887
if (getClient() !== client) {
4988
return;
5089
}
5190

91+
// Skip if configured to ignore
92+
if (matchesIgnore(options.ignore ?? [], reason)) {
93+
return;
94+
}
95+
5296
const level: SeverityLevel = options.mode === 'strict' ? 'fatal' : 'error';
5397

5498
// this can be set in places where we cannot reliably get access to the active span/error

0 commit comments

Comments
 (0)