Skip to content

Commit f0cf443

Browse files
Merge daa76e2 into 7ba69b6
2 parents 7ba69b6 + daa76e2 commit f0cf443

File tree

7 files changed

+269
-24
lines changed

7 files changed

+269
-24
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Encode envelopes using Base64, fix array length limit when transferring over Bridge. ([#2852](https://github.com/getsentry/sentry-react-native/pull/2852))
88
- This fix requires a rebuild of the native app
99
- Symbolicate message and non-Error stacktraces locally in debug mode ([#3420](https://github.com/getsentry/sentry-react-native/pull/3420))
10+
- Remove Sentry SDK frames from rejected promise SyntheticError stack ([#3423](https://github.com/getsentry/sentry-react-native/pull/3423))
1011

1112
## 5.14.1
1213

src/js/integrations/debugsymbolicator.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Event, EventHint, EventProcessor, Hub, Integration, StackFrame as SentryStackFrame } from '@sentry/types';
22
import { addContextToFrame, logger } from '@sentry/utils';
33

4+
import { getFramesToPop, isErrorLike } from '../utils/error';
45
import type * as ReactNative from '../vendor/react-native';
56

67
const INTERNAL_CALLSITES_REGEX = new RegExp(['ReactNativeRenderer-dev\\.js$', 'MessageQueue\\.js$'].join('|'));
@@ -37,24 +38,19 @@ export class DebugSymbolicator implements Integration {
3738
return event;
3839
}
3940

40-
if (
41-
event.exception &&
42-
hint.originalException &&
43-
typeof hint.originalException === 'object' &&
44-
'stack' in hint.originalException &&
45-
typeof hint.originalException.stack === 'string'
46-
) {
41+
if (event.exception && isErrorLike(hint.originalException)) {
4742
// originalException is ErrorLike object
48-
const symbolicatedFrames = await this._symbolicate(hint.originalException.stack);
43+
const symbolicatedFrames = await this._symbolicate(
44+
hint.originalException.stack,
45+
getFramesToPop(hint.originalException as Error),
46+
);
4947
symbolicatedFrames && this._replaceExceptionFramesInEvent(event, symbolicatedFrames);
50-
} else if (
51-
hint.syntheticException &&
52-
typeof hint.syntheticException === 'object' &&
53-
'stack' in hint.syntheticException &&
54-
typeof hint.syntheticException.stack === 'string'
55-
) {
48+
} else if (hint.syntheticException && isErrorLike(hint.syntheticException)) {
5649
// syntheticException is Error object
57-
const symbolicatedFrames = await this._symbolicate(hint.syntheticException.stack);
50+
const symbolicatedFrames = await this._symbolicate(
51+
hint.syntheticException.stack,
52+
getFramesToPop(hint.syntheticException),
53+
);
5854

5955
if (event.exception) {
6056
symbolicatedFrames && this._replaceExceptionFramesInEvent(event, symbolicatedFrames);
@@ -73,7 +69,7 @@ export class DebugSymbolicator implements Integration {
7369
* Symbolicates the stack on the device talking to local dev server.
7470
* Mutates the passed event.
7571
*/
76-
private async _symbolicate(rawStack: string): Promise<SentryStackFrame[] | null> {
72+
private async _symbolicate(rawStack: string, skipFirstFrames: number = 0): Promise<SentryStackFrame[] | null> {
7773
const parsedStack = this._parseErrorStack(rawStack);
7874

7975
try {
@@ -86,7 +82,14 @@ export class DebugSymbolicator implements Integration {
8682
// This has been changed in an react-native version so stack is contained in here
8783
const newStack = prettyStack.stack || prettyStack;
8884

89-
const stackWithoutInternalCallsites = newStack.filter(
85+
// https://github.com/getsentry/sentry-javascript/blob/739d904342aaf9327312f409952f14ceff4ae1ab/packages/utils/src/stacktrace.ts#L23
86+
// Match SentryParser which counts lines of stack (-1 for first line with the Error message)
87+
const skipFirstAdjustedToSentryStackParser = Math.max(skipFirstFrames - 1, 0);
88+
const stackWithoutPoppedFrames = skipFirstAdjustedToSentryStackParser
89+
? newStack.slice(skipFirstAdjustedToSentryStackParser)
90+
: newStack;
91+
92+
const stackWithoutInternalCallsites = stackWithoutPoppedFrames.filter(
9093
(frame: { file?: string }) => frame.file && frame.file.match(INTERNAL_CALLSITES_REGEX) === null,
9194
);
9295

src/js/integrations/reactnativeerrorhandlers.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { EventHint, Integration, SeverityLevel } from '@sentry/types';
33
import { addExceptionMechanism, logger } from '@sentry/utils';
44

55
import type { ReactNativeClient } from '../client';
6+
import { createSyntheticError, isErrorLike } from '../utils/error';
67
import { RN_GLOBAL_OBJ } from '../utils/worldwide';
78

89
/** ReactNativeErrorHandlers Options */
@@ -100,11 +101,7 @@ export class ReactNativeErrorHandlers implements Integration {
100101
* Attach the unhandled rejection handler
101102
*/
102103
private _attachUnhandledRejectionHandler(): void {
103-
const tracking: {
104-
disable: () => void;
105-
enable: (arg: unknown) => void;
106-
// eslint-disable-next-line import/no-extraneous-dependencies,@typescript-eslint/no-var-requires
107-
} = require('promise/setimmediate/rejection-tracking');
104+
const tracking = this._loadRejectionTracking();
108105

109106
const promiseRejectionTrackingOptions: PromiseRejectionTrackingOptions = {
110107
onUnhandled: (id, rejection = {}) => {
@@ -123,14 +120,15 @@ export class ReactNativeErrorHandlers implements Integration {
123120

124121
tracking.enable({
125122
allRejections: true,
126-
onUnhandled: (id: string, error: Error) => {
123+
onUnhandled: (id: string, error: unknown) => {
127124
if (__DEV__) {
128125
promiseRejectionTrackingOptions.onUnhandled(id, error);
129126
}
130127

131128
getCurrentHub().captureException(error, {
132129
data: { id },
133130
originalException: error,
131+
syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
134132
});
135133
},
136134
onHandled: (id: string) => {
@@ -251,4 +249,15 @@ export class ReactNativeErrorHandlers implements Integration {
251249
});
252250
}
253251
}
252+
253+
/**
254+
* Loads and returns rejection tracking module
255+
*/
256+
private _loadRejectionTracking(): {
257+
disable: () => void;
258+
enable: (arg: unknown) => void;
259+
} {
260+
// eslint-disable-next-line @typescript-eslint/no-var-requires,import/no-extraneous-dependencies
261+
return require('promise/setimmediate/rejection-tracking');
262+
}
254263
}

src/js/utils/error.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export interface ExtendedError extends Error {
2+
framesToPop?: number | undefined;
3+
}
4+
5+
// Sentry Stack Parser is skipping lines not frames
6+
// https://github.com/getsentry/sentry-javascript/blob/739d904342aaf9327312f409952f14ceff4ae1ab/packages/utils/src/stacktrace.ts#L23
7+
// 1 for first line with the Error message
8+
const SENTRY_STACK_PARSER_OFFSET = 1;
9+
const REMOVE_ERROR_CREATION_FRAMES = 2 + SENTRY_STACK_PARSER_OFFSET;
10+
11+
/**
12+
* Creates synthetic trace. By default pops 2 frames - `createSyntheticError` and the caller
13+
*/
14+
export function createSyntheticError(framesToPop: number = 0): ExtendedError {
15+
const error: ExtendedError = new Error();
16+
error.framesToPop = framesToPop + REMOVE_ERROR_CREATION_FRAMES; // Skip createSyntheticError's own stack frame.
17+
return error;
18+
}
19+
20+
/**
21+
* Returns the number of frames to pop from the stack trace.
22+
* @param error ExtendedError
23+
*/
24+
export function getFramesToPop(error: ExtendedError): number {
25+
return error.framesToPop !== undefined ? error.framesToPop : 0;
26+
}
27+
28+
/**
29+
* Check if `potentialError` is an object with string stack property.
30+
*/
31+
export function isErrorLike(potentialError: unknown): potentialError is { stack: string } {
32+
return (
33+
potentialError !== null &&
34+
typeof potentialError === 'object' &&
35+
'stack' in potentialError &&
36+
typeof potentialError.stack === 'string'
37+
);
38+
}

test/error.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { isErrorLike } from '../src/js/utils/error';
2+
3+
describe('error', () => {
4+
describe('isErrorLike', () => {
5+
test('returns true for Error object', () => {
6+
expect(isErrorLike(new Error('test'))).toBe(true);
7+
});
8+
9+
test('returns true for ErrorLike object', () => {
10+
expect(isErrorLike({ stack: 'test' })).toBe(true);
11+
});
12+
13+
test('returns false for non object', () => {
14+
expect(isErrorLike('test')).toBe(false);
15+
});
16+
17+
test('returns false for object without stack', () => {
18+
expect(isErrorLike({})).toBe(false);
19+
});
20+
21+
test('returns false for object with non string stack', () => {
22+
expect(isErrorLike({ stack: 1 })).toBe(false);
23+
});
24+
});
25+
});

test/integrations/debugsymbolicator.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,98 @@ describe('Debug Symbolicator Integration', () => {
247247
},
248248
});
249249
});
250+
251+
it('skips first frame (callee) for exception', async () => {
252+
const symbolicatedEvent = await executeIntegrationFor(
253+
{
254+
exception: {
255+
values: [
256+
{
257+
type: 'Error',
258+
value: 'Error: test',
259+
stacktrace: {
260+
frames: mockSentryParsedFrames,
261+
},
262+
},
263+
],
264+
},
265+
},
266+
{
267+
originalException: {
268+
stack: mockRawStack,
269+
framesToPop: 2,
270+
// The current behavior matches https://github.com/getsentry/sentry-javascript/blob/739d904342aaf9327312f409952f14ceff4ae1ab/packages/utils/src/stacktrace.ts#L23
271+
// 2 for first line with the Error message
272+
},
273+
},
274+
);
275+
276+
expect(symbolicatedEvent).toStrictEqual(<Event>{
277+
exception: {
278+
values: [
279+
{
280+
type: 'Error',
281+
value: 'Error: test',
282+
stacktrace: {
283+
frames: [
284+
{
285+
function: 'bar',
286+
filename: '/User/project/node_modules/bar/bar.js',
287+
lineno: 2,
288+
colno: 2,
289+
in_app: false,
290+
},
291+
],
292+
},
293+
},
294+
],
295+
},
296+
});
297+
});
298+
299+
it('skips first frame (callee) for message', async () => {
300+
const symbolicatedEvent = await executeIntegrationFor(
301+
{
302+
threads: {
303+
values: [
304+
{
305+
stacktrace: {
306+
frames: mockSentryParsedFrames,
307+
},
308+
},
309+
],
310+
},
311+
},
312+
{
313+
syntheticException: {
314+
stack: mockRawStack,
315+
framesToPop: 2,
316+
// The current behavior matches https://github.com/getsentry/sentry-javascript/blob/739d904342aaf9327312f409952f14ceff4ae1ab/packages/utils/src/stacktrace.ts#L23
317+
// 2 for first line with the Error message
318+
} as unknown as Error,
319+
},
320+
);
321+
322+
expect(symbolicatedEvent).toStrictEqual(<Event>{
323+
threads: {
324+
values: [
325+
{
326+
stacktrace: {
327+
frames: [
328+
{
329+
function: 'bar',
330+
filename: '/User/project/node_modules/bar/bar.js',
331+
lineno: 2,
332+
colno: 2,
333+
in_app: false,
334+
},
335+
],
336+
},
337+
},
338+
],
339+
},
340+
});
341+
});
250342
});
251343

252344
function executeIntegrationFor(mockedEvent: Event, hint: EventHint): Promise<Event | null> {

0 commit comments

Comments
 (0)