Skip to content

Commit 96df988

Browse files
committed
feat: include stack trace in 'get_console_message' tool
This is the stack trace of the console message itself. If the argument is an Error object or an "Error.stack" like string we don't do anything special (yet). The stack trace is source mapped if source maps are available. For now, this only works for console messages of the main page target as puppeteer doesn't tell us yet from which target a console message is coming from.
1 parent 11d1f59 commit 96df988

File tree

6 files changed

+83
-4
lines changed

6 files changed

+83
-4
lines changed

src/McpContext.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import fs from 'node:fs/promises';
88
import os from 'node:os';
99
import path from 'node:path';
1010

11-
import {extractUrlLikeFromDevToolsTitle, urlsEqual} from './DevtoolsUtils.js';
11+
import {extractUrlLikeFromDevToolsTitle, TargetUniverse, UniverseManager, urlsEqual} from './DevtoolsUtils.js';
1212
import type {ListenerMap} from './PageCollector.js';
1313
import {NetworkCollector, ConsoleCollector} from './PageCollector.js';
1414
import {Locator} from './third_party/index.js';
@@ -104,6 +104,7 @@ export class McpContext implements Context {
104104
#textSnapshot: TextSnapshot | null = null;
105105
#networkCollector: NetworkCollector;
106106
#consoleCollector: ConsoleCollector;
107+
#devtoolsUniverseManager: UniverseManager;
107108

108109
#isRunningTrace = false;
109110
#networkConditionsMap = new WeakMap<Page, string>();
@@ -149,17 +150,20 @@ export class McpContext implements Context {
149150
},
150151
} as ListenerMap;
151152
});
153+
this.#devtoolsUniverseManager = new UniverseManager(this.browser);
152154
}
153155

154156
async #init() {
155157
const pages = await this.createPagesSnapshot();
156158
await this.#networkCollector.init(pages);
157159
await this.#consoleCollector.init(pages);
160+
await this.#devtoolsUniverseManager.init(pages);
158161
}
159162

160163
dispose() {
161164
this.#networkCollector.dispose();
162165
this.#consoleCollector.dispose();
166+
this.#devtoolsUniverseManager.dispose();
163167
}
164168

