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

Commit ffa00da

Browse files
szuendOrKoN
andauthored
feat: show message and stack trace in details when console.log'ging Error objects (#902)
This PR improves the `get_console_message` tool when logging `Error` objects. We use the existing `getExceptionDetails` CDP command to retrieve the structured stack trace, we'll then source map. Example: ```js try { compute(); } catch (e) { console.log('Compute failed', e); } ``` Before: ```txt ### Arguments Arg #0: Compute failed Arg #1: {} ``` After: ```txt ### Arguments Arg #0: Compute failed Arg #1: ComputeError: Invariant violation at compute (foo.ts:1:20) at <anonymous> (main.ts:2:8) Note: line and column numbers use 1-based indexing ``` --------- Co-authored-by: Alex Rudenko <OrKoN@users.noreply.github.com>
1 parent 1fad330 commit ffa00da

File tree

5 files changed

+166
-16
lines changed

5 files changed

+166
-16
lines changed

src/DevtoolsUtils.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,18 +281,71 @@ export class SymbolizedError {
281281
return new SymbolizedError(message, stackTrace);
282282
}
283283

284+
static async fromError(opts: {
285+
devTools?: TargetUniverse;
286+
error: Protocol.Runtime.RemoteObject;
287+
targetId: string;
288+
}): Promise<SymbolizedError> {
289+
const details = await SymbolizedError.#getExceptionDetails(
290+
opts.devTools,
291+
opts.error,
292+
opts.targetId,
293+
);
294+
if (details) {
295+
return SymbolizedError.fromDetails({
296+
details,
297+
devTools: opts.devTools,
298+
targetId: opts.targetId,
299+
includeStackAndCause: true,
300+
});
301+
}
302+
303+
return new SymbolizedError(
304+
SymbolizedError.#getMessageFromException(opts.error),
305+
);
306+
}
307+
284308
static #getMessage(details: Protocol.Runtime.ExceptionDetails): string {
285309
// For Runtime.exceptionThrown with a present exception object, `details.text` will be "Uncaught" and
286310
// we have to manually parse out the error text from the exception description.
287311
// In the case of Runtime.getExceptionDetails, `details.text` has the Error.message.
288-
if (details.text === 'Uncaught') {
289-
const messageWithRest =
290-
details.exception?.description?.split('\n at ', 2) ?? [];
291-
return 'Uncaught ' + (messageWithRest[0] ?? '');
312+
if (details.text === 'Uncaught' && details.exception) {
313+
return (
314+
'Uncaught ' +
315+
SymbolizedError.#getMessageFromException(details.exception)
316+
);
292317
}
293318
return details.text;
294319
}
295320

