From 0f1c5c83bdd88717ac6cc600cda6218861d12ee6 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 16 Apr 2026 12:56:19 -0400 Subject: [PATCH] fix(node-core): Pass rejection reason instead of Promise as originalException In the onUnhandledRejection handler, captureException was being called with originalException set to the rejected Promise object instead of the rejection reason. This meant hint.originalException in beforeSend and downstream integrations (localVariablesAsync, extraErrorData, zoderrors) received a Promise rather than the actual error, breaking any logic that inspects it. This aligns Node with the browser SDK and the onUncaughtException handler. Fixes getsentry/sentry-javascript#20325 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/integrations/onunhandledrejection.ts | 4 +- .../integrations/onunhandledrejection.test.ts | 54 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 packages/node-core/test/integrations/onunhandledrejection.test.ts diff --git a/packages/node-core/src/integrations/onunhandledrejection.ts b/packages/node-core/src/integrations/onunhandledrejection.ts index af40bacfda57..8e2483d6a8cb 100644 --- a/packages/node-core/src/integrations/onunhandledrejection.ts +++ b/packages/node-core/src/integrations/onunhandledrejection.ts @@ -85,7 +85,7 @@ export function makeUnhandledPromiseHandler( client: Client, options: OnUnhandledRejectionOptions, ): (reason: unknown, promise: unknown) => void { - return function sendUnhandledPromise(reason: unknown, promise: unknown): void { + return function sendUnhandledPromise(reason: unknown, _promise: unknown): void { // Only handle for the active client if (getClient() !== client) { return; @@ -109,7 +109,7 @@ export function makeUnhandledPromiseHandler( activeSpanWrapper(() => { captureException(reason, { - originalException: promise, + originalException: reason, captureContext: { extra: { unhandledPromiseRejection: true }, level, diff --git a/packages/node-core/test/integrations/onunhandledrejection.test.ts b/packages/node-core/test/integrations/onunhandledrejection.test.ts new file mode 100644 index 000000000000..1f2c2c3c2581 --- /dev/null +++ b/packages/node-core/test/integrations/onunhandledrejection.test.ts @@ -0,0 +1,54 @@ +import * as SentryCore from '@sentry/core'; +import type { Client } from '@sentry/core'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { + makeUnhandledPromiseHandler, + onUnhandledRejectionIntegration, +} from '../../src/integrations/onunhandledrejection'; + +// 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 +global.console.warn = () => null; +global.console.error = () => null; + +describe('unhandled promises', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('installs a global listener', () => { + const client = { getOptions: () => ({}) } as unknown as Client; + SentryCore.setCurrentClient(client); + + const beforeListeners = process.listeners('unhandledRejection').length; + + const integration = onUnhandledRejectionIntegration(); + integration.setup!(client); + + expect(process.listeners('unhandledRejection').length).toBe(beforeListeners + 1); + }); + + it('passes the rejection reason (not the promise) as originalException', () => { + const client = { getOptions: () => ({}) } as unknown as Client; + SentryCore.setCurrentClient(client); + + const reason = new Error('boom'); + const promise = Promise.reject(reason); + // swallow the rejection so it does not leak into the test runner + promise.catch(() => {}); + + const captureException = vi.spyOn(SentryCore, 'captureException').mockImplementation(() => 'test'); + + const handler = makeUnhandledPromiseHandler(client, { mode: 'warn', ignore: [] }); + handler(reason, promise); + + expect(captureException).toHaveBeenCalledTimes(1); + const [capturedReason, hint] = captureException.mock.calls[0]!; + expect(capturedReason).toBe(reason); + expect(hint?.originalException).toBe(reason); + expect(hint?.originalException).not.toBe(promise); + expect(hint?.mechanism).toEqual({ + handled: false, + type: 'auto.node.onunhandledrejection', + }); + }); +});