From e6102efb47c299406c73396454a705c6df54658e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Z=C3=BCnd?= Date: Thu, 8 Jan 2026 10:11:07 +0100 Subject: [PATCH] chore: implement stack trace formatting for console messages --- src/formatters/consoleFormatter.ts | 41 ++++++++++++++++++ src/third_party/devtools.ts | 1 + .../consoleFormatter.test.js.snapshot | 10 +++++ tests/formatters/consoleFormatter.test.ts | 43 +++++++++++++++++++ 4 files changed, 95 insertions(+) diff --git a/src/formatters/consoleFormatter.ts b/src/formatters/consoleFormatter.ts index 6111e3370..06071c197 100644 --- a/src/formatters/consoleFormatter.ts +++ b/src/formatters/consoleFormatter.ts @@ -15,6 +15,7 @@ export interface ConsoleMessageData { count?: number; description?: string; args?: string[]; + stackTrace?: DevTools.StackTrace.StackTrace.StackTrace; } // The short format for a console message, based on a previous format. @@ -46,6 +47,7 @@ export function formatConsoleEventVerbose( `ID: ${msg.consoleMessageStableId}`, `Message: ${msg.type}> ${aggregatedIssue ? formatIssue(aggregatedIssue, msg.description, context) : msg.message}`, aggregatedIssue ? undefined : formatArgs(msg), + formatStackTrace(msg.stackTrace), ].filter(line => !!line); return result.join('\n'); } @@ -163,3 +165,42 @@ export function formatIssue( if (result.length === 0) return 'No affected resources found'; return result.join('\n'); } + +function formatStackTrace( + stackTrace: DevTools.StackTrace.StackTrace.StackTrace | undefined, +): string { + if (!stackTrace) { + return ''; + } + + return [ + '### Stack trace', + formatFragment(stackTrace.syncFragment), + ...stackTrace.asyncFragments.map(formatAsyncFragment), + ].join('\n'); +} + +function formatFragment( + fragment: DevTools.StackTrace.StackTrace.Fragment, +): string { + return fragment.frames.map(formatFrame).join('\n'); +} + +function formatAsyncFragment( + fragment: DevTools.StackTrace.StackTrace.AsyncFragment, +): string { + const separatorLineLength = 40; + const prefix = `--- ${fragment.description || 'async'} `; + const separator = prefix + '-'.repeat(separatorLineLength - prefix.length); + return separator + '\n' + formatFragment(fragment); +} + +function formatFrame(frame: DevTools.StackTrace.StackTrace.Frame): string { + let result = `at ${frame.name ?? ''}`; + if (frame.uiSourceCode) { + result += ` (${frame.uiSourceCode.displayName()}:${frame.line}:${frame.column})`; + } else if (frame.url) { + result += ` (${frame.url}:${frame.line}:${frame.column})`; + } + return result; +} diff --git a/src/third_party/devtools.ts b/src/third_party/devtools.ts index 5e06db4ee..d4838b178 100644 --- a/src/third_party/devtools.ts +++ b/src/third_party/devtools.ts @@ -7,6 +7,7 @@ export type { IssuesManagerEventTypes, CDPConnection, + StackTrace, } from '../../node_modules/chrome-devtools-frontend/mcp/mcp.js'; export { AgentFocus, diff --git a/tests/formatters/consoleFormatter.test.js.snapshot b/tests/formatters/consoleFormatter.test.js.snapshot index d6803572e..411b28e53 100644 --- a/tests/formatters/consoleFormatter.test.js.snapshot +++ b/tests/formatters/consoleFormatter.test.js.snapshot @@ -10,6 +10,16 @@ exports[`consoleFormatter > formatConsoleEventShort > formats a console.log mess msgid=2 [log] Processing file: (1 args) `; +exports[`consoleFormatter > formatConsoleEventVerbose > formats a console message with a stack trace 1`] = ` +ID: 5 +Message: log> Hello stack trace! +### Stack trace +at foo (foo.ts:10:2) +at bar (foo.ts:20:2) +--- setTimeout ------------------------- +at schedule (util.ts:5:2) +`; + exports[`consoleFormatter > formatConsoleEventVerbose > formats a console.error message 1`] = ` ID: 4 Message: error> Something went wrong diff --git a/tests/formatters/consoleFormatter.test.ts b/tests/formatters/consoleFormatter.test.ts index 8734a91fe..4c7e69b71 100644 --- a/tests/formatters/consoleFormatter.test.ts +++ b/tests/formatters/consoleFormatter.test.ts @@ -11,6 +11,7 @@ import { formatConsoleEventShort, formatConsoleEventVerbose, } from '../../src/formatters/consoleFormatter.js'; +import type {DevTools} from '../../src/third_party/index.js'; import {getMockAggregatedIssue} from '../utils.js'; describe('consoleFormatter', () => { @@ -92,6 +93,48 @@ describe('consoleFormatter', () => { const result = formatConsoleEventVerbose(message); t.assert.snapshot?.(result); }); + + it('formats a console message with a stack trace', t => { + const message: ConsoleMessageData = { + consoleMessageStableId: 5, + type: 'log', + message: 'Hello stack trace!', + args: [], + stackTrace: { + syncFragment: { + frames: [ + { + line: 10, + column: 2, + url: 'foo.ts', + name: 'foo', + }, + { + line: 20, + column: 2, + url: 'foo.ts', + name: 'bar', + }, + ], + }, + asyncFragments: [ + { + description: 'setTimeout', + frames: [ + { + line: 5, + column: 2, + url: 'util.ts', + name: 'schedule', + }, + ], + }, + ], + } as unknown as DevTools.StackTrace.StackTrace.StackTrace, + }; + const result = formatConsoleEventVerbose(message); + t.assert.snapshot?.(result); + }); }); it('formats a console.log message with issue type', t => {