321+
static #getMessageFromException(
322+
error: Protocol.Runtime.RemoteObject,
323+
): string {
324+
const messageWithRest = error.description?.split('\n at ', 2) ?? [];
325+
return messageWithRest[0] ?? '';
326+
}
327+
328+
static async #getExceptionDetails(
329+
devTools: TargetUniverse | undefined,
330+
error: Protocol.Runtime.RemoteObject,
331+
targetId: string,
332+
): Promise<Protocol.Runtime.ExceptionDetails | null> {
333+
if (!devTools || (error.type !== 'object' && error.subtype !== 'error')) {
334+
return null;
335+
}
336+
337+
const targetManager = devTools.universe.context.get(DevTools.TargetManager);
338+
const target = targetId
339+
? targetManager.targetById(targetId) || devTools.target
340+
: devTools.target;
341+
const model = target.model(DevTools.RuntimeModel) as DevTools.RuntimeModel;
342+
return (
343+
(await model.getExceptionDetails(
344+
error.objectId as DevTools.Protocol.Runtime.RemoteObjectId,
345+
)) ?? null
346+
);
347+
}
348+
296349
static createForTesting(
297350
message: string,
298351
stackTrace?: DevTools.StackTrace.StackTrace.StackTrace,

src/formatters/ConsoleFormatter.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ export class ConsoleFormatter {
7878
resolvedArgs = await Promise.all(
7979
msg.args().map(async (arg, i) => {
8080
try {
81+
const remoteObject = arg.remoteObject();
82+
if (
83+
remoteObject.type === 'object' &&
84+
remoteObject.subtype === 'error'
85+
) {
86+
return await SymbolizedError.fromError({
87+
devTools: options.devTools,
88+
error: remoteObject,
89+
// @ts-expect-error Internal ConsoleMessage API
90+
targetId: msg._targetId(),
91+
});
92+
}
8193
return await arg.jsonValue();
8294
} catch {
8395
return `<error: Argument ${i} is no longer available>`;

tests/formatters/ConsoleFormatter.test.ts

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ import {describe, it} from 'node:test';
1010
import {SymbolizedError} from '../../src/DevtoolsUtils.js';
1111
import {ConsoleFormatter} from '../../src/formatters/ConsoleFormatter.js';
1212
import {UncaughtError} from '../../src/PageCollector.js';
13-
import type {ConsoleMessage} from '../../src/third_party/index.js';
13+
import type {ConsoleMessage, Protocol} from '../../src/third_party/index.js';
1414
import type {DevTools} from '../../src/third_party/index.js';
1515

1616
interface MockConsoleMessage {
1717
type: () => string;
1818
text: () => string;
19-
args: () => Array<{jsonValue: () => Promise<unknown>}>;
19+
args: () => Array<{
20+
jsonValue: () => Promise<unknown>;
21+
remoteObject: () => Protocol.Runtime.RemoteObject;
22+
}>;
2023
stackTrace?: DevTools.StackTrace.StackTrace.StackTrace;
2124
}
2225

@@ -46,7 +49,12 @@ describe('ConsoleFormatter', () => {
4649
const message = createMockMessage({
4750
type: () => 'log',
4851
text: () => 'Processing file:',
49-
args: () => [{jsonValue: async () => 'file.txt'}],
52+
args: () => [
53+
{
54+
jsonValue: async () => 'file.txt',
55+
remoteObject: () => ({type: 'string'}),
56+
},
57+
],
5058
});
5159
const result = (
5260
await ConsoleFormatter.from(message, {id: 2, fetchDetailedData: true})
@@ -59,8 +67,14 @@ describe('ConsoleFormatter', () => {
5967
type: () => 'log',
6068
text: () => 'Processing file:',
6169
args: () => [
62-
{jsonValue: async () => 'file.txt'},
63-
{jsonValue: async () => 'another file'},
70+
{
71+
jsonValue: async () => 'file.txt',
72+
remoteObject: () => ({type: 'string'}),
73+
},
74+
{
75+
jsonValue: async () => 'another file',
76+
remoteObject: () => ({type: 'string'}),
77+
},
6478
],
6579
});
6680
const result = (
@@ -106,7 +120,12 @@ describe('ConsoleFormatter', () => {
106120
const message = createMockMessage({
107121
type: () => 'log',
108122
text: () => 'Processing file:',
109-
args: () => [{jsonValue: async () => 'file.txt'}],
123+
args: () => [
124+
{
125+
jsonValue: async () => 'file.txt',
126+
remoteObject: () => ({type: 'string'}),
127+
},
128+
],
110129
});
111130
const result = (
112131
await ConsoleFormatter.from(message, {id: 2, fetchDetailedData: true})
@@ -119,8 +138,14 @@ describe('ConsoleFormatter', () => {
119138
type: () => 'log',
120139
text: () => 'Processing file:',
121140
args: () => [
122-
{jsonValue: async () => 'file.txt'},
123-
{jsonValue: async () => 'another file'},
141+
{
142+
jsonValue: async () => 'file.txt',
143+
remoteObject: () => ({type: 'string'}),
144+
},
145+
{
146+
jsonValue: async () => 'another file',
147+
remoteObject: () => ({type: 'string'}),
148+
},
124149
],
125150
});
126151
const result = (
@@ -195,6 +220,7 @@ describe('ConsoleFormatter', () => {
195220
jsonValue: async () => {
196221
throw new Error('Execution context is not available');
197222
},
223+
remoteObject: () => ({type: 'string'}),
198224
},
199225
],
200226
});
@@ -320,8 +346,14 @@ describe('ConsoleFormatter', () => {
320346
type: () => 'log',
321347
text: () => 'Processing file:',
322348
args: () => [
323-
{jsonValue: async () => 'file.txt'},
324-
{jsonValue: async () => 'another file'},
349+
{
350+
jsonValue: async () => 'file.txt',
351+
remoteObject: () => ({type: 'string'}),
352+
},
353+
{
354+
jsonValue: async () => 'another file',
355+
remoteObject: () => ({type: 'string'}),
356+
},
325357
],
326358
});
327359
const result = (await ConsoleFormatter.from(message, {id: 1})).toJSON();
@@ -357,8 +389,14 @@ describe('ConsoleFormatter', () => {
357389
type: () => 'log',
358390
text: () => 'Processing file:',
359391
args: () => [
360-
{jsonValue: async () => 'file.txt'},
361-
{jsonValue: async () => 'another file'},
392+
{
393+
jsonValue: async () => 'file.txt',
394+
remoteObject: () => ({type: 'string'}),
395+
},
396+
{
397+
jsonValue: async () => 'another file',
398+
remoteObject: () => ({type: 'string'}),
399+
},
362400
],
363401
});
364402
const result = (

tests/tools/console.test.js.snapshot

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
exports[`console > get_console_message > applies source maps to stack traces of Error object console.log arguments 1`] = `
2+
# test response
3+
ID: 1
4+
Message: log> An error happened: JSHandle@error
5+
### Arguments
6+
Arg #0: An error happened:
7+
Arg #1: Error: b00m!
8+
at bar (main.js:3:9)
9+
at foo (main.js:7:3)
10+
at Iife (main.js:12:5)
11+
at <anonymous> (main.js:10:1)
12+
Note: line and column numbers use 1-based indexing
13+
### Stack trace
14+
at Iife (main.js:14:13)
15+
at <anonymous> (main.js:10:1)
16+
Note: line and column numbers use 1-based indexing
17+
`;
18+
119
exports[`console > get_console_message > applies source maps to stack traces of console messages 1`] = `
220
# test response
321
ID: 1

tests/tools/console.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,5 +286,34 @@ describe('console', () => {
286286
t.assert.snapshot?.(rawText);
287287
});
288288
});
289+
290+
it('applies source maps to stack traces of Error object console.log arguments', async t => {
291+
server.addRoute('/main.min.js', (_req, res) => {
292+
res.setHeader('Content-Type', 'text/javascript');
293+
res.statusCode = 200;
294+
res.end(`function n(){throw new Error("b00m!")}function o(){n()}(function n(){try{o()}catch(n){console.log("An error happened:",n)}})();
295+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJiYXIiLCJFcnJvciIsImZvbyIsIklpZmUiLCJlIiwiY29uc29sZSIsImxvZyJdLCJzb3VyY2VzIjpbIi4vbWFpbi5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyJcbmZ1bmN0aW9uIGJhcigpIHtcbiAgdGhyb3cgbmV3IEVycm9yKCdiMDBtIScpO1xufVxuXG5mdW5jdGlvbiBmb28oKSB7XG4gIGJhcigpO1xufVxuXG4oZnVuY3Rpb24gSWlmZSgpIHtcbiAgdHJ5IHtcbiAgICBmb28oKTtcbiAgfSBjYXRjaCAoZSkge1xuICAgIGNvbnNvbGUubG9nKCdBbiBlcnJvciBoYXBwZW5lZDonLCBlKTtcbiAgfVxufSkoKTtcblxuIl0sIm1hcHBpbmdzIjoiQUFDQSxTQUFTQSxJQUNQLE1BQU0sSUFBSUMsTUFBTSxRQUNsQixDQUVBLFNBQVNDLElBQ1BGLEdBQ0YsRUFFQSxTQUFVRyxJQUNSLElBQ0VELEdBQ0YsQ0FBRSxNQUFPRSxHQUNQQyxRQUFRQyxJQUFJLHFCQUFzQkYsRUFDcEMsQ0FDRCxFQU5EIiwiaWdub3JlTGlzdCI6W119
296+
`);
297+
});
298+
server.addHtmlRoute(
299+
'/index.html',
300+
`<script src="${server.getRoute('/main.min.js')}"></script>`,
301+
);
302+
303+
await withMcpContext(async (response, context) => {
304+
const page = await context.newPage();
305+
await page.goto(server.getRoute('/index.html'));
306+
307+
await getConsoleMessage.handler(
308+
{params: {msgid: 1}},
309+
response,
310+
context,
311+
);
312+
const formattedResponse = await response.handle('test', context);
313+
const rawText = getTextContent(formattedResponse.content[0]);
314+
315+
t.assert.snapshot?.(rawText);
316+
});
317+
});
289318
});
290319
});

0 commit comments

Comments
 (0)