Skip to content

Commit 2c47a8b

Browse files
authored
ref(browser): Replace TracekitStackFrame with Sentry StackFrame (#4523)
- Replaces all usages of `TracekitStackFrame` with the Sentry `StackFrame` and renames the fields appropriately. - Merge Opera regex tests in same function as others to remove duplication - Makes some simplifications because `undefined` is now used instead of `null` - Tightens up some `Error` types
1 parent eddfcd3 commit 2c47a8b

11 files changed

+482
-519
lines changed

packages/browser/src/parsers.ts

+12-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Event, Exception, StackFrame } from '@sentry/types';
22
import { extractExceptionKeysForMessage, isEvent, normalizeToSize } from '@sentry/utils';
33

4-
import { computeStackTrace, StackFrame as TraceKitStackFrame, StackTrace as TraceKitStackTrace } from './tracekit';
4+
import { computeStackTrace, StackTrace as TraceKitStackTrace } from './tracekit';
55

66
const STACKTRACE_LIMIT = 50;
77

@@ -80,15 +80,15 @@ export function eventFromStacktrace(stacktrace: TraceKitStackTrace): Event {
8080
/**
8181
* @hidden
8282
*/
83-
export function prepareFramesForEvent(stack: TraceKitStackFrame[]): StackFrame[] {
84-
if (!stack || !stack.length) {
83+
export function prepareFramesForEvent(stack: StackFrame[]): StackFrame[] {
84+
if (!stack.length) {
8585
return [];
8686
}
8787

8888
let localStack = stack;
8989

90-
const firstFrameFunction = localStack[0].func || '';
91-
const lastFrameFunction = localStack[localStack.length - 1].func || '';
90+
const firstFrameFunction = localStack[0].function || '';
91+
const lastFrameFunction = localStack[localStack.length - 1].function || '';
9292

9393
// If stack starts with one of our API calls, remove it (starts, meaning it's the top of the stack - aka last call)
9494
if (firstFrameFunction.indexOf('captureMessage') !== -1 || firstFrameFunction.indexOf('captureException') !== -1) {
@@ -103,14 +103,12 @@ export function prepareFramesForEvent(stack: TraceKitStackFrame[]): StackFrame[]
103103
// The frame where the crash happened, should be the last entry in the array
104104
return localStack
105105
.slice(0, STACKTRACE_LIMIT)
106-
.map(
107-
(frame: TraceKitStackFrame): StackFrame => ({
108-
colno: frame.column === null ? undefined : frame.column,
109-
filename: frame.url || localStack[0].url,
110-
function: frame.func || '?',
111-
in_app: true,
112-
lineno: frame.line === null ? undefined : frame.line,
113-
}),
114-
)
106+
.map(frame => ({
107+
filename: frame.filename || localStack[0].filename,
108+
function: frame.function || '?',
109+
lineno: frame.lineno,
110+
colno: frame.colno,
111+
in_app: true,
112+
}))
115113
.reverse();
116114
}

packages/browser/src/tracekit.ts

+55-142
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,12 @@
1+
import { StackFrame } from '@sentry/types';
2+
13
/**
24
* This was originally forked from https://github.com/occ/TraceKit, but has since been
35
* largely modified and is now maintained as part of Sentry JS SDK.
46
*/
57

68
/* eslint-disable @typescript-eslint/no-unsafe-member-access, max-lines */
79

8-
/**
9-
* An object representing a single stack frame.
10-
* {Object} StackFrame
11-
* {string} url The JavaScript or HTML file URL.
12-
* {string} func The function name, or empty for anonymous functions (if guessing did not work).
13-
* {string[]?} args The arguments passed to the function, if known.
14-
* {number=} line The line number, if known.
15-
* {number=} column The column number, if known.
16-
* {string[]} context An array of source code lines; the middle element corresponds to the correct line#.
17-
*/
18-
export interface StackFrame {
19-
url: string;
20-
func: string;
21-
line: number | null;
22-
column: number | null;
23-
}
24-
2510
/**
2611
* An object representing a JavaScript stack trace.
2712
* {Object} StackTrace
@@ -32,9 +17,7 @@ export interface StackFrame {
3217
export interface StackTrace {
3318
name: string;
3419
message: string;
35-
mechanism?: string;
3620
stack: StackFrame[];
37-
failed?: boolean;
3821
}
3922

4023
// global reference to slice
@@ -54,11 +37,13 @@ const geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i;
5437
const chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/;
5538
// Based on our own mapping pattern - https://github.com/getsentry/sentry/blob/9f08305e09866c8bd6d0c24f5b0aabdd7dd6c59c/src/sentry/lang/javascript/errormapping.py#L83-L108
5639
const reactMinifiedRegexp = /Minified React error #\d+;/i;
40+
const opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i;
41+
const opera11Regex =
42+
/ line (\d+), column (\d+)\s*(?:in (?:<anonymous function: ([^>]+)>|([^)]+))\(.*\))? in (.*):\s*$/i;
5743

5844
/** JSDoc */
59-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
60-
export function computeStackTrace(ex: any): StackTrace {
61-
let stack = null;
45+
export function computeStackTrace(ex: Error & { framesToPop?: number; stacktrace?: string }): StackTrace {
46+
let frames: StackFrame[] = [];
6247
let popSize = 0;
6348

6449
if (ex) {
@@ -70,50 +55,52 @@ export function computeStackTrace(ex: any): StackTrace {
7055
}
7156

7257
try {
73-
// This must be tried first because Opera 10 *destroys*
74-
// its stacktrace property if you try to access the stack
75-
// property first!!
76-
stack = computeStackTraceFromStacktraceProp(ex);
77-
if (stack) {
78-
return popFrames(stack, popSize);
79-
}
58+
// Access and store the stacktrace property before doing ANYTHING
59+
// else to it because Opera is not very good at providing it
60+
// reliably in other circumstances.
61+
const stacktrace = ex.stacktrace || ex.stack || '';
62+
63+
frames = parseFrames(stacktrace);
8064
} catch (e) {
8165
// no-empty
8266
}
8367

84-
try {
85-
stack = computeStackTraceFromStackProp(ex);
86-
if (stack) {
87-
return popFrames(stack, popSize);
88-
}
89-
} catch (e) {
90-
// no-empty
68+
if (frames.length && popSize > 0) {
69+
frames = frames.slice(popSize);
9170
}
9271

9372
return {
9473
message: extractMessage(ex),
9574
name: ex && ex.name,
96-
stack: [],
97-
failed: true,
75+
stack: frames,
9876
};
9977
}
10078

10179
/** JSDoc */
102-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, complexity
103-
function computeStackTraceFromStackProp(ex: any): StackTrace | null {
104-
if (!ex || !ex.stack) {
105-
return null;
106-
}
107-
108-
const stack = [];
109-
const lines = ex.stack.split('\n');
80+
// eslint-disable-next-line complexity
81+
function parseFrames(stackString: string): StackFrame[] {
82+
const frames: StackFrame[] = [];
83+
const lines = stackString.split('\n');
11084
let isEval;
11185
let submatch;
11286
let parts;
113-
let element;
87+
let element: StackFrame | undefined;
11488

11589
for (const line of lines) {
116-
if ((parts = chrome.exec(line))) {
90+
if ((parts = opera10Regex.exec(line))) {
91+
element = {
92+
filename: parts[2],
93+
function: parts[3] || UNKNOWN_FUNCTION,
94+
lineno: +parts[1],
95+
};
96+
} else if ((parts = opera11Regex.exec(line))) {
97+
element = {
98+
filename: parts[5],
99+
function: parts[3] || parts[4] || UNKNOWN_FUNCTION,
100+
lineno: +parts[1],
101+
colno: +parts[2],
102+
};
103+
} else if ((parts = chrome.exec(line))) {
117104
isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line
118105
if (isEval && (submatch = chromeEval.exec(parts[2]))) {
119106
// throw out eval line/column and use top-most line/column number
@@ -124,20 +111,20 @@ function computeStackTraceFromStackProp(ex: any): StackTrace | null {
124111

125112
// Kamil: One more hack won't hurt us right? Understanding and adding more rules on top of these regexps right now
126113
// would be way too time consuming. (TODO: Rewrite whole RegExp to be more readable)
127-
const [func, url] = extractSafariExtensionDetails(parts[1] || UNKNOWN_FUNCTION, parts[2]);
114+
const [func, filename] = extractSafariExtensionDetails(parts[1] || UNKNOWN_FUNCTION, parts[2]);
128115

129116
element = {
130-
url,
131-
func,
132-
line: parts[3] ? +parts[3] : null,
133-
column: parts[4] ? +parts[4] : null,
117+
filename,
118+
function: func,
119+
lineno: parts[3] ? +parts[3] : undefined,
120+
colno: parts[4] ? +parts[4] : undefined,
134121
};
135122
} else if ((parts = winjs.exec(line))) {
136123
element = {
137-
url: parts[2],
138-
func: parts[1] || UNKNOWN_FUNCTION,
139-
line: +parts[3],
140-
column: parts[4] ? +parts[4] : null,
124+
filename: parts[2],
125+
function: parts[1] || UNKNOWN_FUNCTION,
126+
lineno: +parts[3],
127+
colno: parts[4] ? +parts[4] : undefined,
141128
};
142129
} else if ((parts = gecko.exec(line))) {
143130
isEval = parts[3] && parts[3].indexOf(' > eval') > -1;
@@ -149,86 +136,24 @@ function computeStackTraceFromStackProp(ex: any): StackTrace | null {
149136
parts[5] = ''; // no column when eval
150137
}
151138

152-
let url = parts[3];
139+
let filename = parts[3];
153140
let func = parts[1] || UNKNOWN_FUNCTION;
154-
[func, url] = extractSafariExtensionDetails(func, url);
141+
[func, filename] = extractSafariExtensionDetails(func, filename);
155142

156143
element = {
157-
url,
158-
func,
159-
line: parts[4] ? +parts[4] : null,
160-
column: parts[5] ? +parts[5] : null,
144+
filename,
145+
function: func,
146+
lineno: parts[4] ? +parts[4] : undefined,
147+
colno: parts[5] ? +parts[5] : undefined,
161148
};
162149
} else {
163150
continue;
164151
}
165152

166-
stack.push(element);
167-
}
168-
169-
if (!stack.length) {
170-
return null;
153+
frames.push(element);
171154
}
172155

173-
return {
174-
message: extractMessage(ex),
175-
name: ex.name,
176-
stack,
177-
};
178-
}
179-
180-
/** JSDoc */
181-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
182-
function computeStackTraceFromStacktraceProp(ex: any): StackTrace | null {
183-
if (!ex || !ex.stacktrace) {
184-
return null;
185-
}
186-
// Access and store the stacktrace property before doing ANYTHING
187-
// else to it because Opera is not very good at providing it
188-
// reliably in other circumstances.
189-
const stacktrace = ex.stacktrace;
190-
const opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i;
191-
const opera11Regex =
192-
/ line (\d+), column (\d+)\s*(?:in (?:<anonymous function: ([^>]+)>|([^)]+))\(.*\))? in (.*):\s*$/i;
193-
const lines = stacktrace.split('\n');
194-
const stack = [];
195-
let parts;
196-
197-
for (let line = 0; line < lines.length; line += 2) {
198-
let element = null;
199-
if ((parts = opera10Regex.exec(lines[line]))) {
200-
element = {
201-
url: parts[2],
202-
func: parts[3],
203-
line: +parts[1],
204-
column: null,
205-
};
206-
} else if ((parts = opera11Regex.exec(lines[line]))) {
207-
element = {
208-
url: parts[5],
209-
func: parts[3] || parts[4],
210-
line: +parts[1],
211-
column: +parts[2],
212-
};
213-
}
214-
215-
if (element) {
216-
if (!element.func && element.line) {
217-
element.func = UNKNOWN_FUNCTION;
218-
}
219-
stack.push(element);
220-
}
221-
}
222-
223-
if (!stack.length) {
224-
return null;
225-
}
226-
227-
return {
228-
message: extractMessage(ex),
229-
name: ex.name,
230-
stack,
231-
};
156+
return frames;
232157
}
233158

234159
/**
@@ -251,30 +176,18 @@ function computeStackTraceFromStacktraceProp(ex: any): StackTrace | null {
251176
* Unfortunatelly "just" changing RegExp is too complicated now and making it pass all tests
252177
* and fix this case seems like an impossible, or at least way too time-consuming task.
253178
*/
254-
const extractSafariExtensionDetails = (func: string, url: string): [string, string] => {
179+
const extractSafariExtensionDetails = (func: string, filename: string): [string, string] => {
255180
const isSafariExtension = func.indexOf('safari-extension') !== -1;
256181
const isSafariWebExtension = func.indexOf('safari-web-extension') !== -1;
257182

258183
return isSafariExtension || isSafariWebExtension
259184
? [
260185
func.indexOf('@') !== -1 ? func.split('@')[0] : UNKNOWN_FUNCTION,
261-
isSafariExtension ? `safari-extension:${url}` : `safari-web-extension:${url}`,
186+
isSafariExtension ? `safari-extension:${filename}` : `safari-web-extension:${filename}`,
262187
]
263-
: [func, url];
188+
: [func, filename];
264189
};
265190

266-
/** Remove N number of frames from the stack */
267-
function popFrames(stacktrace: StackTrace, popSize: number): StackTrace {
268-
try {
269-
return {
270-
...stacktrace,
271-
stack: stacktrace.stack.slice(popSize),
272-
};
273-
} catch (e) {
274-
return stacktrace;
275-
}
276-
}
277-
278191
/**
279192
* There are cases where stacktrace.message is an Event object
280193
* https://github.com/getsentry/sentry-javascript/issues/1949

packages/browser/test/unit/parsers.test.ts

+13-13
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ describe('Parsers', () => {
55
describe('removed top frame if its internally reserved word (public API)', () => {
66
it('reserved captureException', () => {
77
const stack = [
8-
{ context: ['x'], column: 1, line: 4, url: 'anything.js', func: 'captureException', args: [] },
9-
{ context: ['x'], column: 1, line: 3, url: 'anything.js', func: 'foo', args: [] },
10-
{ context: ['x'], column: 1, line: 2, url: 'anything.js', func: 'bar', args: [] },
8+
{ colno: 1, lineno: 4, filename: 'anything.js', function: 'captureException' },
9+
{ colno: 1, lineno: 3, filename: 'anything.js', function: 'foo' },
10+
{ colno: 1, lineno: 2, filename: 'anything.js', function: 'bar' },
1111
];
1212

1313
// Should remove `captureException` as its a name considered "internal"
@@ -20,9 +20,9 @@ describe('Parsers', () => {
2020

2121
it('reserved captureMessage', () => {
2222
const stack = [
23-
{ context: ['x'], column: 1, line: 4, url: 'anything.js', func: 'captureMessage', args: [] },
24-
{ context: ['x'], column: 1, line: 3, url: 'anything.js', func: 'foo', args: [] },
25-
{ context: ['x'], column: 1, line: 2, url: 'anything.js', func: 'bar', args: [] },
23+
{ colno: 1, lineno: 4, filename: 'anything.js', function: 'captureMessage' },
24+
{ colno: 1, lineno: 3, filename: 'anything.js', function: 'foo' },
25+
{ colno: 1, lineno: 2, filename: 'anything.js', function: 'bar' },
2626
];
2727

2828
// Should remove `captureMessage` as its a name considered "internal"
@@ -37,9 +37,9 @@ describe('Parsers', () => {
3737
describe('removed bottom frame if its internally reserved word (internal API)', () => {
3838
it('reserved sentryWrapped', () => {
3939
const stack = [
40-
{ context: ['x'], column: 1, line: 3, url: 'anything.js', func: 'foo', args: [] },
41-
{ context: ['x'], column: 1, line: 2, url: 'anything.js', func: 'bar', args: [] },
42-
{ context: ['x'], column: 1, line: 1, url: 'anything.js', func: 'sentryWrapped', args: [] },
40+
{ colno: 1, lineno: 3, filename: 'anything.js', function: 'foo' },
41+
{ colno: 1, lineno: 2, filename: 'anything.js', function: 'bar' },
42+
{ colno: 1, lineno: 1, filename: 'anything.js', function: 'sentryWrapped' },
4343
];
4444

4545
// Should remove `sentryWrapped` as its a name considered "internal"
@@ -53,10 +53,10 @@ describe('Parsers', () => {
5353

5454
it('removed top and bottom frame if they are internally reserved words', () => {
5555
const stack = [
56-
{ context: ['x'], column: 1, line: 4, url: 'anything.js', func: 'captureMessage', args: [] },
57-
{ context: ['x'], column: 1, line: 3, url: 'anything.js', func: 'foo', args: [] },
58-
{ context: ['x'], column: 1, line: 2, url: 'anything.js', func: 'bar', args: [] },
59-
{ context: ['x'], column: 1, line: 1, url: 'anything.js', func: 'sentryWrapped', args: [] },
56+
{ colno: 1, lineno: 4, filename: 'anything.js', function: 'captureMessage' },
57+
{ colno: 1, lineno: 3, filename: 'anything.js', function: 'foo' },
58+
{ colno: 1, lineno: 2, filename: 'anything.js', function: 'bar' },
59+
{ colno: 1, lineno: 1, filename: 'anything.js', function: 'sentryWrapped' },
6060
];
6161

6262
// Should remove `captureMessage` and `sentryWrapped` as its a name considered "internal"

0 commit comments

Comments
 (0)