Skip to content

Commit c869965

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 c869965

File tree

6 files changed

+108
-4
lines changed

6 files changed

+108
-4
lines changed

src/McpContext.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ 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 type {TargetUniverse} from './DevtoolsUtils.js';
12+
import {
13+
extractUrlLikeFromDevToolsTitle,
14+
UniverseManager,
15+
urlsEqual,
16+
} from './DevtoolsUtils.js';
1217
import type {ListenerMap} from './PageCollector.js';
1318
import {NetworkCollector, ConsoleCollector} from './PageCollector.js';
1419
import {Locator} from './third_party/index.js';
@@ -104,6 +109,7 @@ export class McpContext implements Context {
104109
#textSnapshot: TextSnapshot | null = null;
105110
#networkCollector: NetworkCollector;
106111
#consoleCollector: ConsoleCollector;
112+
#devtoolsUniverseManager: UniverseManager;
107113

108114
#isRunningTrace = false;
109115
#networkConditionsMap = new WeakMap<Page, string>();
@@ -149,17 +155,20 @@ export class McpContext implements Context {
149155
},
150156
} as ListenerMap;
151157
});
158+
this.#devtoolsUniverseManager = new UniverseManager(this.browser);
152159
}
153160

154161
async #init() {
155162
const pages = await this.createPagesSnapshot();
156163
await this.#networkCollector.init(pages);
157164
await this.#consoleCollector.init(pages);
165+
await this.#devtoolsUniverseManager.init(pages);
158166
}
159167

160168
dispose() {
161169
this.#networkCollector.dispose();
162170
this.#consoleCollector.dispose();
171+
this.#devtoolsUniverseManager.dispose();
163172
}
164173

165174
static async from(
@@ -226,6 +235,10 @@ export class McpContext implements Context {
226235
return this.#consoleCollector.getData(page, includePreservedMessages);
227236
}
228237

238+
getDevToolsUniverse(): TargetUniverse | null {
239+
return this.#devtoolsUniverseManager.get(this.getSelectedPage());
240+
}
241+
229242
getConsoleMessageStableId(
230243
message: ConsoleMessage | Error | DevTools.AggregatedIssue,
231244
): number {

src/McpResponse.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,26 @@ 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+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
241+
const rawStackTrace = (consoleMessage as any)._rawStackTrace();
242+
if (rawStackTrace) {
243+
const devTools = context.getDevToolsUniverse();
244+
if (devTools) {
245+
// TODO: We use the default page target at the moment but really puppeteer should attach the target ID to the
246+
// console message and we resolve that via the universes' TargetManager.
247+
const {universe, target} = devTools;
248+
const binding = universe.context.get(
249+
DevTools.DebuggerWorkspaceBinding,
250+
);
251+
stackTrace = await binding.createStackTraceFromProtocolRuntime(
252+
rawStackTrace,
253+
target,
254+
);
255+
}
256+
}
257+
238258
consoleData = {
239259
consoleMessageStableId,
240260
type: consoleMessage.type(),
@@ -249,6 +269,7 @@ export class McpResponse implements Response {
249269
: String(stringArg);
250270
}),
251271
),
272+
stackTrace,
252273
};
253274
} else if (message instanceof DevTools.AggregatedIssue) {
254275
const mappedIssueMessage = mapIssueToMessageObject(message);
@@ -296,6 +317,26 @@ export class McpResponse implements Response {
296317
context.getConsoleMessageStableId(item);
297318
if ('args' in item) {
298319
const consoleMessage = item as ConsoleMessage;
320+
321+
let stackTrace = undefined;
322+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
323+
const rawStackTrace = (consoleMessage as any)._rawStackTrace();
324+
if (rawStackTrace) {
325+
const devTools = context.getDevToolsUniverse();
326+
if (devTools) {
327+
// TODO: We use the default page target at the moment but really puppeteer should attach the target ID to the
328+
// console message and we resolve that via the universes' TargetManager.
329+
const {universe, target} = devTools;
330+
const binding = universe.context.get(
331+
DevTools.DebuggerWorkspaceBinding,
332+
);
333+
stackTrace =
334+
await binding.createStackTraceFromProtocolRuntime(
335+
rawStackTrace,
336+
target,
337+
);
338+
}
339+
}
299340
return {
300341
consoleMessageStableId,
301342
type: consoleMessage.type(),
@@ -310,6 +351,7 @@ export class McpResponse implements Response {
310351
: String(stringArg);
311352
}),
312353
),
354+
stackTrace,
313355
};
314356
}
315357
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: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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,34 @@ 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(
241+
'/index.html',
242+
`<script src="${server.getRoute('/main.min.js')}"></script>`,
243+
);
244+
245+
await withMcpContext(async (response, context) => {
246+
const page = await context.newPage();
247+
await page.goto(server.getRoute('/index.html'));
248+
249+
await getConsoleMessage.handler(
250+
{params: {msgid: 1}},
251+
response,
252+
context,
253+
);
254+
const formattedResponse = await response.handle('test', context);
255+
const rawText = getTextContent(formattedResponse[0]);
256+
257+
t.assert.snapshot?.(rawText);
258+
});
259+
});
231260
});
232261
});

tsconfig.json

Lines changed: 7 additions & 1 deletion
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,8 @@
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/codemirror",
6366
"node_modules/chrome-devtools-frontend/front_end/third_party/i18n",
6467
"node_modules/chrome-devtools-frontend/front_end/third_party/intl-messageformat",
6568
"node_modules/chrome-devtools-frontend/front_end/third_party/legacy-javascript",
@@ -68,5 +71,8 @@
6871
"node_modules/chrome-devtools-frontend/front_end/third_party/third-party-web",
6972
"node_modules/chrome-devtools-frontend/mcp"
7073
],
71-
"exclude": ["node_modules/chrome-devtools-frontend/**/*.test.ts"]
74+
"exclude": ["node_modules/chrome-devtools-frontend/**/*.test.ts"],
75+
"files": [
76+
"node_modules/chrome-devtools-frontend/front_end/third_party/acorn/package/dist/acorn.mjs"
77+
]
7278
}

0 commit comments

Comments
 (0)