diff --git a/src/McpContext.ts b/src/McpContext.ts index bee6d637c..fa4bb9e83 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -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 { + ): Array { 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); } diff --git a/src/McpResponse.ts b/src/McpResponse.ts index f17e2dc0e..1f73181d5 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -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 => { 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, diff --git a/src/PageCollector.ts b/src/PageCollector.ts index 01feb2e77..abbd6ebd3 100644 --- a/src/PageCollector.ts +++ b/src/PageCollector.ts @@ -236,7 +236,7 @@ export class PageCollector { } export class ConsoleCollector extends PageCollector< - ConsoleMessage | Error | DevTools.AggregatedIssue + ConsoleMessage | Error | DevTools.AggregatedIssue | UncaughtError > { #subscribedPages = new WeakMap(); diff --git a/tests/tools/console.test.js.snapshot b/tests/tools/console.test.js.snapshot index aafe4e868..ce559f746 100644 --- a/tests/tools/console.test.js.snapshot +++ b/tests/tools/console.test.js.snapshot @@ -11,6 +11,17 @@ at Iife (main.js:10:2) at (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 (main.js:9:0) +`; + exports[`console > get_console_message > issues type > gets issue details with node id parsing 1`] = ` # test response ID: 1 diff --git a/tests/tools/console.test.ts b/tests/tools/console.test.ts index b207ca853..d80bc43a1 100644 --- a/tests/tools/console.test.ts +++ b/tests/tools/console.test.ts @@ -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', + ``, + ); + + 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); + }); + }); }); });