Skip to content

Commit a87183e

Browse files
logaretmclaude
andauthored
fix(node-core): Pass rejection reason instead of Promise as originalException (#20366)
This PR passes the rejection reason to `captureException` as `originalException`. Previously it was passing the promise which was useless to users. This aligns Node with browser/onUncaughtException behavior as well. closes #20325 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5d0d145 commit a87183e

File tree

2 files changed

+56
-2
lines changed

2 files changed

+56
-2
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export function makeUnhandledPromiseHandler(
8585
client: Client,
8686
options: OnUnhandledRejectionOptions,
8787
): (reason: unknown, promise: unknown) => void {
88-
return function sendUnhandledPromise(reason: unknown, promise: unknown): void {
88+
return function sendUnhandledPromise(reason: unknown, _promise: unknown): void {
8989
// Only handle for the active client
9090
if (getClient() !== client) {
9191
return;
@@ -109,7 +109,7 @@ export function makeUnhandledPromiseHandler(
109109

110110
activeSpanWrapper(() => {
111111
captureException(reason, {
112-
originalException: promise,
112+
originalException: reason,
113113
captureContext: {
114114
extra: { unhandledPromiseRejection: true },
115115
level,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import * as SentryCore from '@sentry/core';
2+
import type { Client } from '@sentry/core';
3+
import { afterEach, describe, expect, it, vi } from 'vitest';
4+
import {
5+
makeUnhandledPromiseHandler,
6+
onUnhandledRejectionIntegration,
7+
} from '../../src/integrations/onunhandledrejection';
8+
9+
// don't log the test errors we're going to throw, so at a quick glance it doesn't look like the test itself has failed
10+
global.console.warn = () => null;
11+
global.console.error = () => null;
12+
13+
describe('unhandled promises', () => {
14+
afterEach(() => {
15+
vi.restoreAllMocks();
16+
});
17+
18+
it('installs a global listener', () => {
19+
const client = { getOptions: () => ({}) } as unknown as Client;
20+
SentryCore.setCurrentClient(client);
21+
22+
const beforeListeners = process.listeners('unhandledRejection').length;
23+
24+
const integration = onUnhandledRejectionIntegration();
25+
integration.setup!(client);
26+
27+
expect(process.listeners('unhandledRejection').length).toBe(beforeListeners + 1);
28+
});
29+
30+
it('passes the rejection reason (not the promise) as originalException', () => {
31+
const client = { getOptions: () => ({}) } as unknown as Client;
32+
SentryCore.setCurrentClient(client);
33+
34+
const reason = new Error('boom');
35+
const promise = Promise.reject(reason);
36+
// swallow the rejection so it does not leak into the test runner
37+
promise.catch(() => {});
38+
39+
const captureException = vi.spyOn(SentryCore, 'captureException').mockImplementation(() => 'test');
40+
41+
const handler = makeUnhandledPromiseHandler(client, { mode: 'warn', ignore: [] });
42+
handler(reason, promise);
43+
44+
expect(captureException).toHaveBeenCalledTimes(1);
45+
const [capturedReason, hint] = captureException.mock.calls[0]!;
46+
expect(capturedReason).toBe(reason);
47+
expect(hint?.originalException).toBe(reason);
48+
expect(hint?.originalException).not.toBe(promise);
49+
expect(hint?.mechanism).toEqual({
50+
handled: false,
51+
type: 'auto.node.onunhandledrejection',
52+
});
53+
});
54+
});

0 commit comments

Comments
 (0)