-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
Copy pathdevErrorSymbolicationEventProcessor.ts
157 lines (139 loc) · 5.44 KB
/
devErrorSymbolicationEventProcessor.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
import type { Event, EventHint } from '@sentry/types';
import type { StackFrame } from 'stacktrace-parser';
import * as stackTraceParser from 'stacktrace-parser';
type OriginalStackFrameResponse = {
originalStackFrame: StackFrame;
originalCodeFrame: string | null;
sourcePackage?: string;
};
async function resolveStackFrame(
frame: StackFrame,
error: Error,
): Promise<{ originalCodeFrame: string | null; originalStackFrame: StackFrame | null } | null> {
try {
if (!(frame.file?.startsWith('webpack-internal:') || frame.file?.startsWith('file:'))) {
return null;
}
const params = new URLSearchParams();
params.append('isServer', String(false)); // doesn't matter since it is overwritten by isAppDirectory
params.append('isEdgeServer', String(false)); // doesn't matter since it is overwritten by isAppDirectory
params.append('isAppDirectory', String(true)); // will force server to do more thorough checking
params.append('errorMessage', error.toString());
Object.keys(frame).forEach(key => {
params.append(key, (frame[key as keyof typeof frame] ?? '').toString());
});
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3000);
const res = await fetch(
`${
// eslint-disable-next-line no-restricted-globals
typeof window === 'undefined' ? 'http://localhost:3000' : '' // TODO: handle the case where users define a different port
}/__nextjs_original-stack-frame?${params.toString()}`,
{
signal: controller.signal,
},
).finally(() => {
clearTimeout(timer);
});
if (!res.ok || res.status === 204) {
return null;
}
const body: OriginalStackFrameResponse = await res.json();
return {
originalCodeFrame: body.originalCodeFrame,
originalStackFrame: body.originalStackFrame,
};
} catch (e) {
return null;
}
}
function parseOriginalCodeFrame(codeFrame: string): {
contextLine: string | undefined;
preContextLines: string[];
postContextLines: string[];
} {
const preProcessedLines = codeFrame
// Remove ASCII control characters that are used for syntax highlighting
.replace(
// eslint-disable-next-line no-control-regex
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, // https://stackoverflow.com/a/29497680
'',
)
.split('\n')
// Remove line that is supposed to indicate where the error happened
.filter(line => !line.match(/^\s*\|/))
// Find the error line
.map(line => ({
line,
isErrorLine: !!line.match(/^>/),
}))
// Remove the leading part that is just for prettier output
.map(lineObj => ({
...lineObj,
line: lineObj.line.replace(/^.*\|/, ''),
}));
const preContextLines = [];
let contextLine: string | undefined = undefined;
const postContextLines = [];
let reachedContextLine = false;
for (const preProcessedLine of preProcessedLines) {
if (preProcessedLine.isErrorLine) {
contextLine = preProcessedLine.line;
reachedContextLine = true;
} else if (reachedContextLine) {
postContextLines.push(preProcessedLine.line);
} else {
preContextLines.push(preProcessedLine.line);
}
}
return {
contextLine,
preContextLines,
postContextLines,
};
}
/**
* Event processor that will symbolicate errors by using the webpack/nextjs dev server that is used to show stack traces
* in the dev overlay.
*/
export async function devErrorSymbolicationEventProcessor(event: Event, hint: EventHint): Promise<Event | null> {
// Due to changes across Next.js versions, there are a million things that can go wrong here so we just try-catch the // entire event processor.Symbolicated stack traces are just a nice to have.
try {
if (hint.originalException && hint.originalException instanceof Error && hint.originalException.stack) {
const frames = stackTraceParser.parse(hint.originalException.stack);
const resolvedFrames = await Promise.all(
frames.map(frame => resolveStackFrame(frame, hint.originalException as Error)),
);
if (event.exception?.values?.[0].stacktrace?.frames) {
event.exception.values[0].stacktrace.frames = event.exception.values[0].stacktrace.frames.map(
(frame, i, frames) => {
const resolvedFrame = resolvedFrames[frames.length - 1 - i];
if (!resolvedFrame || !resolvedFrame.originalStackFrame || !resolvedFrame.originalCodeFrame) {
return {
...frame,
platform: frame.filename?.startsWith('node:internal') ? 'nodejs' : undefined, // simple hack that will prevent a source mapping error from showing up
in_app: false,
};
}
const { contextLine, preContextLines, postContextLines } = parseOriginalCodeFrame(
resolvedFrame.originalCodeFrame,
);
return {
...frame,
pre_context: preContextLines,
context_line: contextLine,
post_context: postContextLines,
function: resolvedFrame.originalStackFrame.methodName,
filename: resolvedFrame.originalStackFrame.file || undefined,
lineno: resolvedFrame.originalStackFrame.lineNumber || undefined,
colno: resolvedFrame.originalStackFrame.column || undefined,
};
},
);
}
}
} catch (e) {
return event;
}
return event;
}