Skip to content

Commit 6196bc5

Browse files
authored
fix(core): Filter internal API frames for synthetic frames (#8710)
As well as fixes the ordering of operations and removes some redundant code. The stack should be trimmed _after_ filtering. Fixes #8396
1 parent 462bb24 commit 6196bc5

File tree

2 files changed

+65
-6
lines changed

2 files changed

+65
-6
lines changed

packages/utils/src/stacktrace.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { node } from './node-stack-trace';
66
const STACKTRACE_FRAME_LIMIT = 50;
77
// Used to sanitize webpack (error: *) wrapped stack errors
88
const WEBPACK_ERROR_REGEXP = /\(error: (.*)\)/;
9+
const STRIP_FRAME_REGEXP = /captureMessage|captureException/;
910

1011
/**
1112
* Creates a stack parser with the supplied line parsers
@@ -83,24 +84,34 @@ export function stripSentryFramesAndReverse(stack: ReadonlyArray<StackFrame>): S
8384
return [];
8485
}
8586

86-
const localStack = stack.slice(0, STACKTRACE_FRAME_LIMIT);
87+
const localStack = Array.from(stack);
8788

88-
const lastFrameFunction = localStack[localStack.length - 1].function;
8989
// If stack starts with one of our API calls, remove it (starts, meaning it's the top of the stack - aka last call)
90-
if (lastFrameFunction && /sentryWrapped/.test(lastFrameFunction)) {
90+
if (/sentryWrapped/.test(localStack[localStack.length - 1].function || '')) {
9191
localStack.pop();
9292
}
9393

9494
// Reversing in the middle of the procedure allows us to just pop the values off the stack
9595
localStack.reverse();
9696

97-
const firstFrameFunction = localStack[localStack.length - 1].function;
9897
// If stack ends with one of our internal API calls, remove it (ends, meaning it's the bottom of the stack - aka top-most call)
99-
if (firstFrameFunction && /captureMessage|captureException/.test(firstFrameFunction)) {
98+
if (STRIP_FRAME_REGEXP.test(localStack[localStack.length - 1].function || '')) {
10099
localStack.pop();
100+
101+
// When using synthetic events, we will have a 2 levels deep stack, as `new Error('Sentry syntheticException')`
102+
// is produced within the hub itself, making it:
103+
//
104+
// Sentry.captureException()
105+
// getCurrentHub().captureException()
106+
//
107+
// instead of just the top `Sentry` call itself.
108+
// This forces us to possibly strip an additional frame in the exact same was as above.
109+
if (STRIP_FRAME_REGEXP.test(localStack[localStack.length - 1].function || '')) {
110+
localStack.pop();
111+
}
101112
}
102113

103-
return localStack.map(frame => ({
114+
return localStack.slice(0, STACKTRACE_FRAME_LIMIT).map(frame => ({
104115
...frame,
105116
filename: frame.filename || localStack[localStack.length - 1].filename,
106117
function: frame.function || '?',

packages/utils/test/stacktrace.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,34 @@ describe('Stacktrace', () => {
3232
expect(frames[0].function).toBe('bar');
3333
expect(frames[1].function).toBe('foo');
3434
});
35+
36+
it('remove two occurences if they are present', () => {
37+
const exceptionStack = [
38+
{ colno: 1, lineno: 4, filename: 'anything.js', function: 'captureException' },
39+
{ colno: 1, lineno: 4, filename: 'anything.js', function: 'captureException' },
40+
{ colno: 1, lineno: 3, filename: 'anything.js', function: 'foo' },
41+
{ colno: 1, lineno: 2, filename: 'anything.js', function: 'bar' },
42+
];
43+
44+
const exceptionFrames = stripSentryFramesAndReverse(exceptionStack);
45+
46+
expect(exceptionFrames.length).toBe(2);
47+
expect(exceptionFrames[0].function).toBe('bar');
48+
expect(exceptionFrames[1].function).toBe('foo');
49+
50+
const messageStack = [
51+
{ colno: 1, lineno: 4, filename: 'anything.js', function: 'captureMessage' },
52+
{ colno: 1, lineno: 4, filename: 'anything.js', function: 'captureMessage' },
53+
{ colno: 1, lineno: 3, filename: 'anything.js', function: 'foo' },
54+
{ colno: 1, lineno: 2, filename: 'anything.js', function: 'bar' },
55+
];
56+
57+
const messageFrames = stripSentryFramesAndReverse(messageStack);
58+
59+
expect(messageFrames.length).toBe(2);
60+
expect(messageFrames[0].function).toBe('bar');
61+
expect(messageFrames[1].function).toBe('foo');
62+
});
3563
});
3664

3765
describe('removed bottom frame if its internally reserved word (internal API)', () => {
@@ -53,6 +81,7 @@ describe('Stacktrace', () => {
5381

5482
it('removed top and bottom frame if they are internally reserved words', () => {
5583
const stack = [
84+
{ colno: 1, lineno: 4, filename: 'anything.js', function: 'captureMessage' },
5685
{ colno: 1, lineno: 4, filename: 'anything.js', function: 'captureMessage' },
5786
{ colno: 1, lineno: 3, filename: 'anything.js', function: 'foo' },
5887
{ colno: 1, lineno: 2, filename: 'anything.js', function: 'bar' },
@@ -66,6 +95,25 @@ describe('Stacktrace', () => {
6695
expect(frames[0].function).toBe('bar');
6796
expect(frames[1].function).toBe('foo');
6897
});
98+
99+
it('applies frames limit after the stripping, not before', () => {
100+
const stack = Array.from({ length: 55 }).map((_, i) => {
101+
return { colno: 1, lineno: 4, filename: 'anything.js', function: `${i}` };
102+
});
103+
104+
stack.unshift({ colno: 1, lineno: 4, filename: 'anything.js', function: 'captureMessage' });
105+
stack.unshift({ colno: 1, lineno: 4, filename: 'anything.js', function: 'captureMessage' });
106+
stack.push({ colno: 1, lineno: 4, filename: 'anything.js', function: 'sentryWrapped' });
107+
108+
// Should remove 2x `captureMessage`, `sentryWrapped`, and then limit frames to default 50.
109+
const frames = stripSentryFramesAndReverse(stack);
110+
111+
expect(frames.length).toBe(50);
112+
113+
// Frames are named 0-54, thus after reversal and trimming, we should have frames 54-5, 50 in total.
114+
expect(frames[0].function).toBe('54');
115+
expect(frames[49].function).toBe('5');
116+
});
69117
});
70118
});
71119

0 commit comments

Comments
 (0)