Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit a7d0f63

Browse files
authored
Log uncaught source-mapped errors to the console (#446)
Previously, errors _thrown_ using the `MF-Experimental-Error-Stack` header were only shown in a pretty-error page. This PR logs those errors with source-maps applied to the console too, using the `source-map-support` package. This simplifies our code, and also means we don't need to touch internal Youch methods, as the errors we pass to Youch are already source-mapped.
1 parent 5ecaae2 commit a7d0f63

File tree

7 files changed

+315
-139
lines changed

7 files changed

+315
-139
lines changed

package-lock.json

Lines changed: 50 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/tre/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"glob-to-regexp": "^0.4.1",
3636
"http-cache-semantics": "^4.1.0",
3737
"kleur": "^4.1.5",
38-
"source-map": "^0.7.4",
38+
"source-map-support": "0.5.21",
3939
"stoppable": "^1.1.0",
4040
"undici": "^5.12.0",
4141
"workerd": "^1.20221111.5",
@@ -49,6 +49,7 @@
4949
"@types/estree": "^1.0.0",
5050
"@types/glob-to-regexp": "^0.4.1",
5151
"@types/http-cache-semantics": "^4.0.1",
52+
"@types/source-map-support": "^0.5.6",
5253
"@types/stoppable": "^1.1.1",
5354
"@types/ws": "^8.5.3"
5455
},

packages/tre/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,11 @@ export class Miniflare {
360360
const workerSrcOpts = this.#workerOpts.map<SourceOptions>(
361361
({ core }) => core
362362
);
363-
response = await handlePrettyErrorRequest(workerSrcOpts, request);
363+
response = await handlePrettyErrorRequest(
364+
this.#log,
365+
workerSrcOpts,
366+
request
367+
);
364368
} else {
365369
// TODO: check for proxying/outbound fetch header first (with plans for fetch mocking)
366370
response = await this.#handleLoopbackPlugins(request, url);
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Lifted from `node-stack-trace`:
2+
// https://github.com/felixge/node-stack-trace/blob/4c41a4526e74470179b3b6dd5d75191ca8c56c17/index.js
3+
// Ideally, we'd just use this package as-is, but it has a strict
4+
// `engines.node == 16` constraint in its `package.json`. There's a PR open to
5+
// fix this (https://github.com/felixge/node-stack-trace/pull/39), but it's been
6+
// open for a while. As soon as it's merged, we should just depend on it.
7+
8+
/*!
9+
* Copyright (c) 2011 Felix Geisendörfer ([email protected])
10+
*
11+
* Permission is hereby granted, free of charge, to any person obtaining a copy
12+
* of this software and associated documentation files (the "Software"), to deal
13+
* in the Software without restriction, including without limitation the rights
14+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15+
* copies of the Software, and to permit persons to whom the Software is
16+
* furnished to do so, subject to the following conditions:
17+
*
18+
* The above copyright notice and this permission notice shall be included in
19+
* all copies or substantial portions of the Software.
20+
*
21+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27+
* THE SOFTWARE.
28+
*/
29+
30+
export function parseStack(stack: string): CallSite[] {
31+
return stack
32+
.split("\n")
33+
.slice(1)
34+
.map(parseCallSite)
35+
.filter((site): site is CallSite => site !== undefined);
36+
}
37+
38+
function parseCallSite(line: string): CallSite | undefined {
39+
const lineMatch = line.match(
40+
/at (?:(.+?)\s+\()?(?:(.+?):(\d+)(?::(\d+))?|([^)]+))\)?/
41+
);
42+
if (!lineMatch) {
43+
return;
44+
}
45+
46+
let object = null;
47+
let method = null;
48+
let functionName = null;
49+
let typeName = null;
50+
let methodName = null;
51+
const isNative = lineMatch[5] === "native";
52+
53+
if (lineMatch[1]) {
54+
functionName = lineMatch[1];
55+
let methodStart = functionName.lastIndexOf(".");
56+
if (functionName[methodStart - 1] == ".") methodStart--;
57+
if (methodStart > 0) {
58+
object = functionName.substring(0, methodStart);
59+
method = functionName.substring(methodStart + 1);
60+
const objectEnd = object.indexOf(".Module");
61+
if (objectEnd > 0) {
62+
functionName = functionName.substring(objectEnd + 1);
63+
object = object.substring(0, objectEnd);
64+
}
65+
}
66+
}
67+
68+
if (method) {
69+
typeName = object;
70+
methodName = method;
71+
}
72+
73+
if (method === "<anonymous>") {
74+
methodName = null;
75+
functionName = null;
76+
}
77+
78+
return new CallSite({
79+
typeName,
80+
functionName,
81+
methodName,
82+
fileName: lineMatch[2] || null,
83+
lineNumber: parseInt(lineMatch[3]) || null,
84+
columnNumber: parseInt(lineMatch[4]) || null,
85+
native: isNative,
86+
});
87+
}
88+
89+
export interface CallSiteOptions {
90+
typeName: string | null;
91+
functionName: string | null;
92+
methodName: string | null;
93+
fileName: string | null;
94+
lineNumber: number | null;
95+
columnNumber: number | null;
96+
native: boolean;
97+
}
98+
99+
// https://v8.dev/docs/stack-trace-api#customizing-stack-traces
100+
// This class supports the subset of options implemented by `node-stack-trace`:
101+
// https://github.com/felixge/node-stack-trace/blob/4c41a4526e74470179b3b6dd5d75191ca8c56c17/index.js
102+
export class CallSite implements NodeJS.CallSite {
103+
constructor(private readonly opts: CallSiteOptions) {}
104+
105+
getThis(): unknown {
106+
return null;
107+
}
108+
getTypeName(): string | null {
109+
return this.opts.typeName;
110+
}
111+
// eslint-disable-next-line @typescript-eslint/ban-types
112+
getFunction(): Function | undefined {
113+
return undefined;
114+
}
115+
getFunctionName(): string | null {
116+
return this.opts.functionName;
117+
}
118+
getMethodName(): string | null {
119+
return this.opts.methodName;
120+
}
121+
getFileName(): string | null {
122+
return this.opts.fileName;
123+
}
124+
getLineNumber(): number | null {
125+
return this.opts.lineNumber;
126+
}
127+
getColumnNumber(): number | null {
128+
return this.opts.columnNumber;
129+
}
130+
getEvalOrigin(): string | undefined {
131+
return undefined;
132+
}
133+
isToplevel(): boolean {
134+
return false;
135+
}
136+
isEval(): boolean {
137+
return false;
138+
}
139+
isNative(): boolean {
140+
return this.opts.native;
141+
}
142+
isConstructor(): boolean {
143+
return false;
144+
}
145+
isAsync(): boolean {
146+
return false;
147+
}
148+
isPromiseAll(): boolean {
149+
return false;
150+
}
151+
isPromiseAny(): boolean {
152+
return false;
153+
}
154+
getPromiseIndex(): number | null {
155+
return null;
156+
}
157+
}

0 commit comments

Comments
 (0)