Skip to content

Commit 09f1971

Browse files
antonisclaude
andauthored
fix(android): Use componentStack as fallback for missing error stack traces (#5965)
* 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) <noreply@anthropic.com> * Update changelog * fix: Also handle stack with no frames (message-only stack string) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Add null guard for error object in componentStack fallback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: Use optional chaining for error null check Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 104f7ee commit 09f1971

File tree

3 files changed

+74
-0
lines changed

3 files changed

+74
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
### Fixes
1212

13+
- Use React `componentStack` as fallback when error has no stack trace on Android ([#5965](https://github.com/getsentry/sentry-react-native/pull/5965)
1314
- 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))
1415

1516
### Features

packages/core/src/js/integrations/reactnativeerrorhandlers.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,16 @@ function setupErrorUtilsGlobalHandler(): void {
170170
return;
171171
}
172172

173+
// React render errors may arrive without useful frames in .stack but with a
174+
// .componentStack (set by ReactFiberErrorDialog) that contains component
175+
// locations with bundle offsets. Use componentStack as a fallback so
176+
// eventFromException can extract frames with source locations.
177+
// oxlint-disable-next-line typescript-eslint(no-unsafe-member-access)
178+
if (error?.componentStack && (!error.stack || !hasStackFrames(error.stack))) {
179+
// oxlint-disable-next-line typescript-eslint(no-unsafe-member-access)
180+
error.stack = `${error.message || 'Error'}${error.componentStack}`;
181+
}
182+
173183
const hint: EventHint = {
174184
originalException: error,
175185
attachments: getCurrentScope().getScopeData().attachments,
@@ -211,3 +221,10 @@ function setupErrorUtilsGlobalHandler(): void {
211221
);
212222
});
213223
}
224+
225+
/**
226+
* Checks if a stack trace string contains at least one frame line.
227+
*/
228+
function hasStackFrames(stack: unknown): boolean {
229+
return typeof stack === 'string' && stack.includes('\n');
230+
}

packages/core/test/integrations/reactnativeerrorhandlers.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,62 @@ describe('ReactNativeErrorHandlers', () => {
140140

141141
expect(hint).toEqual(expect.objectContaining({ originalException: new Error('Test Error') }));
142142
});
143+
144+
test('Uses componentStack as fallback when error has no stack', async () => {
145+
const integration = reactNativeErrorHandlersIntegration();
146+
integration.setupOnce!();
147+
148+
const error: any = {
149+
message: 'Value is undefined, expected an Object',
150+
componentStack:
151+
'\n at UserMessage (http://localhost:8081/index.bundle:1:5274251)' +
152+
'\n at renderItem (http://localhost:8081/index.bundle:1:5280705)',
153+
};
154+
155+
await errorHandlerCallback!(error, true);
156+
await client.flush();
157+
158+
expect(error.stack).toBe(
159+
'Value is undefined, expected an Object' +
160+
'\n at UserMessage (http://localhost:8081/index.bundle:1:5274251)' +
161+
'\n at renderItem (http://localhost:8081/index.bundle:1:5280705)',
162+
);
163+
});
164+
165+
test('Uses componentStack as fallback when stack has no frames', async () => {
166+
const integration = reactNativeErrorHandlersIntegration();
167+
integration.setupOnce!();
168+
169+
const error: any = {
170+
message: 'Value is undefined, expected an Object',
171+
stack: 'Error: Value is undefined, expected an Object',
172+
componentStack:
173+
'\n at UserMessage (http://localhost:8081/index.bundle:1:5274251)' +
174+
'\n at renderItem (http://localhost:8081/index.bundle:1:5280705)',
175+
};
176+
177+
await errorHandlerCallback!(error, true);
178+
await client.flush();
179+
180+
expect(error.stack).toBe(
181+
'Value is undefined, expected an Object' +
182+
'\n at UserMessage (http://localhost:8081/index.bundle:1:5274251)' +
183+
'\n at renderItem (http://localhost:8081/index.bundle:1:5280705)',
184+
);
185+
});
186+
187+
test('Does not override stack when error already has frames', async () => {
188+
const integration = reactNativeErrorHandlersIntegration();
189+
integration.setupOnce!();
190+
191+
const error = new Error('Test Error');
192+
(error as any).componentStack = '\n at SomeComponent (http://localhost:8081/index.bundle:1:100)';
193+
const originalStack = error.stack;
194+
195+
await errorHandlerCallback!(error, false);
196+
197+
expect(error.stack).toBe(originalStack);
198+
});
143199
});
144200

145201
describe('onUnhandledRejection', () => {

0 commit comments

Comments
 (0)