feat: include source-mapped stack trace for uncaught errors (#876)

Note that we replace the existing "pageerror" handler with our new
`UncaughtError` handler. The latter has a structured stack trace
attached which we can source map via DevTools.

This regresses the current functionality slightly, as we lose
"pageerror"s for workers and oopif.
This commit is contained in:
Simon Zünd authored and GitHub committed 2026-02-03 12:24:23 +00:00
1 parent a08f550b28
commit ecef712e70
5 files changed
+53 -18

No files matched your search

+6 -12
View File
@@ -14,7 +14,7 @@ import {
UniverseManager,
urlsEqual,
} from './DevtoolsUtils.js';
import type {ListenerMap} from './PageCollector.js';
import type {ListenerMap, UncaughtError} from './PageCollector.js';
import {NetworkCollector, ConsoleCollector} from './PageCollector.js';
import {Locator} from './third_party/index.js';
import type {DevTools} from './third_party/index.js';
@@ -156,14 +156,8 @@ export class McpContext implements Context {
console: event => {
collect(event);
},
pageerror: event => {
if (event instanceof Error) {
collect(event);
} else {
const error = new Error(`${event}`);
error.stack = undefined;
collect(error);
}
uncaughtError: event => {
collect(event);
},
issue: event => {
collect(event);
@@ -245,7 +239,7 @@ export class McpContext implements Context {
getConsoleData(
includePreservedMessages?: boolean,
): Array<ConsoleMessage | Error | DevTools.AggregatedIssue> {
): Array<ConsoleMessage | Error | DevTools.AggregatedIssue | UncaughtError> {
const page = this.getSelectedPage();
return this.#consoleCollector.getData(page, includePreservedMessages);
}
@@ -255,14 +249,14 @@ export class McpContext implements Context {
}
getConsoleMessageStableId(
message: ConsoleMessage | Error | DevTools.AggregatedIssue,
message: ConsoleMessage | Error | DevTools.AggregatedIssue | UncaughtError,
): number {
return this.#consoleCollector.getIdForResource(message);
}
getConsoleMessageById(
id: number,
): ConsoleMessage | Error | DevTools.AggregatedIssue {
): ConsoleMessage | Error | DevTools.AggregatedIssue | UncaughtError {
return this.#consoleCollector.getById(this.getSelectedPage(), id);
}
+5 -4
View File
@@ -9,6 +9,7 @@ import {IssueFormatter} from './formatters/IssueFormatter.js';
import {NetworkFormatter} from './formatters/NetworkFormatter.js';
import {SnapshotFormatter} from './formatters/SnapshotFormatter.js';
import type {McpContext} from './McpContext.js';
import {UncaughtError} from './PageCollector.js';
import {DevTools} from './third_party/index.js';
import type {
ConsoleMessage,
@@ -276,8 +277,8 @@ export class McpResponse implements Response {
this.#attachedConsoleMessageId,
);
const consoleMessageStableId = this.#attachedConsoleMessageId;
if ('args' in message) {
const consoleMessage = message as ConsoleMessage;
if ('args' in message || message instanceof UncaughtError) {
const consoleMessage = message as ConsoleMessage | UncaughtError;
const devTools = context.getDevToolsUniverse();
detailedConsoleMessage = await ConsoleFormatter.from(consoleMessage, {
id: consoleMessageStableId,
@@ -332,8 +333,8 @@ export class McpResponse implements Response {
async (item): Promise<ConsoleFormatter | IssueFormatter | null> => {
const consoleMessageStableId =
context.getConsoleMessageStableId(item);
if ('args' in item) {
const consoleMessage = item as ConsoleMessage;
if ('args' in item || item instanceof UncaughtError) {
const consoleMessage = item as ConsoleMessage | UncaughtError;
const devTools = context.getDevToolsUniverse();
return await ConsoleFormatter.from(consoleMessage, {
id: consoleMessageStableId,
+1 -1
View File
@@ -236,7 +236,7 @@ export class PageCollector<T> {
}
export class ConsoleCollector extends PageCollector<
ConsoleMessage | Error | DevTools.AggregatedIssue
ConsoleMessage | Error | DevTools.AggregatedIssue | UncaughtError
> {
#subscribedPages = new WeakMap<Page, PageEventSubscriber>();
+11
View File
@@ -11,6 +11,17 @@ at Iife (main.js:10:2)
at <anonymous> (main.js:9:0)
`;
exports[`console > get_console_message > applies source maps to stack traces of uncaught exceptions 1`] = `
# test response
ID: 1
Message: error> Uncaught Error: b00m!
### Stack trace
at bar (main.js:2:8)
at foo (main.js:6:2)
at Iife (main.js:10:2)
at <anonymous> (main.js:9:0)
`;
exports[`console > get_console_message > issues type > gets issue details with node id parsing 1`] = `
# test response
ID: 1
+30 -1
View File
@@ -49,7 +49,7 @@ describe('console', () => {
await listConsoleMessages.handler({params: {}}, response, context);
const formattedResponse = await response.handle('test', context);
const textContent = getTextContent(formattedResponse.content[0]);
assert.ok(textContent.includes('msgid=1 [error] undefined (0 args)'));
assert.ok(textContent.includes('msgid=1 [error] Uncaught (0 args)'));
});
});
@@ -257,5 +257,34 @@ describe('console', () => {
t.assert.snapshot?.(rawText);
});
});
it('applies source maps to stack traces of uncaught exceptions', async t => {
server.addRoute('/main.min.js', (_req, res) => {
res.setHeader('Content-Type', 'text/javascript');
res.statusCode = 200;
res.end(`function n(){throw new Error("b00m!")}function o(){n()}(function n(){o()})();
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJiYXIiLCJFcnJvciIsImZvbyIsIklpZmUiXSwic291cmNlcyI6WyIuL21haW4uanMiXSwic291cmNlc0NvbnRlbnQiOlsiXG5mdW5jdGlvbiBiYXIoKSB7XG4gIHRocm93IG5ldyBFcnJvcignYjAwbSEnKTtcbn1cblxuZnVuY3Rpb24gZm9vKCkge1xuICBiYXIoKTtcbn1cblxuKGZ1bmN0aW9uIElpZmUoKSB7XG4gIGZvbygpO1xufSkoKTtcblxuIl0sIm1hcHBpbmdzIjoiQUFDQSxTQUFTQSxJQUNQLE1BQU0sSUFBSUMsTUFBTSxRQUNsQixDQUVBLFNBQVNDLElBQ1BGLEdBQ0YsRUFFQSxTQUFVRyxJQUNSRCxHQUNELEVBRkQiLCJpZ25vcmVMaXN0IjpbXX0=
`);
});
server.addHtmlRoute(
'/index.html',
`<script src="${server.getRoute('/main.min.js')}"></script>`,
);
await withMcpContext(async (response, context) => {
const page = await context.newPage();
await page.goto(server.getRoute('/index.html'));
await getConsoleMessage.handler(
{params: {msgid: 1}},
response,
context,
);
const formattedResponse = await response.handle('test', context);
const rawText = getTextContent(formattedResponse.content[0]);
t.assert.snapshot?.(rawText);
});
});
});
});