forked from getsentry/sentry-javascript
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtracekit.ts
314 lines (267 loc) · 9.17 KB
/
tracekit.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
/**
* This was originally forked from https://github.com/occ/TraceKit, but has since been
* largely modified and is now maintained as part of Sentry JS SDK.
*/
/* eslint-disable @typescript-eslint/no-unsafe-member-access, max-lines */
/**
* An object representing a single stack frame.
* {Object} StackFrame
* {string} url The JavaScript or HTML file URL.
* {string} func The function name, or empty for anonymous functions (if guessing did not work).
* {string[]?} args The arguments passed to the function, if known.
* {number=} line The line number, if known.
* {number=} column The column number, if known.
* {string[]} context An array of source code lines; the middle element corresponds to the correct line#.
*/
export interface StackFrame {
url: string;
func: string;
args: string[];
line: number | null;
column: number | null;
}
/**
* An object representing a JavaScript stack trace.
* {Object} StackTrace
* {string} name The name of the thrown exception.
* {string} message The exception error message.
* {TraceKit.StackFrame[]} stack An array of stack frames.
*/
export interface StackTrace {
name: string;
message: string;
mechanism?: string;
stack: StackFrame[];
failed?: boolean;
}
// global reference to slice
const UNKNOWN_FUNCTION = '?';
type StackLineParser = (line: string) => StackFrame | undefined;
// Chromium based browsers: Chrome, Brave, new Opera, new Edge
const chrome =
/^\s*at (?:(.*?) ?\((?:address at )?)?((?:file|https?|blob|chrome-extension|address|native|eval|webpack|<anonymous>|[-a-z]+:|.*bundle|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i;
const chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/;
const chromeParser: StackLineParser = line => {
let parts;
let submatch;
if ((parts = chrome.exec(line))) {
const isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line
const isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line
if (isEval && (submatch = chromeEval.exec(parts[2]))) {
// throw out eval line/column and use top-most line/column number
parts[2] = submatch[1]; // url
parts[3] = submatch[2]; // line
parts[4] = submatch[3]; // column
}
// Kamil: One more hack won't hurt us right? Understanding and adding more rules on top of these regexps right now
// would be way too time consuming. (TODO: Rewrite whole RegExp to be more readable)
const [func, url] = extractSafariExtensionDetails(parts[1] || UNKNOWN_FUNCTION, parts[2]);
return {
url,
func,
args: isNative ? [parts[2]] : [],
line: parts[3] ? +parts[3] : null,
column: parts[4] ? +parts[4] : null,
};
}
return undefined;
};
const winjs =
/^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i;
const winjsParser: StackLineParser = line => {
let parts;
if ((parts = winjs.exec(line))) {
return {
url: parts[2],
func: parts[1] || UNKNOWN_FUNCTION,
args: [],
line: +parts[3],
column: parts[4] ? +parts[4] : null,
};
}
return undefined;
};
// gecko regex: `(?:bundle|\d+\.js)`: `bundle` is for react native, `\d+\.js` also but specifically for ram bundles because it
// generates filenames without a prefix like `file://` the filenames in the stacktrace are just 42.js
// We need this specific case for now because we want no other regex to match.
const gecko =
/^\s*(.*?)(?:\((.*?)\))?(?:^|@)?((?:file|https?|blob|chrome|webpack|resource|moz-extension|capacitor).*?:\/.*?|\[native code\]|[^@]*(?:bundle|\d+\.js)|\/[\w\-. /=]+)(?::(\d+))?(?::(\d+))?\s*$/i;
const geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i;
const geckoParser: StackLineParser = line => {
let parts;
let submatch;
if ((parts = gecko.exec(line))) {
const isEval = parts[3] && parts[3].indexOf(' > eval') > -1;
if (isEval && (submatch = geckoEval.exec(parts[3]))) {
// throw out eval line/column and use top-most line number
parts[1] = parts[1] || `eval`;
parts[3] = submatch[1];
parts[4] = submatch[2];
parts[5] = ''; // no column when eval
}
// else if (i === 0 && !parts[5] && ex.columnNumber !== void 0) {
// FireFox uses this awesome columnNumber property for its top frame
// Also note, Firefox's column number is 0-based and everything else expects 1-based,
// so adding 1
// NOTE: this hack doesn't work if top-most frame is eval
// stack[0].column = (ex.columnNumber as number) + 1;
// }
let url = parts[3];
let func = parts[1] || UNKNOWN_FUNCTION;
[func, url] = extractSafariExtensionDetails(func, url);
return {
url,
func,
args: parts[2] ? parts[2].split(',') : [],
line: parts[4] ? +parts[4] : null,
column: parts[5] ? +parts[5] : null,
};
}
return undefined;
};
const opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i;
const opera10Parser: StackLineParser = line => {
let parts;
if ((parts = opera10Regex.exec(line))) {
return {
url: parts[2],
func: parts[3],
args: [],
line: +parts[1],
column: null,
};
}
return undefined;
};
const opera11Regex =
/ line (\d+), column (\d+)\s*(?:in (?:<anonymous function: ([^>]+)>|([^)]+))\((.*)\))? in (.*):\s*$/i;
const opera11Parser: StackLineParser = line => {
let parts;
if ((parts = opera11Regex.exec(line))) {
return {
url: parts[6],
func: parts[3] || parts[4],
args: parts[5] ? parts[5].split(',') : [],
line: +parts[1],
column: +parts[2],
};
}
return undefined;
};
const parsers = [opera10Parser, opera11Parser, chromeParser, winjsParser, geckoParser];
// Based on our own mapping pattern - https://github.com/getsentry/sentry/blob/9f08305e09866c8bd6d0c24f5b0aabdd7dd6c59c/src/sentry/lang/javascript/errormapping.py#L83-L108
const reactMinifiedRegexp = /Minified React error #\d+;/i;
/** JSDoc */
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
export function computeStackTrace(ex: any): StackTrace {
let stack = null;
let popSize = 0;
if (ex) {
if (typeof ex.framesToPop === 'number') {
popSize = ex.framesToPop;
} else if (reactMinifiedRegexp.test(ex.message)) {
popSize = 1;
}
}
try {
stack = computeStackTraceFromStackProp(ex);
if (stack) {
return popFrames(stack, popSize);
}
} catch (e) {
// no-empty
}
return {
message: extractMessage(ex),
name: ex && ex.name,
stack: [],
failed: true,
};
}
/** JSDoc */
// eslint-disable-next-line @typescript-eslint/no-explicit-any, complexity
function computeStackTraceFromStackProp(ex: any): StackTrace | null {
const stacktrace = ex?.stacktrace || ex?.stack;
if (!stacktrace) {
return null;
}
const stack = [];
const lines = stacktrace.split('\n');
for (const line of lines) {
for (const parser of parsers) {
const result = parser(line);
if (result) {
if (!result.func && result.line) {
result.func = UNKNOWN_FUNCTION;
}
stack.push(result);
break;
}
}
}
if (!stack.length) {
return null;
}
return {
message: extractMessage(ex),
name: ex.name,
stack,
};
}
/**
* Safari web extensions, starting version unknown, can produce "frames-only" stacktraces.
* What it means, is that instead of format like:
*
* Error: wat
* at function@url:row:col
* at function@url:row:col
* at function@url:row:col
*
* it produces something like:
*
* function@url:row:col
* function@url:row:col
* function@url:row:col
*
* Because of that, it won't be captured by `chrome` RegExp and will fall into `Gecko` branch.
* This function is extracted so that we can use it in both places without duplicating the logic.
* Unfortunatelly "just" changing RegExp is too complicated now and making it pass all tests
* and fix this case seems like an impossible, or at least way too time-consuming task.
*/
const extractSafariExtensionDetails = (func: string, url: string): [string, string] => {
const isSafariExtension = func.indexOf('safari-extension') !== -1;
const isSafariWebExtension = func.indexOf('safari-web-extension') !== -1;
return isSafariExtension || isSafariWebExtension
? [
func.indexOf('@') !== -1 ? func.split('@')[0] : UNKNOWN_FUNCTION,
isSafariExtension ? `safari-extension:${url}` : `safari-web-extension:${url}`,
]
: [func, url];
};
/** Remove N number of frames from the stack */
function popFrames(stacktrace: StackTrace, popSize: number): StackTrace {
try {
return {
...stacktrace,
stack: stacktrace.stack.slice(popSize),
};
} catch (e) {
return stacktrace;
}
}
/**
* There are cases where stacktrace.message is an Event object
* https://github.com/getsentry/sentry-javascript/issues/1949
* In this specific case we try to extract stacktrace.message.error.message
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function extractMessage(ex: any): string {
const message = ex && ex.message;
if (!message) {
return 'No error message';
}
if (message.error && typeof message.error.message === 'string') {
return message.error.message;
}
return message;
}