From abaf5941065e9cdf2960ad79725ae6bd2f1dad35 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 7 Apr 2026 15:15:42 +0200 Subject: [PATCH 1/5] fix(android): Use componentStack as fallback for missing error stack traces When React render errors occur on Android with Hermes, the error may arrive at ErrorUtils.setGlobalHandler without a .stack property. However, React attaches a .componentStack with component locations and bundle offsets before the error reaches the handler. Use componentStack as a fallback for error.stack so that eventFromException can extract frames with source locations. Fixes #5071 Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 1 + .../integrations/reactnativeerrorhandlers.ts | 9 +++++ .../reactnativeerrorhandlers.test.ts | 34 +++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e64a69dd41..dc8891b7b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixes +- Use React `componentStack` as fallback when error has no stack trace on Android ([#5071](https://github.com/getsentry/sentry-react-native/issues/5071)) - Add `SENTRY_PROJECT_ROOT` env var to override project root in Xcode build phase scripts for monorepo setups ([#5961](https://github.com/getsentry/sentry-react-native/pull/5961)) ### Features diff --git a/packages/core/src/js/integrations/reactnativeerrorhandlers.ts b/packages/core/src/js/integrations/reactnativeerrorhandlers.ts index ea12cd205a..579e0b8421 100644 --- a/packages/core/src/js/integrations/reactnativeerrorhandlers.ts +++ b/packages/core/src/js/integrations/reactnativeerrorhandlers.ts @@ -170,6 +170,15 @@ function setupErrorUtilsGlobalHandler(): void { return; } + // React render errors may arrive without a .stack but with a .componentStack + // (set by ReactFiberErrorDialog). Use the componentStack as a fallback so + // eventFromException can extract frames with source locations. + // oxlint-disable-next-line typescript-eslint(no-unsafe-member-access) + if (!error.stack && error.componentStack) { + // oxlint-disable-next-line typescript-eslint(no-unsafe-member-access) + error.stack = `${error.message || 'Error'}${error.componentStack}`; + } + const hint: EventHint = { originalException: error, attachments: getCurrentScope().getScopeData().attachments, diff --git a/packages/core/test/integrations/reactnativeerrorhandlers.test.ts b/packages/core/test/integrations/reactnativeerrorhandlers.test.ts index 9ab4d7dfe7..5de007992b 100644 --- a/packages/core/test/integrations/reactnativeerrorhandlers.test.ts +++ b/packages/core/test/integrations/reactnativeerrorhandlers.test.ts @@ -140,6 +140,40 @@ describe('ReactNativeErrorHandlers', () => { expect(hint).toEqual(expect.objectContaining({ originalException: new Error('Test Error') })); }); + + test('Uses componentStack as fallback when error has no stack', async () => { + const integration = reactNativeErrorHandlersIntegration(); + integration.setupOnce!(); + + const error: any = { + message: 'Value is undefined, expected an Object', + componentStack: + '\n at UserMessage (http://localhost:8081/index.bundle:1:5274251)' + + '\n at renderItem (http://localhost:8081/index.bundle:1:5280705)', + }; + + await errorHandlerCallback!(error, true); + await client.flush(); + + expect(error.stack).toBe( + 'Value is undefined, expected an Object' + + '\n at UserMessage (http://localhost:8081/index.bundle:1:5274251)' + + '\n at renderItem (http://localhost:8081/index.bundle:1:5280705)', + ); + }); + + test('Does not override stack when error already has one', async () => { + const integration = reactNativeErrorHandlersIntegration(); + integration.setupOnce!(); + + const error = new Error('Test Error'); + (error as any).componentStack = '\n at SomeComponent (http://localhost:8081/index.bundle:1:100)'; + const originalStack = error.stack; + + await errorHandlerCallback!(error, false); + + expect(error.stack).toBe(originalStack); + }); }); describe('onUnhandledRejection', () => { From bc7535cc454ac3ad6c79a59055bee9ffe416375e Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 7 Apr 2026 15:18:39 +0200 Subject: [PATCH 2/5] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8891b7b6..abfc17dbe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### Fixes -- Use React `componentStack` as fallback when error has no stack trace on Android ([#5071](https://github.com/getsentry/sentry-react-native/issues/5071)) +- Use React `componentStack` as fallback when error has no stack trace on Android ([#5965](https://github.com/getsentry/sentry-react-native/pull/5965) - Add `SENTRY_PROJECT_ROOT` env var to override project root in Xcode build phase scripts for monorepo setups ([#5961](https://github.com/getsentry/sentry-react-native/pull/5961)) ### Features From 583c99144a68ec7c6ea922291db61959e2d6ef09 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 7 Apr 2026 15:32:57 +0200 Subject: [PATCH 3/5] fix: Also handle stack with no frames (message-only stack string) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../integrations/reactnativeerrorhandlers.ts | 14 ++++++++--- .../reactnativeerrorhandlers.test.ts | 24 ++++++++++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/core/src/js/integrations/reactnativeerrorhandlers.ts b/packages/core/src/js/integrations/reactnativeerrorhandlers.ts index 579e0b8421..5a2a1da2da 100644 --- a/packages/core/src/js/integrations/reactnativeerrorhandlers.ts +++ b/packages/core/src/js/integrations/reactnativeerrorhandlers.ts @@ -170,11 +170,12 @@ function setupErrorUtilsGlobalHandler(): void { return; } - // React render errors may arrive without a .stack but with a .componentStack - // (set by ReactFiberErrorDialog). Use the componentStack as a fallback so + // React render errors may arrive without useful frames in .stack but with a + // .componentStack (set by ReactFiberErrorDialog) that contains component + // locations with bundle offsets. Use componentStack as a fallback so // eventFromException can extract frames with source locations. // oxlint-disable-next-line typescript-eslint(no-unsafe-member-access) - if (!error.stack && error.componentStack) { + if (error.componentStack && (!error.stack || !hasStackFrames(error.stack))) { // oxlint-disable-next-line typescript-eslint(no-unsafe-member-access) error.stack = `${error.message || 'Error'}${error.componentStack}`; } @@ -220,3 +221,10 @@ function setupErrorUtilsGlobalHandler(): void { ); }); } + +/** + * Checks if a stack trace string contains at least one frame line. + */ +function hasStackFrames(stack: unknown): boolean { + return typeof stack === 'string' && stack.includes('\n'); +} diff --git a/packages/core/test/integrations/reactnativeerrorhandlers.test.ts b/packages/core/test/integrations/reactnativeerrorhandlers.test.ts index 5de007992b..4611a6425b 100644 --- a/packages/core/test/integrations/reactnativeerrorhandlers.test.ts +++ b/packages/core/test/integrations/reactnativeerrorhandlers.test.ts @@ -162,7 +162,29 @@ describe('ReactNativeErrorHandlers', () => { ); }); - test('Does not override stack when error already has one', async () => { + test('Uses componentStack as fallback when stack has no frames', async () => { + const integration = reactNativeErrorHandlersIntegration(); + integration.setupOnce!(); + + const error: any = { + message: 'Value is undefined, expected an Object', + stack: 'Error: Value is undefined, expected an Object', + componentStack: + '\n at UserMessage (http://localhost:8081/index.bundle:1:5274251)' + + '\n at renderItem (http://localhost:8081/index.bundle:1:5280705)', + }; + + await errorHandlerCallback!(error, true); + await client.flush(); + + expect(error.stack).toBe( + 'Value is undefined, expected an Object' + + '\n at UserMessage (http://localhost:8081/index.bundle:1:5274251)' + + '\n at renderItem (http://localhost:8081/index.bundle:1:5280705)', + ); + }); + + test('Does not override stack when error already has frames', async () => { const integration = reactNativeErrorHandlersIntegration(); integration.setupOnce!(); From 13b29596adbeb63fc328f13183cf101aa263ad2c Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 7 Apr 2026 15:35:46 +0200 Subject: [PATCH 4/5] fix: Add null guard for error object in componentStack fallback Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/js/integrations/reactnativeerrorhandlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/js/integrations/reactnativeerrorhandlers.ts b/packages/core/src/js/integrations/reactnativeerrorhandlers.ts index 5a2a1da2da..09e49a3d16 100644 --- a/packages/core/src/js/integrations/reactnativeerrorhandlers.ts +++ b/packages/core/src/js/integrations/reactnativeerrorhandlers.ts @@ -175,7 +175,7 @@ function setupErrorUtilsGlobalHandler(): void { // locations with bundle offsets. Use componentStack as a fallback so // eventFromException can extract frames with source locations. // oxlint-disable-next-line typescript-eslint(no-unsafe-member-access) - if (error.componentStack && (!error.stack || !hasStackFrames(error.stack))) { + if (error && error.componentStack && (!error.stack || !hasStackFrames(error.stack))) { // oxlint-disable-next-line typescript-eslint(no-unsafe-member-access) error.stack = `${error.message || 'Error'}${error.componentStack}`; } From 8c77549f00a99bc80268e11d1288ccae65e20d1f Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 7 Apr 2026 16:15:10 +0200 Subject: [PATCH 5/5] fix: Use optional chaining for error null check Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/js/integrations/reactnativeerrorhandlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/js/integrations/reactnativeerrorhandlers.ts b/packages/core/src/js/integrations/reactnativeerrorhandlers.ts index 09e49a3d16..35ff650ae3 100644 --- a/packages/core/src/js/integrations/reactnativeerrorhandlers.ts +++ b/packages/core/src/js/integrations/reactnativeerrorhandlers.ts @@ -175,7 +175,7 @@ function setupErrorUtilsGlobalHandler(): void { // locations with bundle offsets. Use componentStack as a fallback so // eventFromException can extract frames with source locations. // oxlint-disable-next-line typescript-eslint(no-unsafe-member-access) - if (error && error.componentStack && (!error.stack || !hasStackFrames(error.stack))) { + if (error?.componentStack && (!error.stack || !hasStackFrames(error.stack))) { // oxlint-disable-next-line typescript-eslint(no-unsafe-member-access) error.stack = `${error.message || 'Error'}${error.componentStack}`; }