diff --git a/src/formatters/ConsoleFormatter.ts b/src/formatters/ConsoleFormatter.ts index b6a1f59ab..98213230c 100644 --- a/src/formatters/ConsoleFormatter.ts +++ b/src/formatters/ConsoleFormatter.ts @@ -10,7 +10,7 @@ import { SymbolizedError, } from '../DevtoolsUtils.js'; import {UncaughtError} from '../PageCollector.js'; -import type * as DevTools from '../third_party/index.js'; +import * as DevTools from '../third_party/index.js'; import type {ConsoleMessage} from '../third_party/index.js'; export interface ConsoleFormatterOptions { @@ -20,8 +20,13 @@ export interface ConsoleFormatterOptions { resolvedArgsForTesting?: unknown[]; resolvedStackTraceForTesting?: DevTools.DevTools.StackTrace.StackTrace.StackTrace; resolvedCauseForTesting?: SymbolizedError; + isIgnoredForTesting?: IgnoreCheck; } +export type IgnoreCheck = ( + frame: DevTools.DevTools.StackTrace.StackTrace.Frame, +) => boolean; + export class ConsoleFormatter { static readonly #STACK_TRACE_MAX_LINES = 50; @@ -35,6 +40,8 @@ export class ConsoleFormatter { readonly #stack?: DevTools.DevTools.StackTrace.StackTrace.StackTrace; readonly #cause?: SymbolizedError; + readonly #isIgnored: IgnoreCheck; + private constructor(params: { id: number; type: string; @@ -43,6 +50,7 @@ export class ConsoleFormatter { resolvedArgs?: unknown[]; stack?: DevTools.DevTools.StackTrace.StackTrace.StackTrace; cause?: SymbolizedError; + isIgnored: IgnoreCheck; }) { this.#id = params.id; this.#type = params.type; @@ -51,12 +59,37 @@ export class ConsoleFormatter { this.#resolvedArgs = params.resolvedArgs ?? []; this.#stack = params.stack; this.#cause = params.cause; + this.#isIgnored = params.isIgnored; } static async from( msg: ConsoleMessage | UncaughtError, options: ConsoleFormatterOptions, ): Promise { + const ignoreListManager = options?.devTools?.universe.context.get( + DevTools.DevTools.IgnoreListManager, + ); + const isIgnored: IgnoreCheck = + options.isIgnoredForTesting || + (frame => { + if (!ignoreListManager) { + return false; + } + if (frame.uiSourceCode) { + return ignoreListManager.isUserOrSourceMapIgnoreListedUISourceCode( + frame.uiSourceCode, + ); + } + if (frame.url) { + return ignoreListManager.isUserIgnoreListedURL( + frame.url as Parameters< + DevTools.DevTools.IgnoreListManager['isUserIgnoreListedURL'] + >[0], + ); + } + return false; + }); + if (msg instanceof UncaughtError) { const error = await SymbolizedError.fromDetails({ devTools: options?.devTools, @@ -72,6 +105,7 @@ export class ConsoleFormatter { text: error.message, stack: error.stackTrace, cause: error.cause, + isIgnored, }); } @@ -120,6 +154,7 @@ export class ConsoleFormatter { argCount: resolvedArgs.length || msg.args().length, resolvedArgs, stack, + isIgnored, }); } @@ -229,16 +264,22 @@ export class ConsoleFormatter { #formatFragment( fragment: DevTools.DevTools.StackTrace.StackTrace.Fragment, ): string[] { - return fragment.frames.map(this.#formatFrame.bind(this)); + const frames = fragment.frames.filter(frame => !this.#isIgnored(frame)); + return frames.map(this.#formatFrame.bind(this)); } #formatAsyncFragment( fragment: DevTools.DevTools.StackTrace.StackTrace.AsyncFragment, ): string[] { + const formattedFrames = this.#formatFragment(fragment); + if (formattedFrames.length === 0) { + return []; + } + const separatorLineLength = 40; const prefix = `--- ${fragment.description || 'async'} `; const separator = prefix + '-'.repeat(separatorLineLength - prefix.length); - return [separator, ...this.#formatFragment(fragment)]; + return [separator, ...formattedFrames]; } #formatFrame(frame: DevTools.DevTools.StackTrace.StackTrace.Frame): string { diff --git a/tests/formatters/ConsoleFormatter.test.js.snapshot b/tests/formatters/ConsoleFormatter.test.js.snapshot index 4e1e29aab..c836c7087 100644 --- a/tests/formatters/ConsoleFormatter.test.js.snapshot +++ b/tests/formatters/ConsoleFormatter.test.js.snapshot @@ -14,6 +14,25 @@ exports[`ConsoleFormatter > toString > formats an UncaughtError 1`] = ` msgid=4 [error] Uncaught TypeError: Cannot read properties of undefined (0 args) `; +exports[`ConsoleFormatter > toStringDetailed > does not show call frames with ignore listed scripts 1`] = ` +ID: 12 +Message: log> Hello stack trace! +### Stack trace +at foo (foo.ts:10:2) +at bar (foo.ts:20:2) +Note: line and column numbers use 1-based indexing +`; + +exports[`ConsoleFormatter > toStringDetailed > does not show fragments where all frames are ignore listed 1`] = ` +ID: 13 +Message: log> Hello stack trace! +### Stack trace +at foo (foo.ts:10:2) +--- await ------------------------------ +at bar (foo.ts:20:2) +Note: line and column numbers use 1-based indexing +`; + exports[`ConsoleFormatter > toStringDetailed > formats a console message with a stack trace 1`] = ` ID: 5 Message: log> Hello stack trace! diff --git a/tests/formatters/ConsoleFormatter.test.ts b/tests/formatters/ConsoleFormatter.test.ts index 5a880fc42..1fbc8c19d 100644 --- a/tests/formatters/ConsoleFormatter.test.ts +++ b/tests/formatters/ConsoleFormatter.test.ts @@ -479,6 +479,113 @@ describe('ConsoleFormatter', () => { ).toStringDetailed(); t.assert.snapshot?.(result); }); + + it('does not show call frames with ignore listed scripts', async t => { + const message = createMockMessage({ + type: () => 'log', + text: () => 'Hello stack trace!', + }); + const stackTrace = { + syncFragment: { + frames: [ + { + line: 10, + column: 2, + url: 'foo.ts', + name: 'foo', + }, + { + line: 200, + column: 46, + url: './node_modules/some-third-party-package/lib/index.js', + name: 'doThings', + }, + { + line: 250, + column: 12, + url: './node_modules/some-third-party-package/lib/index.js', + name: 'doThings2', + }, + { + line: 20, + column: 2, + url: 'foo.ts', + name: 'bar', + }, + ], + }, + asyncFragments: [], + } as unknown as DevTools.StackTrace.StackTrace.StackTrace; + + const result = ( + await ConsoleFormatter.from(message, { + id: 12, + resolvedStackTraceForTesting: stackTrace, + isIgnoredForTesting: frame => + Boolean(frame.url?.includes('node_modules')), + }) + ).toStringDetailed(); + t.assert.snapshot?.(result); + }); + + it('does not show fragments where all frames are ignore listed', async t => { + const message = createMockMessage({ + type: () => 'log', + text: () => 'Hello stack trace!', + }); + const stackTrace = { + syncFragment: { + frames: [ + { + line: 10, + column: 2, + url: 'foo.ts', + name: 'foo', + }, + ], + }, + asyncFragments: [ + { + description: 'setTimeout', + frames: [ + { + line: 200, + column: 46, + url: './node_modules/some-third-party-package/lib/index.js', + name: 'doThings', + }, + { + line: 250, + column: 12, + url: './node_modules/some-third-party-package/lib/index.js', + name: 'doThings2', + }, + ], + }, + { + description: 'await', + frames: [ + { + line: 20, + column: 2, + url: 'foo.ts', + name: 'bar', + }, + ], + }, + ], + } as unknown as DevTools.StackTrace.StackTrace.StackTrace; + + const result = ( + await ConsoleFormatter.from(message, { + id: 13, + resolvedStackTraceForTesting: stackTrace, + isIgnoredForTesting: frame => + Boolean(frame.url?.includes('node_modules')), + }) + ).toStringDetailed(); + t.assert.snapshot?.(result); + }); }); describe('toJSON', () => { it('formats a console.log message', async () => { diff --git a/tests/tools/console.test.js.snapshot b/tests/tools/console.test.js.snapshot index dc7ed1c55..5ded1f083 100644 --- a/tests/tools/console.test.js.snapshot +++ b/tests/tools/console.test.js.snapshot @@ -83,6 +83,19 @@ at (main.js:14:1) Note: line and column numbers use 1-based indexing `; +exports[`console > get_console_message > ignores frames from ignore listed URLs 1`] = ` +# test response +ID: 1 +Message: log> hello from callback +### Arguments +Arg #0: hello from callback +### Stack trace +at callback ('main.js':3:21) +at callIt ('main.js':7:13) +at ('main.js':8:13) +Note: line and column numbers use 1-based indexing +`; + exports[`console > get_console_message > issues type > gets issue details with node id parsing 1`] = ` # test response ID: 1 diff --git a/tests/tools/console.test.ts b/tests/tools/console.test.ts index bb074377c..64ed12dce 100644 --- a/tests/tools/console.test.ts +++ b/tests/tools/console.test.ts @@ -386,5 +386,48 @@ describe('console', () => { t.assert.snapshot?.(rawText); }); }); + + it('ignores frames from ignore listed URLs', async t => { + server.addHtmlRoute( + '/index.html', + ` + + + `, + ); + + await withMcpContext(async (response, context) => { + const page = await context.newPage(); + await page.goto(server.getRoute('/index.html')); + + await getConsoleMessage.handler( + {params: {msgid: 1}}, + response, + context, + ); + const formattedResponse = await response.handle('test', context); + const rawText = getTextContent(formattedResponse.content[0]); + + t.assert.snapshot?.(rawText); + }); + }); }); });