豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 1fd2f86

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 ee35f20 commit 1fd2f86

File tree

6 files changed

+153
-4
lines changed

6 files changed

+153
-4
lines changed

src/DevtoolsUtils.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import {Mutex} from './Mutex.js';
1111
import {DevTools} from './third_party/index.js';
1212
import type {
1313
Browser,
14+
ConsoleMessage,
1415
Page,
16+
Protocol,
1517
Target as PuppeteerTarget,
1618
} from './third_party/index.js';
1719

@@ -270,3 +272,77 @@ const SKIP_ALL_PAUSES = {
270272
// Do nothing.
271273
},
272274
};
275+
276+
export async function createStackTraceForConsoleMessage(
277+
devTools: TargetUniverse,
278+
consoleMessage: ConsoleMessage,
279+
): Promise<DevTools.StackTrace.StackTrace.StackTrace | undefined> {
280+
const message = consoleMessage as ConsoleMessage & {
281+
_rawStackTrace(): Protocol.Runtime.StackTrace | undefined;
282+
_targetId(): string | undefined;
283+
};
284+
const rawStackTrace = message._rawStackTrace();
285+
if (!rawStackTrace) {
286+
return undefined;
287+
}
288+
289+
const targetManager = devTools.universe.context.get(DevTools.TargetManager);
290+
const messageTargetId = message._targetId();
291+
const target = messageTargetId
292+
? targetManager.targetById(messageTargetId) || devTools.target
293+
: devTools.target;
294+
const model = target.model(DevTools.DebuggerModel) as DevTools.DebuggerModel;
295+
296+
// DevTools doesn't wait for source maps to attach before building a stack trace, rather it'll send
297+
// an update event once a source map was attached and the stack trace retranslated. This doesn't
298+
// work in the MCP case, so we'll collect all script IDs upfront and wait for any pending source map
299+
// loads before creating the stack trace. We might also have to wait for Debugger.ScriptParsed events if
300+
// the stack trace is created particularly early.
301+
const scriptIds = new Set<Protocol.Runtime.ScriptId>();
302+
rawStackTrace.callFrames.forEach(frame => scriptIds.add(frame.scriptId));
303+
for (
304+
let asyncStack = rawStackTrace.parent;
305+
asyncStack;
306+
asyncStack = asyncStack.parent
307+
) {
308+
asyncStack.callFrames.forEach(frame => scriptIds.add(frame.scriptId));
309+
}
310+
311+
await Promise.all(
312+
[...scriptIds].map(id =>
313+
Promise.race([
314+
waitForScript(model, id).then(script =>
315+
model.sourceMapManager().sourceMapForClientPromise(script),
316+
),
317+
new Promise(r => setTimeout(r, 1_000)),
318+
]),
319+
),
320+
);
321+
322+
const binding = devTools.universe.context.get(
323+
DevTools.DebuggerWorkspaceBinding,
324+
);
325+
// DevTools uses branded types for ScriptId and others. Casting the puppeteer protocol type to the DevTools protocol type is safe.
326+
return binding.createStackTraceFromProtocolRuntime(
327+
rawStackTrace as Parameters<
328+
DevTools.DebuggerWorkspaceBinding['createStackTraceFromProtocolRuntime']
329+
>[0],
330+
target,
331+
);
332+
}
333+
334+
// Waits indefinitely for the script so pair it with Promise.race.
335+
async function waitForScript(
336+
model: DevTools.DebuggerModel,
337+
scriptId: Protocol.Runtime.ScriptId,
338+
) {
339+
while (true) {
340+
const script = model.scriptForId(scriptId);
341+
if (script) {
342+
return script;
343+
}
344+
345+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
346+
await model.once('ParsedScriptSource' as any);
347+
}
348+
}

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>();
@@ -152,17 +158,20 @@ export class McpContext implements Context {
152158
},
153159
} as ListenerMap;
154160
});
161+
this.#devtoolsUniverseManager = new UniverseManager(this.browser);
155162
}
156163

157164
async #init() {
158165
const pages = await this.createPagesSnapshot();
159166
await this.#networkCollector.init(pages);
160167
await this.#consoleCollector.init(pages);
168+
await this.#devtoolsUniverseManager.init(pages);
161169
}
162170

163171
dispose() {
164172
this.#networkCollector.dispose();
165173
this.#consoleCollector.dispose();
174+
this.#devtoolsUniverseManager.dispose();
166175
}
167176

168177
static async from(
@@ -229,6 +238,10 @@ export class McpContext implements Context {
229238
return this.#consoleCollector.getData(page, includePreservedMessages);
230239
}
231240

241+
getDevToolsUniverse(): TargetUniverse | null {
242+
return this.#devtoolsUniverseManager.get(this.getSelectedPage());
243+
}
244+
232245
getConsoleMessageStableId(
233246
message: ConsoleMessage | Error | DevTools.AggregatedIssue,
234247
): number {

src/McpResponse.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import {mapIssueToMessageObject} from './DevtoolsUtils.js';
7+
import {
8+
createStackTraceForConsoleMessage,
9+
mapIssueToMessageObject,
10+
} from './DevtoolsUtils.js';
811
import type {ConsoleMessageData} from './formatters/consoleFormatter.js';
912
import {
1013
formatConsoleEventShort,
@@ -242,6 +245,11 @@ export class McpResponse implements Response {
242245
const consoleMessageStableId = this.#attachedConsoleMessageId;
243246
if ('args' in message) {
244247
const consoleMessage = message as ConsoleMessage;
248+
const devTools = context.getDevToolsUniverse();
249+
const stackTrace = devTools
250+
? await createStackTraceForConsoleMessage(devTools, consoleMessage)
251+
: undefined;
252+
245253
consoleData = {
246254
consoleMessageStableId,
247255
type: consoleMessage.type(),
@@ -256,6 +264,7 @@ export class McpResponse implements Response {
256264
: String(stringArg);
257265
}),
258266
),
267+
stackTrace,
259268
};
260269
} else if (message instanceof DevTools.AggregatedIssue) {
261270
const mappedIssueMessage = mapIssueToMessageObject(message);
@@ -304,6 +313,13 @@ export class McpResponse implements Response {
304313
context.getConsoleMessageStableId(item);
305314
if ('args' in item) {
306315
const consoleMessage = item as ConsoleMessage;
316+
const devTools = context.getDevToolsUniverse();
317+
const stackTrace = devTools
318+
? await createStackTraceForConsoleMessage(
319+
devTools,
320+
consoleMessage,
321+
)
322+
: undefined;
307323
return {
308324
consoleMessageStableId,
309325
type: consoleMessage.type(),
@@ -318,6 +334,7 @@ export class McpResponse implements Response {
318334
: String(stringArg);
319335
}),
320336
),
337+
stackTrace,
321338
};
322339
}
323340
if (item instanceof DevTools.AggregatedIssue) {

src/third_party/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import 'core-js/modules/es.promise.with-resolvers.js';
8+
import 'core-js/modules/es.set.union.v2.js';
89
import 'core-js/proposals/iterator-helpers.js';
910

1011
export type {Options as YargsOptions} from 'yargs';

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.content[0]);
256+
257+
t.assert.snapshot?.(rawText);
258+
});
259+
});
231260
});
232261
});

0 commit comments

Comments
 (0)