165169
static async from(
@@ -226,6 +230,10 @@ export class McpContext implements Context {
226230
return this.#consoleCollector.getData(page, includePreservedMessages);
227231
}
228232

233+
getDevToolsUniverse(): TargetUniverse | null {
234+
return this.#devtoolsUniverseManager.get(this.getSelectedPage());
235+
}
236+
229237
getConsoleMessageStableId(
230238
message: ConsoleMessage | Error | DevTools.AggregatedIssue,
231239
): number {

src/McpResponse.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,19 @@ export class McpResponse implements Response {
235235
const consoleMessageStableId = this.#attachedConsoleMessageId;
236236
if ('args' in message) {
237237
const consoleMessage = message as ConsoleMessage;
238+
239+
let stackTrace = undefined;
240+
if ((consoleMessage as any)._rawStackTrace()) {
241+
const devTools = context.getDevToolsUniverse();
242+
if (devTools) {
243+
// TODO: We use the default page target at the moment but really puppeteer should attach the target ID to the
244+
// console message and we resolve that via the universes' TargetManager.
245+
const {universe, target} = devTools;
246+
const binding = universe.context.get(DevTools.DebuggerWorkspaceBinding);
247+
stackTrace = await binding.createStackTraceFromProtocolRuntime((consoleMessage as any)._rawStackTrace(), target);
248+
}
249+
}
250+
238251
consoleData = {
239252
consoleMessageStableId,
240253
type: consoleMessage.type(),
@@ -249,6 +262,7 @@ export class McpResponse implements Response {
249262
: String(stringArg);
250263
}),
251264
),
265+
stackTrace,
252266
};
253267
} else if (message instanceof DevTools.AggregatedIssue) {
254268
const mappedIssueMessage = mapIssueToMessageObject(message);
@@ -296,6 +310,18 @@ export class McpResponse implements Response {
296310
context.getConsoleMessageStableId(item);
297311
if ('args' in item) {
298312
const consoleMessage = item as ConsoleMessage;
313+
314+
let stackTrace = undefined;
315+
if ((consoleMessage as any)._rawStackTrace()) {
316+
const devTools = context.getDevToolsUniverse();
317+
if (devTools) {
318+
// TODO: We use the default page target at the moment but really puppeteer should attach the target ID to the
319+
// console message and we resolve that via the universes' TargetManager.
320+
const {universe, target} = devTools;
321+
const binding = universe.context.get(DevTools.DebuggerWorkspaceBinding);
322+
stackTrace = await binding.createStackTraceFromProtocolRuntime((consoleMessage as any)._rawStackTrace(), target);
323+
}
324+
}
299325
return {
300326
consoleMessageStableId,
301327
type: consoleMessage.type(),
@@ -310,6 +336,7 @@ export class McpResponse implements Response {
310336
: String(stringArg);
311337
}),
312338
),
339+
stackTrace,
313340
};
314341
}
315342
if (item instanceof DevTools.AggregatedIssue) {

src/third_party/devtools.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ export {
2929
IssuesManagerEvents,
3030
createIssuesFromProtocolIssue,
3131
IssueAggregator,
32+
DebuggerWorkspaceBinding,
3233
} from '../../node_modules/chrome-devtools-frontend/mcp/mcp.js';

tests/tools/console.test.js.snapshot

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
exports[`console > get_console_message > applies source maps to stack traces of console messages 1`] = `
2+
# test response
3+
ID: 1
4+
Message: warn> hello world
5+
### Arguments
6+
Arg #0: hello world
7+
### Stack trace
8+
at bar (main.js:2:10)
9+
at foo (main.js:6:2)
10+
at Iife (main.js:10:2)
11+
at <anonymous> (main.js:9:0)
12+
`;
13+
114
exports[`console > get_console_message > issues type > gets issue details with node id parsing 1`] = `
215
# test response
316
ID: 1

tests/tools/console.test.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
listConsoleMessages,
1616
} from '../../src/tools/console.js';
1717
import {serverHooks} from '../server.js';
18-
import {getTextContent, withMcpContext} from '../utils.js';
18+
import {getTextContent, withBrowser, withMcpContext} from '../utils.js';
1919

2020
describe('console', () => {
2121
before(async () => {
@@ -120,6 +120,8 @@ describe('console', () => {
120120
});
121121

122122
describe('get_console_message', () => {
123+
const server = serverHooks();
124+
123125
it('gets a specific console message', async () => {
124126
await withMcpContext(async (response, context) => {
125127
const page = await context.newPage();
@@ -143,8 +145,6 @@ describe('console', () => {
143145
});
144146

145147
describe('issues type', () => {
146-
const server = serverHooks();
147-
148148
it('gets issue details with node id parsing', async t => {
149149
await withMcpContext(async (response, context) => {
150150
const page = await context.newPage();
@@ -228,5 +228,31 @@ describe('console', () => {
228228
});
229229
});
230230
});
231+
232+
it('applies source maps to stack traces of console messages', async t => {
233+
server.addRoute('/main.min.js', (_req, res) => {
234+
res.setHeader('Content-Type', 'text/javascript');
235+
res.statusCode = 200;
236+
res.end(`function n(){console.warn("hello world")}function o(){n()}(function n(){o()})();
237+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJiYXIiLCJjb25zb2xlIiwid2FybiIsImZvbyIsIklpZmUiXSwic291cmNlcyI6WyIuL21haW4uanMiXSwic291cmNlc0NvbnRlbnQiOlsiXG5mdW5jdGlvbiBiYXIoKSB7XG4gIGNvbnNvbGUud2FybignaGVsbG8gd29ybGQnKTtcbn1cblxuZnVuY3Rpb24gZm9vKCkge1xuICBiYXIoKTtcbn1cblxuKGZ1bmN0aW9uIElpZmUoKSB7XG4gIGZvbygpO1xufSkoKTtcblxuIl0sIm1hcHBpbmdzIjoiQUFDQSxTQUFTQSxJQUNQQyxRQUFRQyxLQUFLLGNBQ2YsQ0FFQSxTQUFTQyxJQUNQSCxHQUNGLEVBRUEsU0FBVUksSUFDUkQsR0FDRCxFQUZEIiwiaWdub3JlTGlzdCI6W119
238+
`);
239+
});
240+
server.addHtmlRoute('/index.html', `<script src="${server.getRoute('/main.min.js')}"></script>`);
241+
242+
await withMcpContext(async (response, context) => {
243+
const page = await context.newPage();
244+
await page.goto(server.getRoute('/index.html'));
245+
246+
await getConsoleMessage.handler(
247+
{params: {msgid: 1}},
248+
response,
249+
context,
250+
);
251+
const formattedResponse = await response.handle('test', context);
252+
const rawText = getTextContent(formattedResponse[0]);
253+
254+
t.assert.snapshot?.(rawText);
255+
});
256+
});
231257
});
232258
});

tsconfig.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"node_modules/chrome-devtools-frontend/front_end/core/protocol_client",
3434
"node_modules/chrome-devtools-frontend/front_end/core/root",
3535
"node_modules/chrome-devtools-frontend/front_end/core/sdk",
36+
"node_modules/chrome-devtools-frontend/front_end/entrypoints/formatter_worker",
3637
"node_modules/chrome-devtools-frontend/front_end/foundation/foundation.ts",
3738
"node_modules/chrome-devtools-frontend/front_end/foundation/Universe.ts",
3839
"node_modules/chrome-devtools-frontend/front_end/generated",
@@ -60,6 +61,9 @@
6061
"node_modules/chrome-devtools-frontend/front_end/models/trace",
6162
"node_modules/chrome-devtools-frontend/front_end/models/workspace",
6263
"node_modules/chrome-devtools-frontend/front_end/panels/issues/IssueAggregator.ts",
64+
"node_modules/chrome-devtools-frontend/front_end/third_party/acorn",
65+
"node_modules/chrome-devtools-frontend/front_end/third_party/acorn/package/dist/acorn.mjs",
66+
"node_modules/chrome-devtools-frontend/front_end/third_party/codemirror",
6367
"node_modules/chrome-devtools-frontend/front_end/third_party/i18n",
6468
"node_modules/chrome-devtools-frontend/front_end/third_party/intl-messageformat",
6569
"node_modules/chrome-devtools-frontend/front_end/third_party/legacy-javascript",

0 commit comments

Comments
 (0)