-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathdevErrorSymbolicationEventProcessor.ts
184 lines (161 loc) · 6.34 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
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
import { suppressTracing } from '@sentry/core';
import type { Event, EventHint } from '@sentry/types';
import { GLOBAL_OBJ } from '@sentry/utils';
import type { StackFrame } from 'stacktrace-parser';
import * as stackTraceParser from 'stacktrace-parser';
type OriginalStackFrameResponse = {
originalStackFrame: StackFrame;
originalCodeFrame: string | null;
sourcePackage?: string;
};
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
_sentryBasePath?: 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());
});
let basePath = process.env._sentryBasePath ?? globalWithInjectedValues._sentryBasePath ?? '';
// Prefix the basepath with a slash if it doesn't have one
if (basePath !== '' && !basePath.match(/^\//)) {
basePath = `/${basePath}`;
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3000);
const res = await suppressTracing(() =>
fetch(
`${
// eslint-disable-next-line no-restricted-globals
typeof window === 'undefined' ? 'http://localhost:3000' : '' // TODO: handle the case where users define a different port
}${basePath}/__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> {
// Filter out spans for requests resolving source maps for stack frames in dev mode
if (event.type === 'transaction') {
event.spans = event.spans?.filter(span => {
const httpUrlAttribute: unknown = span.data?.['http.url'];
if (typeof httpUrlAttribute === 'string') {
return !httpUrlAttribute.includes('__nextjs_original-stack-frame');
}
return true;
});
}
// 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;
}