forked from getsentry/sentry-javascript
-
Notifications
You must be signed in to change notification settings - Fork 0
/
stacktrace.ts
200 lines (168 loc) · 6.23 KB
/
stacktrace.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
import { StackFrame, StackLineParser, StackLineParserFn, StackParser } from '@sentry/types';
const STACKTRACE_LIMIT = 50;
/**
* Creates a stack parser with the supplied line parsers
*
* StackFrames are returned in the correct order for Sentry Exception
* frames and with Sentry SDK internal frames removed from the top and bottom
*
*/
export function createStackParser(...parsers: StackLineParser[]): StackParser {
const sortedParsers = parsers.sort((a, b) => a[0] - b[0]).map(p => p[1]);
return (stack: string, skipFirst: number = 0): StackFrame[] => {
const frames: StackFrame[] = [];
for (const line of stack.split('\n').slice(skipFirst)) {
// Ignore lines over 1kb as they are unlikely to be stack frames.
// Many of the regular expressions use backtracking which results in run time that increases exponentially with
// input size. Huge strings can result in hangs/Denial of Service:
// https://github.com/getsentry/sentry-javascript/issues/2286
if (line.length > 1024) {
continue;
}
// https://github.com/getsentry/sentry-javascript/issues/5459
// Remove webpack (error: *) wrappers
const cleanedLine = line.replace(/\(error: (.*)\)/, '$1');
for (const parser of sortedParsers) {
const frame = parser(cleanedLine);
if (frame) {
frames.push(frame);
break;
}
}
}
return stripSentryFramesAndReverse(frames);
};
}
/**
* Gets a stack parser implementation from Options.stackParser
* @see Options
*
* If options contains an array of line parsers, it is converted into a parser
*/
export function stackParserFromStackParserOptions(stackParser: StackParser | StackLineParser[]): StackParser {
if (Array.isArray(stackParser)) {
return createStackParser(...stackParser);
}
return stackParser;
}
/**
* @hidden
*/
export function stripSentryFramesAndReverse(stack: StackFrame[]): StackFrame[] {
if (!stack.length) {
return [];
}
let localStack = stack;
const firstFrameFunction = localStack[0].function || '';
const lastFrameFunction = localStack[localStack.length - 1].function || '';
// If stack starts with one of our API calls, remove it (starts, meaning it's the top of the stack - aka last call)
if (firstFrameFunction.indexOf('captureMessage') !== -1 || firstFrameFunction.indexOf('captureException') !== -1) {
localStack = localStack.slice(1);
}
// 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)
if (lastFrameFunction.indexOf('sentryWrapped') !== -1) {
localStack = localStack.slice(0, -1);
}
// The frame where the crash happened, should be the last entry in the array
return localStack
.slice(0, STACKTRACE_LIMIT)
.map(frame => ({
...frame,
filename: frame.filename || localStack[0].filename,
function: frame.function || '?',
}))
.reverse();
}
const defaultFunctionName = '<anonymous>';
/**
* Safely extract function name from itself
*/
export function getFunctionName(fn: unknown): string {
try {
if (!fn || typeof fn !== 'function') {
return defaultFunctionName;
}
return fn.name || defaultFunctionName;
} catch (e) {
// Just accessing custom props in some Selenium environments
// can cause a "Permission denied" exception (see raven-js#495).
return defaultFunctionName;
}
}
type GetModuleFn = (filename: string | undefined) => string | undefined;
// eslint-disable-next-line complexity
function node(getModule?: GetModuleFn): StackLineParserFn {
const FILENAME_MATCH = /^\s*[-]{4,}$/;
const FULL_MATCH = /at (?:async )?(?:(.+?)\s+\()?(?:(.+):(\d+):(\d+)?|([^)]+))\)?/;
// eslint-disable-next-line complexity
return (line: string) => {
if (line.match(FILENAME_MATCH)) {
return {
filename: line,
};
}
const lineMatch = line.match(FULL_MATCH);
if (!lineMatch) {
return undefined;
}
let object: string | undefined;
let method: string | undefined;
let functionName: string | undefined;
let typeName: string | undefined;
let methodName: string | undefined;
if (lineMatch[1]) {
functionName = lineMatch[1];
let methodStart = functionName.lastIndexOf('.');
if (functionName[methodStart - 1] === '.') {
methodStart--;
}
if (methodStart > 0) {
object = functionName.substr(0, methodStart);
method = functionName.substr(methodStart + 1);
const objectEnd = object.indexOf('.Module');
if (objectEnd > 0) {
functionName = functionName.substr(objectEnd + 1);
object = object.substr(0, objectEnd);
}
}
typeName = undefined;
}
if (method) {
typeName = object;
methodName = method;
}
if (method === '<anonymous>') {
methodName = undefined;
functionName = undefined;
}
if (functionName === undefined) {
methodName = methodName || '<anonymous>';
functionName = typeName ? `${typeName}.${methodName}` : methodName;
}
const filename = lineMatch[2]?.startsWith('file://') ? lineMatch[2].substr(7) : lineMatch[2];
const isNative = lineMatch[5] === 'native';
const isInternal =
isNative || (filename && !filename.startsWith('/') && !filename.startsWith('.') && filename.indexOf(':\\') !== 1);
// in_app is all that's not an internal Node function or a module within node_modules
// note that isNative appears to return true even for node core libraries
// see https://github.com/getsentry/raven-node/issues/176
const in_app = !isInternal && filename !== undefined && !filename.includes('node_modules/');
return {
filename,
module: getModule?.(filename),
function: functionName,
lineno: parseInt(lineMatch[3], 10) || undefined,
colno: parseInt(lineMatch[4], 10) || undefined,
in_app,
};
};
}
/**
* Node.js stack line parser
*
* This is in @sentry/utils so it can be used from the Electron SDK in the browser for when `nodeIntegration == true`.
* This allows it to be used without referencing or importing any node specific code which causes bundlers to complain
*/
export function nodeStackLineParser(getModule?: GetModuleFn): StackLineParser {
return [90, node(getModule)];
}