-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathstack-parser.ts
More file actions
167 lines (143 loc) · 4.85 KB
/
stack-parser.ts
File metadata and controls
167 lines (143 loc) · 4.85 KB
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
import type { StackFrame } from 'error-stack-parser';
import ErrorStackParser from 'error-stack-parser';
import type { BacktraceFrame, SourceCodeLine } from '@hawk.so/types';
import fetchTimer from './fetch-timer';
/**
* This module prepares parsed backtrace
*/
export class StackParser {
/**
* Prevents loading one file several times
* name -> content
*/
private sourceFilesCache: {[fileName: string]: Promise<string>} = {};
/**
* Parse Error stack string and return useful information about an Error
*
* @param error - event from which to get backtrace
*/
public async parse(error: Error): Promise<BacktraceFrame[]> {
const stackParsed = ErrorStackParser.parse(error) as StackFrame[];
return Promise.all(stackParsed.map(async (frame) => {
const sourceCode = await this.extractSourceCode(frame);
const file = frame.fileName !== null && frame.fileName !== undefined ? frame.fileName : '';
const line = frame.lineNumber !== null && frame.lineNumber !== undefined ? frame.lineNumber : 0;
return {
file,
line,
column: frame.columnNumber,
sourceCode: sourceCode !== null ? sourceCode : undefined,
function: frame.functionName,
arguments: frame.args,
};
}));
}
/**
* Extract 5 lines below and above the error's line
*
* @param {StackFrame} frame — information about backtrace item
*/
private async extractSourceCode(frame: StackFrame): Promise<SourceCodeLine[] | null> {
const minifiedSourceCodeThreshold = 200;
try {
if (!frame.fileName) {
return null;
}
if (!this.isValidUrl(frame.fileName)) {
return null;
}
/**
* If error occurred in large column number, the script probably minified
* Skip minified bundles — they will be processed if user enabled source-maps tracking
*/
if (frame.columnNumber && frame.columnNumber > minifiedSourceCodeThreshold) {
return null;
}
const file = await this.loadSourceFile(frame.fileName);
if (!file) {
return null;
}
const lines = file.split('\n');
const actualLineNumber = frame.lineNumber ? frame.lineNumber - 1 : 0;
const linesCollectCount = 5;
const lineFrom = Math.max(0, actualLineNumber - linesCollectCount);
const lineTo = Math.min(lines.length - 1, actualLineNumber + linesCollectCount + 1);
const sourceCodeLines: SourceCodeLine[] = [];
let extractedLineIndex = 1;
/**
* In some cases column number of the error stack trace frame would be less then 200, but source code is minified
* For this cases we need to check, that all of the lines to collect have length less than 200 too
*/
lines.slice(lineFrom, lineTo).forEach((lineToCheck) => {
if (lineToCheck.length > minifiedSourceCodeThreshold) {
return null;
} else {
sourceCodeLines.push({
line: lineFrom + extractedLineIndex,
content: lineToCheck,
});
extractedLineIndex += 1;
}
});
return sourceCodeLines;
} catch (e) {
console.warn('Hawk JS SDK: Can not extract source code. Please, report this issue: https://github.com/codex-team/hawk.javascript/issues/new', e);
return null;
}
}
/**
* Check if string is a valid URL
*
* @param string - string with URL to check
*/
private isValidUrl(string: string): boolean {
try {
return !!new URL(string);
} catch (_) {
return false;
}
}
/**
* Downloads source file
*
* @param fileName - name of file to download
*/
private async loadSourceFile(fileName: string): Promise<string | null> {
if (this.sourceFilesCache[fileName] !== undefined) {
return this.sourceFilesCache[fileName];
}
try {
/**
* Try to load source file.
* Wait for maximum 2 sec to skip loading big files.
*/
this.sourceFilesCache[fileName] = fetchTimer(fileName, 2000)
.then((response) => response.text())
.catch((error) => {
/**
* Remove failed promise from cache to allow retry
*/
delete this.sourceFilesCache[fileName];
/**
* Re-throw error so it can be caught by try-catch
*/
throw error;
});
/**
* Dealloc cache when it collects more that 10 files
*/
if (Object.keys(this.sourceFilesCache).length > 10) {
const alone = this.sourceFilesCache[fileName];
this.sourceFilesCache = {};
this.sourceFilesCache[fileName] = alone;
}
return await this.sourceFilesCache[fileName];
} catch (error) {
/**
* Ensure failed promise is removed from cache
*/
delete this.sourceFilesCache[fileName]; // log('Can not load source file. Skipping...');
return null;
}
}
}