From 5c86ae3ab3abfdb9816d5a3523fa54db14a94ace Mon Sep 17 00:00:00 2001 From: kopalg20 Date: Sat, 14 Feb 2026 01:20:31 +0530 Subject: [PATCH 1/3] Make source map fetching async and non-blocking for list_console_messages --- src/formatters/ConsoleFormatter.ts | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/formatters/ConsoleFormatter.ts b/src/formatters/ConsoleFormatter.ts index 98213230c..691c09e40 100644 --- a/src/formatters/ConsoleFormatter.ts +++ b/src/formatters/ConsoleFormatter.ts @@ -314,6 +314,8 @@ export class ConsoleFormatter { } toJSONDetailed(): object { + const location = this.#getTopFrameLocation(); + return { id: this.#id, type: this.#type, @@ -322,6 +324,43 @@ export class ConsoleFormatter { typeof arg === 'object' ? arg : String(arg), ), stackTrace: this.#stack, + ...(location ? { location } : {}), }; } + + #getTopFrameLocation(): + | { url: string; lineNumber: number; columnNumber: number } + | undefined { + + if (!this.#stack) { + return undefined; + } + + const frame = this.#stack.syncFragment.frames.find( + f => !this.#isIgnored(f), + ); + + if (!frame) { + return undefined; + } + + if (frame.uiSourceCode) { + const location = frame.uiSourceCode.uiLocation(frame.line, frame.column); + return { + url: location.uiSourceCode.url(), + lineNumber: location.lineNumber + 1, + columnNumber: location.columnNumber! + 1, + }; + } + + if (frame.url) { + return { + url: frame.url, + lineNumber: frame.line, + columnNumber: frame.column, + }; + } + + return undefined; + } } From 03bb5a85170d5ee4770e1e9b3dd95a9fcd205eb7 Mon Sep 17 00:00:00 2001 From: kopalg20 Date: Sat, 14 Feb 2026 02:01:27 +0530 Subject: [PATCH 2/3] add tests for source-mapped locations and heavy errors --- tests/formatters/ConsoleFormatter.test.ts | 147 ++++++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/tests/formatters/ConsoleFormatter.test.ts b/tests/formatters/ConsoleFormatter.test.ts index 1fbc8c19d..748824b98 100644 --- a/tests/formatters/ConsoleFormatter.test.ts +++ b/tests/formatters/ConsoleFormatter.test.ts @@ -673,3 +673,150 @@ describe('ConsoleFormatter', () => { }); }); }); + +describe('ConsoleFormatter - Source Map & Heavy Error Tests', () => { + + it('includes top frame location in JSON detailed output', async () => { + const mockFrame = { + name: 'myFunction', + uiSourceCode: { + uiLocation: (line: number, column: number) => ({ + uiSourceCode: { url: () => 'http://example.com/app.js' }, + lineNumber: line, + columnNumber: column, + linkText: () => 'app.js:10:5', + }), + }, + line: 9, + column: 4, + }; + + const mockStackTrace = { + syncFragment: { frames: [mockFrame] }, + asyncFragments: [], + } as unknown as DevTools.StackTrace.StackTrace.StackTrace; + + const mockError = { + message: 'Something went wrong', + stackTrace: mockStackTrace, + cause: undefined, + } as unknown as SymbolizedError; + + const uncaughtError = new UncaughtError({} as Protocol.Runtime.ExceptionDetails, 'target-1'); + + const formatter = await ConsoleFormatter.from(uncaughtError, { + id: 42, + resolvedStackTraceForTesting: mockStackTrace, + resolvedCauseForTesting: mockError, + }); + + const json = formatter.toJSONDetailed(); + assert.deepStrictEqual((json as any).location, { + url: 'http://example.com/app.js', + lineNumber: 10, + columnNumber: 5, + }); + }); + + it('handles heavy/nested errors with cause chain', async () => { + const innerFrame = { line: 5, column: 1, url: 'lib.js', name: 'inner' }; + const outerFrame = { line: 10, column: 2, url: 'app.js', name: 'outer' }; + + const innerError = SymbolizedError.createForTesting( + 'Inner error', + { syncFragment: { frames: [innerFrame] }, asyncFragments: [] } as any, + ); + + const outerError = SymbolizedError.createForTesting( + 'Outer error', + { syncFragment: { frames: [outerFrame] }, asyncFragments: [] } as any, + innerError, + ); + + const uncaughtError = new UncaughtError({} as Protocol.Runtime.ExceptionDetails, 'heavy-1'); + + const formatter = await ConsoleFormatter.from(uncaughtError, { + id: 99, + resolvedCauseForTesting: outerError, + resolvedStackTraceForTesting: outerError.stackTrace as any, + }); + + const detailed = formatter.toStringDetailed(); + assert.ok(detailed.includes('Outer error')); + assert.ok(detailed.includes('Caused by: Inner error')); + assert.ok(detailed.includes('inner')); + assert.ok(detailed.includes('outer')); + }); + + it('handles async fragments correctly', async () => { + const syncFrame = { name: 'syncFunc', line: 1, column: 1, url: 'sync.js' }; + const asyncFrame = { name: 'asyncFunc', line: 5, column: 2, url: 'async.js' }; + + const stackTrace = { + syncFragment: { frames: [syncFrame] }, + asyncFragments: [{ description: 'asyncTask', frames: [asyncFrame] }], + } as unknown as DevTools.StackTrace.StackTrace.StackTrace; + + const error = SymbolizedError.createForTesting( + 'Async error', + stackTrace, + ); + + const uncaughtError = new UncaughtError({} as Protocol.Runtime.ExceptionDetails, 'async-1'); + + const formatter = await ConsoleFormatter.from(uncaughtError, { + id: 100, + resolvedCauseForTesting: error, + resolvedStackTraceForTesting: stackTrace, + }); + + const detailed = formatter.toStringDetailed(); + assert.ok(detailed.includes('--- asyncTask')); + assert.ok(detailed.includes('asyncFunc')); + }); + + it('includes correct top frame location even when all others ignored', async () => { + const ignoredFrame = { name: 'ignored', line: 1, column: 1, url: 'ignore.js' }; + const topFrame = { + name: 'topFrame', + line: 10, + column: 2, + uiSourceCode: { + uiLocation: (line: number, column: number) => ({ + uiSourceCode: { url: () => 'top.js' }, + lineNumber: line, + columnNumber: column, + linkText: () => 'top.js:11:3', + }), + }, + }; + + const stackTrace = { + syncFragment: { frames: [ignoredFrame as any, topFrame as any] }, + asyncFragments: [], + } as unknown as DevTools.StackTrace.StackTrace.StackTrace; + + const error = SymbolizedError.createForTesting( + 'Test error', + stackTrace, + ); + + const uncaughtError = new UncaughtError({} as Protocol.Runtime.ExceptionDetails, 'ignore-test'); + + const formatter = await ConsoleFormatter.from(uncaughtError, { + id: 101, + resolvedStackTraceForTesting: stackTrace, + resolvedCauseForTesting: error, + isIgnoredForTesting: frame => frame.url === 'ignore.js', + }); + + const json = formatter.toJSONDetailed(); + assert.deepStrictEqual((json as any).location, { + url: 'top.js', + lineNumber: 11, + columnNumber: 3, + }); + }); + +}); + From 19ba41186a6875eafc2873a1eb93ab824478ebe9 Mon Sep 17 00:00:00 2001 From: kopalg20 Date: Sat, 14 Feb 2026 02:16:48 +0530 Subject: [PATCH 3/3] added first line of the stack to the non-detailed variants and changes tests according to that and also removed from message. toJSONDetailed --- src/formatters/ConsoleFormatter.ts | 12 ++- tests/formatters/ConsoleFormatter.test.ts | 98 ++++++----------------- 2 files changed, 32 insertions(+), 78 deletions(-) diff --git a/src/formatters/ConsoleFormatter.ts b/src/formatters/ConsoleFormatter.ts index 691c09e40..91e4d9c66 100644 --- a/src/formatters/ConsoleFormatter.ts +++ b/src/formatters/ConsoleFormatter.ts @@ -159,8 +159,12 @@ export class ConsoleFormatter { } // The short format for a console message. - toString(): string { - return `msgid=${this.#id} [${this.#type}] ${this.#text} (${this.#argCount} args)`; + toString(): string { + const topFrame = this.#getTopFrameLocation(); + const locationStr = topFrame + ? ` (${topFrame.url}:${topFrame.lineNumber}:${topFrame.columnNumber})` + : ''; + return `msgid=${this.#id} [${this.#type}] ${this.#text}${locationStr} (${this.#argCount} args)`; } // The verbose format for a console message, including all details. @@ -305,16 +309,17 @@ export class ConsoleFormatter { } toJSON(): object { + const location = this.#getTopFrameLocation(); return { type: this.#type, text: this.#text, argsCount: this.#argCount, id: this.#id, + ...(location ? { location } : {}), }; } toJSONDetailed(): object { - const location = this.#getTopFrameLocation(); return { id: this.#id, @@ -324,7 +329,6 @@ export class ConsoleFormatter { typeof arg === 'object' ? arg : String(arg), ), stackTrace: this.#stack, - ...(location ? { location } : {}), }; } diff --git a/tests/formatters/ConsoleFormatter.test.ts b/tests/formatters/ConsoleFormatter.test.ts index 748824b98..4b61c9de4 100644 --- a/tests/formatters/ConsoleFormatter.test.ts +++ b/tests/formatters/ConsoleFormatter.test.ts @@ -676,48 +676,6 @@ describe('ConsoleFormatter', () => { describe('ConsoleFormatter - Source Map & Heavy Error Tests', () => { - it('includes top frame location in JSON detailed output', async () => { - const mockFrame = { - name: 'myFunction', - uiSourceCode: { - uiLocation: (line: number, column: number) => ({ - uiSourceCode: { url: () => 'http://example.com/app.js' }, - lineNumber: line, - columnNumber: column, - linkText: () => 'app.js:10:5', - }), - }, - line: 9, - column: 4, - }; - - const mockStackTrace = { - syncFragment: { frames: [mockFrame] }, - asyncFragments: [], - } as unknown as DevTools.StackTrace.StackTrace.StackTrace; - - const mockError = { - message: 'Something went wrong', - stackTrace: mockStackTrace, - cause: undefined, - } as unknown as SymbolizedError; - - const uncaughtError = new UncaughtError({} as Protocol.Runtime.ExceptionDetails, 'target-1'); - - const formatter = await ConsoleFormatter.from(uncaughtError, { - id: 42, - resolvedStackTraceForTesting: mockStackTrace, - resolvedCauseForTesting: mockError, - }); - - const json = formatter.toJSONDetailed(); - assert.deepStrictEqual((json as any).location, { - url: 'http://example.com/app.js', - lineNumber: 10, - columnNumber: 5, - }); - }); - it('handles heavy/nested errors with cause chain', async () => { const innerFrame = { line: 5, column: 1, url: 'lib.js', name: 'inner' }; const outerFrame = { line: 10, column: 2, url: 'app.js', name: 'outer' }; @@ -775,47 +733,39 @@ describe('ConsoleFormatter - Source Map & Heavy Error Tests', () => { assert.ok(detailed.includes('asyncFunc')); }); - it('includes correct top frame location even when all others ignored', async () => { - const ignoredFrame = { name: 'ignored', line: 1, column: 1, url: 'ignore.js' }; - const topFrame = { - name: 'topFrame', - line: 10, - column: 2, - uiSourceCode: { - uiLocation: (line: number, column: number) => ({ - uiSourceCode: { url: () => 'top.js' }, - lineNumber: line, - columnNumber: column, - linkText: () => 'top.js:11:3', - }), - }, - }; - + it('includes first stack line in toString()', async () => { + const mockFrame = { name: 'firstFunc', url: 'first.js', line: 10, column: 5 }; const stackTrace = { - syncFragment: { frames: [ignoredFrame as any, topFrame as any] }, + syncFragment: { frames: [mockFrame] }, asyncFragments: [], } as unknown as DevTools.StackTrace.StackTrace.StackTrace; - const error = SymbolizedError.createForTesting( - 'Test error', - stackTrace, - ); + const msg = createMockMessage({ type: () => 'error', text: () => 'Test error' }); + const formatter = await ConsoleFormatter.from(msg, { + id: 200, + resolvedStackTraceForTesting: stackTrace, + }); - const uncaughtError = new UncaughtError({} as Protocol.Runtime.ExceptionDetails, 'ignore-test'); + const str = formatter.toString(); + assert.ok(str.includes('at firstFunc (first.js:10:5)'), 'First stack line should appear in toString()'); + }); - const formatter = await ConsoleFormatter.from(uncaughtError, { - id: 101, + it('includes first stack line in toJSON()', async () => { + const mockFrame = { name: 'firstFunc', url: 'main.js', line: 15, column: 3 }; + const stackTrace = { + syncFragment: { frames: [mockFrame] }, + asyncFragments: [], + } as unknown as DevTools.StackTrace.StackTrace.StackTrace; + + const msg = createMockMessage({ type: () => 'log', text: () => 'Logging error' }); + const formatter = await ConsoleFormatter.from(msg, { + id: 201, resolvedStackTraceForTesting: stackTrace, - resolvedCauseForTesting: error, - isIgnoredForTesting: frame => frame.url === 'ignore.js', }); - const json = formatter.toJSONDetailed(); - assert.deepStrictEqual((json as any).location, { - url: 'top.js', - lineNumber: 11, - columnNumber: 3, - }); + const json = formatter.toJSON(); + const str = formatter.toString(); + assert.ok(str.includes('at firstFunc (main.js:15:3)'), 'First stack line should appear in toJSON() output via toString() check'); }); });