From bbe13ccfb84beb283b48547a7efd0ea37174c0d4 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 5 Nov 2025 04:23:06 +0300 Subject: [PATCH 01/14] chore: update @hawk.so/types to version 0.1.36 and use constant for console methods --- package.json | 5 +++-- src/addons/consoleCatcher.ts | 17 +++++++++++++---- yarn.lock | 8 ++++---- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index ebdf1e5..e93c406 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,9 @@ "vue": "^2" }, "dependencies": { - "@hawk.so/types": "^0.1.35", + "@hawk.so/types": "^0.1.36", "error-stack-parser": "^2.1.4", "vite-plugin-dts": "^4.2.4" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index 8989d60..449053a 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -4,6 +4,16 @@ import type { ConsoleLogEvent } from '@hawk.so/types'; import Sanitizer from '../modules/sanitizer'; +/** + * Maximum number of console logs to store + */ +const MAX_LOGS = 20; + +/** + * Console methods to intercept + */ +const CONSOLE_METHODS: string[] = ['log', 'warn', 'error', 'info', 'debug']; + /** * Creates a console interceptor that captures and formats console output */ @@ -11,8 +21,7 @@ function createConsoleCatcher(): { initConsoleCatcher: () => void; addErrorEvent: (event: ErrorEvent | PromiseRejectionEvent) => void; getConsoleLogStack: () => ConsoleLogEvent[]; - } { - const MAX_LOGS = 20; +} { const consoleOutput: ConsoleLogEvent[] = []; let isInitialized = false; @@ -157,9 +166,8 @@ function createConsoleCatcher(): { } isInitialized = true; - const consoleMethods: string[] = ['log', 'warn', 'error', 'info', 'debug']; - consoleMethods.forEach(function overrideConsoleMethod(method) { + CONSOLE_METHODS.forEach(function overrideConsoleMethod(method) { if (typeof window.console[method] !== 'function') { return; } @@ -216,3 +224,4 @@ const consoleCatcher = createConsoleCatcher(); export const { initConsoleCatcher, getConsoleLogStack, addErrorEvent } = consoleCatcher; + diff --git a/yarn.lock b/yarn.lock index 7f9c52f..3aa494e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -316,10 +316,10 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@hawk.so/types@^0.1.35": - version "0.1.35" - resolved "https://registry.yarnpkg.com/@hawk.so/types/-/types-0.1.35.tgz#6afd416dced1cc3282d721ca5621bf452b27aea1" - integrity sha512-uMTAeu6DlRlk+oputJBjTlrm1GzOkIwlMfGhpdOp3sRWe/YPGD6nMYlb9MZoVN6Yee7RIpYD7It+DPeUPAyIFw== +"@hawk.so/types@^0.1.36": + version "0.1.36" + resolved "https://registry.yarnpkg.com/@hawk.so/types/-/types-0.1.36.tgz#234b0e4c81bf5f50b1208910d45fc4ffb62e8ae1" + integrity sha512-AjW4FZPMqlDoXk63ntkTGOC1tdbHuGXIhEbVtBvz8YC9A7qcuxenzfGtjwuW6B9tqyADMGehh+/d+uQbAX7w0Q== dependencies: "@types/mongodb" "^3.5.34" From 643ff114b2ef0d0f9b8b8d549a2ab27b00ef3e4f Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 5 Nov 2025 04:58:37 +0300 Subject: [PATCH 02/14] refactor: implement ConsoleCatcher class for improved console log handling in Catcher --- src/addons/consoleCatcher.ts | 99 +++++++++++++++++++----------------- src/catcher.ts | 24 ++++++--- 2 files changed, 71 insertions(+), 52 deletions(-) diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index 449053a..e3e1988 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -15,15 +15,37 @@ const MAX_LOGS = 20; const CONSOLE_METHODS: string[] = ['log', 'warn', 'error', 'info', 'debug']; /** - * Creates a console interceptor that captures and formats console output + * Console catcher class for intercepting and capturing console logs */ -function createConsoleCatcher(): { - initConsoleCatcher: () => void; - addErrorEvent: (event: ErrorEvent | PromiseRejectionEvent) => void; - getConsoleLogStack: () => ConsoleLogEvent[]; -} { - const consoleOutput: ConsoleLogEvent[] = []; - let isInitialized = false; +export class ConsoleCatcher { + /** + * Singleton instance + */ + private static instance: ConsoleCatcher | null = null; + + /** + * Console output buffer + */ + private readonly consoleOutput: ConsoleLogEvent[] = []; + + /** + * Initialization flag + */ + private isInitialized = false; + + /** + * Private constructor to enforce singleton pattern + */ + private constructor() {} + + /** + * Get singleton instance + */ + public static getInstance(): ConsoleCatcher { + ConsoleCatcher.instance ??= new ConsoleCatcher(); + + return ConsoleCatcher.instance; + } /** * Converts any argument to its string representation @@ -32,7 +54,7 @@ function createConsoleCatcher(): { * @throws Error if the argument can not be stringified, for example by such reason: * SecurityError: Failed to read a named property 'toJSON' from 'Window': Blocked a frame with origin "https://codex.so" from accessing a cross-origin frame. */ - function stringifyArg(arg: unknown): string { + private stringifyArg(arg: unknown): string { if (typeof arg === 'string') { return arg; } @@ -54,7 +76,7 @@ function createConsoleCatcher(): { * * @param args - Console arguments that may include style directives */ - function formatConsoleArgs(args: unknown[]): { + private formatConsoleArgs(args: unknown[]): { message: string; styles: string[]; } { @@ -71,7 +93,7 @@ function createConsoleCatcher(): { return { message: args.map(arg => { try { - return stringifyArg(arg); + return this.stringifyArg(arg); } catch (error) { return '[Error stringifying argument: ' + (error instanceof Error ? error.message : String(error)) + ']'; } @@ -101,7 +123,7 @@ function createConsoleCatcher(): { .slice(styles.length + 1) .map(arg => { try { - return stringifyArg(arg); + return this.stringifyArg(arg); } catch (error) { return '[Error stringifying argument: ' + (error instanceof Error ? error.message : String(error)) + ']'; } @@ -119,11 +141,11 @@ function createConsoleCatcher(): { * * @param logEvent - The console log event to be added to the output buffer */ - function addToConsoleOutput(logEvent: ConsoleLogEvent): void { - if (consoleOutput.length >= MAX_LOGS) { - consoleOutput.shift(); + private addToConsoleOutput(logEvent: ConsoleLogEvent): void { + if (this.consoleOutput.length >= MAX_LOGS) { + this.consoleOutput.shift(); } - consoleOutput.push(logEvent); + this.consoleOutput.push(logEvent); } /** @@ -131,9 +153,7 @@ function createConsoleCatcher(): { * * @param event - The error event or promise rejection event to convert */ - function createConsoleEventFromError( - event: ErrorEvent | PromiseRejectionEvent - ): ConsoleLogEvent { + private createConsoleEventFromError(event: ErrorEvent | PromiseRejectionEvent): ConsoleLogEvent { if (event instanceof ErrorEvent) { return { method: 'error', @@ -160,24 +180,23 @@ function createConsoleCatcher(): { /** * Initializes the console interceptor by overriding default console methods */ - function initConsoleCatcher(): void { - if (isInitialized) { + public init(): void { + if (this.isInitialized) { return; } - isInitialized = true; + this.isInitialized = true; - CONSOLE_METHODS.forEach(function overrideConsoleMethod(method) { - if (typeof window.console[method] !== 'function') { + CONSOLE_METHODS.forEach((method) => { + if (typeof globalThis.console[method] !== 'function') { return; } const oldFunction = window.console[method].bind(window.console); - window.console[method] = function (...args: unknown[]): void { - const stack = new Error().stack?.split('\n').slice(2) - .join('\n') || ''; - const { message, styles } = formatConsoleArgs(args); + window.console[method] = (...args: unknown[]): void => { + const stack = new Error().stack?.split('\n').slice(2).join('\n') || ''; + const { message, styles } = this.formatConsoleArgs(args); const logEvent: ConsoleLogEvent = { method, @@ -189,7 +208,7 @@ function createConsoleCatcher(): { styles, }; - addToConsoleOutput(logEvent); + this.addToConsoleOutput(logEvent); oldFunction(...args); }; }); @@ -200,28 +219,16 @@ function createConsoleCatcher(): { * * @param event - The error or promise rejection event to handle */ - function addErrorEvent(event: ErrorEvent | PromiseRejectionEvent): void { - const logEvent = createConsoleEventFromError(event); + public addErrorEvent(event: ErrorEvent | PromiseRejectionEvent): void { + const logEvent = this.createConsoleEventFromError(event); - addToConsoleOutput(logEvent); + this.addToConsoleOutput(logEvent); } /** * Returns the current console output buffer */ - function getConsoleLogStack(): ConsoleLogEvent[] { - return [ ...consoleOutput ]; + public getConsoleLogStack(): ConsoleLogEvent[] { + return [...this.consoleOutput]; } - - return { - initConsoleCatcher, - addErrorEvent, - getConsoleLogStack, - }; } - -const consoleCatcher = createConsoleCatcher(); - -export const { initConsoleCatcher, getConsoleLogStack, addErrorEvent } = - consoleCatcher; - diff --git a/src/catcher.ts b/src/catcher.ts index 6b8d9b8..9dfb831 100644 --- a/src/catcher.ts +++ b/src/catcher.ts @@ -16,7 +16,7 @@ import type { JavaScriptCatcherIntegrations } from './types/integrations'; import { EventRejectedError } from './errors'; import type { HawkJavaScriptEvent } from './types'; import { isErrorProcessed, markErrorAsProcessed } from './utils/event'; -import { addErrorEvent, getConsoleLogStack, initConsoleCatcher } from './addons/consoleCatcher'; +import { ConsoleCatcher } from './addons/consoleCatcher'; import { validateUser, validateContext } from './utils/validation'; /** @@ -98,6 +98,11 @@ export default class Catcher { */ private readonly consoleTracking: boolean; + /** + * Console catcher instance + */ + private readonly consoleCatcher: ConsoleCatcher; + /** * Catcher constructor * @@ -116,8 +121,15 @@ export default class Catcher { this.setUser(settings.user || Catcher.getGeneratedUser()); this.setContext(settings.context || undefined); this.beforeSend = settings.beforeSend; - this.disableVueErrorHandler = settings.disableVueErrorHandler !== null && settings.disableVueErrorHandler !== undefined ? settings.disableVueErrorHandler : false; - this.consoleTracking = settings.consoleTracking !== null && settings.consoleTracking !== undefined ? settings.consoleTracking : true; + this.disableVueErrorHandler = + settings.disableVueErrorHandler !== null && settings.disableVueErrorHandler !== undefined + ? settings.disableVueErrorHandler + : false; + this.consoleTracking = + settings.consoleTracking !== null && settings.consoleTracking !== undefined + ? settings.consoleTracking + : true; + this.consoleCatcher = ConsoleCatcher.getInstance(); if (!this.token) { log( @@ -144,7 +156,7 @@ export default class Catcher { }); if (this.consoleTracking) { - initConsoleCatcher(); + this.consoleCatcher.init(); } /** @@ -284,7 +296,7 @@ export default class Catcher { */ if (this.consoleTracking) { - addErrorEvent(event); + this.consoleCatcher.addErrorEvent(event); } /** @@ -551,7 +563,7 @@ export default class Catcher { const userAgent = window.navigator.userAgent; const location = window.location.href; const getParams = this.getGetParams(); - const consoleLogs = this.consoleTracking && getConsoleLogStack(); + const consoleLogs = this.consoleTracking && this.consoleCatcher.getConsoleLogStack(); const addons: JavaScriptAddons = { window: { From c64f09199d798bee2e7f38791e063946a0e1a983 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 5 Nov 2025 05:04:03 +0300 Subject: [PATCH 03/14] chore: remove packageManager field from package.json for cleaner configuration --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index e93c406..73d2dbd 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,5 @@ "@hawk.so/types": "^0.1.36", "error-stack-parser": "^2.1.4", "vite-plugin-dts": "^4.2.4" - }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + } } From 5e1eef0e57fa53da7211b89f97a4d020f63f36a0 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 5 Nov 2025 05:07:50 +0300 Subject: [PATCH 04/14] fix: update console method reference from globalThis to window in ConsoleCatcher --- src/addons/consoleCatcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index e3e1988..967fa59 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -188,7 +188,7 @@ export class ConsoleCatcher { this.isInitialized = true; CONSOLE_METHODS.forEach((method) => { - if (typeof globalThis.console[method] !== 'function') { + if (typeof window.console[method] !== 'function') { return; } From 20272ce02f749593811fe5cf9c3da57e744f6673 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 5 Nov 2025 05:25:31 +0300 Subject: [PATCH 05/14] chore: add TODOs for future improvements in ConsoleCatcher --- src/addons/consoleCatcher.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index 967fa59..9f16d8e 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -232,3 +232,13 @@ export class ConsoleCatcher { return [...this.consoleOutput]; } } +// TODO:: 1) replace window with globalThis for better compatibility with different environments +// const G = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : undefined); +// if (!G || !G.console) return; +// G.console[method]... +// TODO:: 2) use .includes() instead of .indexOf() !== -1 for better readability (line 115) +// TODO:: 3) use for...of loop instead of .forEach() for better performance/readability (line 190) for (const method of CONSOLE_METHODS) +// TODO:: 4) extract error stringification logic to separate method to avoid duplication (lines 98, 128) +// TODO:: 5) improve %c style directive parsing logic for better accuracy and edge cases +// TODO:: 6) add type guards for console method access to improve type safety +// TODO:: 7) Store original console methods and implement restore() to revert overrides — allows safe teardown, prevents double-wrapping, and avoids global side-effects. From e8479b5dac3308d87a7f29fc3b52b333904631a4 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 5 Nov 2025 05:32:20 +0300 Subject: [PATCH 06/14] chore: add eslint-disable comments for member ordering in ConsoleCatcher methods --- src/addons/consoleCatcher.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index 9f16d8e..18c8230 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -180,6 +180,7 @@ export class ConsoleCatcher { /** * Initializes the console interceptor by overriding default console methods */ + // eslint-disable-next-line @typescript-eslint/member-ordering public init(): void { if (this.isInitialized) { return; @@ -219,6 +220,7 @@ export class ConsoleCatcher { * * @param event - The error or promise rejection event to handle */ + // eslint-disable-next-line @typescript-eslint/member-ordering public addErrorEvent(event: ErrorEvent | PromiseRejectionEvent): void { const logEvent = this.createConsoleEventFromError(event); @@ -228,6 +230,7 @@ export class ConsoleCatcher { /** * Returns the current console output buffer */ + // eslint-disable-next-line @typescript-eslint/member-ordering public getConsoleLogStack(): ConsoleLogEvent[] { return [...this.consoleOutput]; } From 370b173b3195313762bd8e67299cf43c13ff78b8 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 5 Nov 2025 05:34:50 +0300 Subject: [PATCH 07/14] lint fix --- src/addons/consoleCatcher.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index 18c8230..1579de6 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -196,7 +196,8 @@ export class ConsoleCatcher { const oldFunction = window.console[method].bind(window.console); window.console[method] = (...args: unknown[]): void => { - const stack = new Error().stack?.split('\n').slice(2).join('\n') || ''; + const stack = new Error().stack?.split('\n').slice(2) + .join('\n') || ''; const { message, styles } = this.formatConsoleArgs(args); const logEvent: ConsoleLogEvent = { @@ -232,7 +233,7 @@ export class ConsoleCatcher { */ // eslint-disable-next-line @typescript-eslint/member-ordering public getConsoleLogStack(): ConsoleLogEvent[] { - return [...this.consoleOutput]; + return [ ...this.consoleOutput ]; } } // TODO:: 1) replace window with globalThis for better compatibility with different environments From 8c663cf33846ea7017cbbf92474f01ddfaf86d9d Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 5 Nov 2025 05:36:16 +0300 Subject: [PATCH 08/14] lint fix --- src/addons/consoleCatcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index 1579de6..4a16e58 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -197,7 +197,7 @@ export class ConsoleCatcher { window.console[method] = (...args: unknown[]): void => { const stack = new Error().stack?.split('\n').slice(2) - .join('\n') || ''; + .join('\n') || ''; const { message, styles } = this.formatConsoleArgs(args); const logEvent: ConsoleLogEvent = { From 52da0a970050031b2be6e3ed07465270ef049cda Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 5 Nov 2025 05:48:41 +0300 Subject: [PATCH 09/14] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 73d2dbd..64ac827 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@hawk.so/javascript", "type": "commonjs", - "version": "3.2.10", + "version": "3.2.11", "description": "JavaScript errors tracking for Hawk.so", "files": [ "dist" From 38a0d73b0268bfa745fd9fb41176fa04dc9d6d67 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 5 Nov 2025 06:17:16 +0300 Subject: [PATCH 10/14] remove todos --- src/addons/consoleCatcher.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index 4a16e58..85704f2 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -236,13 +236,3 @@ export class ConsoleCatcher { return [ ...this.consoleOutput ]; } } -// TODO:: 1) replace window with globalThis for better compatibility with different environments -// const G = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : undefined); -// if (!G || !G.console) return; -// G.console[method]... -// TODO:: 2) use .includes() instead of .indexOf() !== -1 for better readability (line 115) -// TODO:: 3) use for...of loop instead of .forEach() for better performance/readability (line 190) for (const method of CONSOLE_METHODS) -// TODO:: 4) extract error stringification logic to separate method to avoid duplication (lines 98, 128) -// TODO:: 5) improve %c style directive parsing logic for better accuracy and edge cases -// TODO:: 6) add type guards for console method access to improve type safety -// TODO:: 7) Store original console methods and implement restore() to revert overrides — allows safe teardown, prevents double-wrapping, and avoids global side-effects. From 8a977c4f75b3058bdc6407eac1a2a7fce83cef2b Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Thu, 6 Nov 2025 06:17:36 +0300 Subject: [PATCH 11/14] chore: initialize consoleCatcher conditionally and handle potential null values --- src/catcher.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/catcher.ts b/src/catcher.ts index 9dfb831..2908238 100644 --- a/src/catcher.ts +++ b/src/catcher.ts @@ -101,7 +101,7 @@ export default class Catcher { /** * Console catcher instance */ - private readonly consoleCatcher: ConsoleCatcher; + private readonly consoleCatcher: ConsoleCatcher | null = null; /** * Catcher constructor @@ -129,7 +129,6 @@ export default class Catcher { settings.consoleTracking !== null && settings.consoleTracking !== undefined ? settings.consoleTracking : true; - this.consoleCatcher = ConsoleCatcher.getInstance(); if (!this.token) { log( @@ -156,6 +155,7 @@ export default class Catcher { }); if (this.consoleTracking) { + this.consoleCatcher = ConsoleCatcher.getInstance(); this.consoleCatcher.init(); } @@ -296,7 +296,7 @@ export default class Catcher { */ if (this.consoleTracking) { - this.consoleCatcher.addErrorEvent(event); + this.consoleCatcher!.addErrorEvent(event); } /** @@ -563,7 +563,7 @@ export default class Catcher { const userAgent = window.navigator.userAgent; const location = window.location.href; const getParams = this.getGetParams(); - const consoleLogs = this.consoleTracking && this.consoleCatcher.getConsoleLogStack(); + const consoleLogs = this.consoleTracking && this.consoleCatcher?.getConsoleLogStack(); const addons: JavaScriptAddons = { window: { From cba22187f529cf57a68b2f3163d3ef03b9ff9131 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Thu, 6 Nov 2025 06:18:32 +0300 Subject: [PATCH 12/14] fix: Console log stack trace --- src/addons/consoleCatcher.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index 85704f2..db2b141 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -196,8 +196,22 @@ export class ConsoleCatcher { const oldFunction = window.console[method].bind(window.console); window.console[method] = (...args: unknown[]): void => { - const stack = new Error().stack?.split('\n').slice(2) - .join('\n') || ''; + const errorStack = new Error('Console log stack trace').stack; + const stackLines = errorStack?.split('\n') || []; + + // Skip first line (Error message) and find first stack frame outside of consoleCatcher module + const consoleCatcherPattern = /consoleCatcher/i; + let userFrameIndex = 1; // Skip Error message line + + for (let i = 1; i < stackLines.length; i++) { + if (!consoleCatcherPattern.test(stackLines[i])) { + userFrameIndex = i; + break; + } + } + + const userStack = stackLines.slice(userFrameIndex).join('\n'); + const fileLine = stackLines[userFrameIndex]?.trim() || ''; const { message, styles } = this.formatConsoleArgs(args); const logEvent: ConsoleLogEvent = { @@ -205,8 +219,8 @@ export class ConsoleCatcher { timestamp: new Date(), type: method, message, - stack, - fileLine: stack.split('\n')[0]?.trim(), + stack: userStack, + fileLine, styles, }; From c43204c42532ff6da2e96d045c7a08e7bb71e958 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Sat, 8 Nov 2025 17:57:43 +0300 Subject: [PATCH 13/14] chore: add docks --- src/addons/consoleCatcher.ts | 43 +++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index db2b141..099aa1e 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -15,7 +15,11 @@ const MAX_LOGS = 20; const CONSOLE_METHODS: string[] = ['log', 'warn', 'error', 'info', 'debug']; /** - * Console catcher class for intercepting and capturing console logs + * Console catcher class for intercepting and capturing console logs. + * + * This singleton class wraps native console methods to capture all console output with accurate + * stack traces. When developers click on console messages in DevTools, they are taken to the + * original call site in their code, not to the interceptor's code. */ export class ConsoleCatcher { /** @@ -178,7 +182,10 @@ export class ConsoleCatcher { } /** - * Initializes the console interceptor by overriding default console methods + * Initializes the console interceptor by overriding default console methods. + * + * Wraps native console methods to intercept all calls, capture their context, and generate + * accurate stack traces that point to the original call site (not the interceptor). */ // eslint-disable-next-line @typescript-eslint/member-ordering public init(): void { @@ -193,13 +200,40 @@ export class ConsoleCatcher { return; } + // Store original function to forward calls after interception const oldFunction = window.console[method].bind(window.console); + /** + * Override console method to intercept all calls. + * + * For each intercepted call, we: + * 1. Generate a stack trace to find the original call site + * 2. Format the console arguments into a structured message + * 3. Create a ConsoleLogEvent with metadata + * 4. Store it in the buffer + * 5. Forward the call to the native console (so output still appears in DevTools) + */ window.console[method] = (...args: unknown[]): void => { + // Capture full stack trace const errorStack = new Error('Console log stack trace').stack; const stackLines = errorStack?.split('\n') || []; - // Skip first line (Error message) and find first stack frame outside of consoleCatcher module + /** + * Dynamic stack frame identification. + * + * Problem: Fixed slice(2) doesn't work reliably because the number of internal frames + * varies based on code structure (arrow functions, class methods, TS→JS transforms, etc.). + * + * Solution: Find the first stack frame that doesn't belong to consoleCatcher module. + * This ensures DevTools will navigate to the user's code, not the interceptor's code. + * + * Process: + * 1. Skip the first line (Error message: "Error: Console log stack trace") + * 2. Iterate through stack frames + * 3. Find first frame that doesn't contain "consoleCatcher" in its path + * 4. Use that frame as fileLine (clickable link in DevTools) + * 5. Use all subsequent frames as the full stack trace + */ const consoleCatcherPattern = /consoleCatcher/i; let userFrameIndex = 1; // Skip Error message line @@ -210,7 +244,9 @@ export class ConsoleCatcher { } } + // Extract user code stack (everything from the first non-consoleCatcher frame) const userStack = stackLines.slice(userFrameIndex).join('\n'); + // First frame is used as fileLine - this is what DevTools shows as clickable link const fileLine = stackLines[userFrameIndex]?.trim() || ''; const { message, styles } = this.formatConsoleArgs(args); @@ -225,6 +261,7 @@ export class ConsoleCatcher { }; this.addToConsoleOutput(logEvent); + // Forward to native console so output still appears in DevTools oldFunction(...args); }; }); From 5a5b924203d475de963026b1e77a1ccfd264582b Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Sat, 8 Nov 2025 18:41:25 +0300 Subject: [PATCH 14/14] chore: implement user stack extraction in ConsoleCatcher metod --- src/addons/consoleCatcher.ts | 71 +++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index 099aa1e..23a9326 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -140,6 +140,42 @@ export class ConsoleCatcher { }; } + /** + * Extracts user code stack trace from the full stack trace. + * + * Dynamic stack frame identification: + * - Problem: Fixed slice(2) doesn't work reliably because the number of internal frames + * varies based on code structure (arrow functions, class methods, TS→JS transforms, etc.). + * - Solution: Find the first stack frame that doesn't belong to consoleCatcher module. + * This ensures DevTools will navigate to the user's code, not the interceptor's code. + * + * @param errorStack - Full stack trace string from Error.stack + * @returns Object with userStack (full stack from user code) and fileLine (first frame for DevTools link) + */ + private extractUserStack(errorStack: string | undefined): { + userStack: string; + fileLine: string; + } { + const stackLines = errorStack?.split('\n') || []; + const consoleCatcherPattern = /consoleCatcher/i; + let userFrameIndex = 1; // Skip Error message line + + // Find first frame that doesn't belong to consoleCatcher module + for (let i = 1; i < stackLines.length; i++) { + if (!consoleCatcherPattern.test(stackLines[i])) { + userFrameIndex = i; + break; + } + } + + // Extract user code stack (everything from the first non-consoleCatcher frame) + const userStack = stackLines.slice(userFrameIndex).join('\n'); + // First frame is used as fileLine - this is what DevTools shows as clickable link + const fileLine = stackLines[userFrameIndex]?.trim() || ''; + + return { userStack, fileLine }; + } + /** * Adds a console log event to the output buffer * @@ -214,40 +250,9 @@ export class ConsoleCatcher { * 5. Forward the call to the native console (so output still appears in DevTools) */ window.console[method] = (...args: unknown[]): void => { - // Capture full stack trace + // Capture full stack trace and extract user code stack const errorStack = new Error('Console log stack trace').stack; - const stackLines = errorStack?.split('\n') || []; - - /** - * Dynamic stack frame identification. - * - * Problem: Fixed slice(2) doesn't work reliably because the number of internal frames - * varies based on code structure (arrow functions, class methods, TS→JS transforms, etc.). - * - * Solution: Find the first stack frame that doesn't belong to consoleCatcher module. - * This ensures DevTools will navigate to the user's code, not the interceptor's code. - * - * Process: - * 1. Skip the first line (Error message: "Error: Console log stack trace") - * 2. Iterate through stack frames - * 3. Find first frame that doesn't contain "consoleCatcher" in its path - * 4. Use that frame as fileLine (clickable link in DevTools) - * 5. Use all subsequent frames as the full stack trace - */ - const consoleCatcherPattern = /consoleCatcher/i; - let userFrameIndex = 1; // Skip Error message line - - for (let i = 1; i < stackLines.length; i++) { - if (!consoleCatcherPattern.test(stackLines[i])) { - userFrameIndex = i; - break; - } - } - - // Extract user code stack (everything from the first non-consoleCatcher frame) - const userStack = stackLines.slice(userFrameIndex).join('\n'); - // First frame is used as fileLine - this is what DevTools shows as clickable link - const fileLine = stackLines[userFrameIndex]?.trim() || ''; + const { userStack, fileLine } = this.extractUserStack(errorStack); const { message, styles } = this.formatConsoleArgs(args); const logEvent: ConsoleLogEvent = {