From 32e6b88e439bb5b20189c0ff1f4b34b1d812f608 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Mon, 12 May 2025 19:16:24 +0300 Subject: [PATCH 1/7] dev --- src/addons/consoleCatcher.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index 997a30c7..bff750ed 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -147,6 +147,16 @@ function createConsoleCatcher(): { const oldFunction = window.console[method].bind(window.console); window.console[method] = function (...args: unknown[]): void { + + /** + * If the console call originates from Vue's internal runtime bundle, skip interception + * to avoid capturing Vue-internal warnings and causing recursive loops. + */ + const rawStack = new Error().stack || ''; + if (rawStack.includes('runtime-core.esm-bundler.js')) { + return oldFunction(...args); + } + const stack = new Error().stack?.split('\n').slice(2) .join('\n') || ''; const { message, styles } = formatConsoleArgs(args); From a80d33a8a50451c5ae409af21d6d60f68ea52749 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Tue, 10 Jun 2025 18:06:17 +0300 Subject: [PATCH 2/7] dev --- src/addons/consoleCatcher.ts | 90 ++++++++++++------------------------ src/catcher.ts | 20 +++++--- src/types/console.ts | 41 ++++++++++++++++ 3 files changed, 83 insertions(+), 68 deletions(-) create mode 100644 src/types/console.ts diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index bff750ed..b97f1177 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -2,26 +2,20 @@ * @file Module for intercepting console logs with stack trace capture */ import safeStringify from 'safe-stringify'; -import type { ConsoleLogEvent } from '@hawk.so/types'; +import type { ExtendedConsoleLogEvent as ConsoleLogEvent } from '../types/console'; /** - * Creates a console interceptor that captures and formats console output + * Console interceptor that captures and formats console output */ -function createConsoleCatcher(): { - initConsoleCatcher: () => void; - addErrorEvent: (event: ErrorEvent | PromiseRejectionEvent) => void; - getConsoleLogStack: () => ConsoleLogEvent[]; - } { - const MAX_LOGS = 20; - const consoleOutput: ConsoleLogEvent[] = []; - let isInitialized = false; +export class ConsoleCatcher { + private readonly MAX_LOGS = 20; + private readonly consoleOutput: ConsoleLogEvent[] = []; + private isInitialized = false; /** * Converts any argument to its string representation - * - * @param arg - Value to convert to string */ - function stringifyArg(arg: unknown): string { + private stringifyArg(arg: unknown): string { if (typeof arg === 'string') { return arg; } @@ -34,10 +28,8 @@ function createConsoleCatcher(): { /** * Formats console arguments handling %c directives - * - * @param args - Console arguments that may include style directives */ - function formatConsoleArgs(args: unknown[]): { + private formatConsoleArgs(args: unknown[]): { message: string; styles: string[]; } { @@ -52,7 +44,7 @@ function createConsoleCatcher(): { if (typeof firstArg !== 'string' || !firstArg.includes('%c')) { return { - message: args.map(stringifyArg).join(' '), + message: args.map((arg) => this.stringifyArg(arg)).join(' '), styles: [], }; } @@ -76,7 +68,7 @@ function createConsoleCatcher(): { // Add remaining arguments that aren't styles const remainingArgs = args .slice(styles.length + 1) - .map(stringifyArg) + .map((arg) => this.stringifyArg(arg)) .join(' '); return { @@ -87,24 +79,18 @@ function createConsoleCatcher(): { /** * Adds a console log event to the output buffer - * - * @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 >= this.MAX_LOGS) { + this.consoleOutput.shift(); } - consoleOutput.push(logEvent); + this.consoleOutput.push(logEvent); } /** * Creates a console log event from an error or promise rejection - * - * @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', @@ -112,9 +98,7 @@ function createConsoleCatcher(): { type: event.error?.name || 'Error', message: event.error?.message || event.message, stack: event.error?.stack || '', - fileLine: event.filename - ? `${event.filename}:${event.lineno}:${event.colno}` - : '', + fileLine: event.filename ? `${event.filename}:${event.lineno}:${event.colno}` : '', }; } @@ -131,23 +115,22 @@ 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; const consoleMethods: string[] = ['log', 'warn', 'error', 'info', 'debug']; - consoleMethods.forEach(function overrideConsoleMethod(method) { + consoleMethods.forEach((method) => { if (typeof window.console[method] !== 'function') { return; } const oldFunction = window.console[method].bind(window.console); - window.console[method] = function (...args: unknown[]): void { - + window.console[method] = (...args: unknown[]): void => { /** * If the console call originates from Vue's internal runtime bundle, skip interception * to avoid capturing Vue-internal warnings and causing recursive loops. @@ -157,9 +140,8 @@ function createConsoleCatcher(): { return oldFunction(...args); } - const stack = new Error().stack?.split('\n').slice(2) - .join('\n') || ''; - const { message, styles } = formatConsoleArgs(args); + const stack = new Error().stack?.split('\n').slice(2).join('\n') || ''; + const { message, styles } = this.formatConsoleArgs(args); const logEvent: ConsoleLogEvent = { method, @@ -171,7 +153,7 @@ function createConsoleCatcher(): { styles, }; - addToConsoleOutput(logEvent); + this.addToConsoleOutput(logEvent); oldFunction(...args); }; }); @@ -179,30 +161,16 @@ function createConsoleCatcher(): { /** * Handles error events by converting them to console log events - * - * @param event - The error or promise rejection event to handle */ - function addErrorEvent(event: ErrorEvent | PromiseRejectionEvent): void { - const logEvent = createConsoleEventFromError(event); - - addToConsoleOutput(logEvent); + public addErrorEvent(event: ErrorEvent | PromiseRejectionEvent): void { + const logEvent = this.createConsoleEventFromError(event); + 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 62ba63ac..7191c2f8 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'; /** * Allow to use global VERSION, that will be overwritten by Webpack @@ -97,6 +97,11 @@ export default class Catcher { */ private readonly consoleTracking: boolean; + /** + * Console catcher instance + */ + private consoleCatcher: ConsoleCatcher | null = null; + /** * Catcher constructor * @@ -143,7 +148,8 @@ export default class Catcher { }); if (this.consoleTracking) { - initConsoleCatcher(); + this.consoleCatcher = new ConsoleCatcher(); + this.consoleCatcher.init(); } /** @@ -244,9 +250,8 @@ export default class Catcher { /** * Add error to console logs */ - - if (this.consoleTracking) { - addErrorEvent(event); + if (this.consoleTracking && this.consoleCatcher) { + this.consoleCatcher.addErrorEvent(event); } /** @@ -513,7 +518,8 @@ 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 ? this.consoleCatcher.getConsoleLogStack() : null; const addons: JavaScriptAddons = { window: { @@ -533,7 +539,7 @@ export default class Catcher { } if (consoleLogs && consoleLogs.length > 0) { - addons.consoleOutput = consoleLogs; + (addons as any).consoleOutput = consoleLogs; } return addons; diff --git a/src/types/console.ts b/src/types/console.ts new file mode 100644 index 00000000..776582e9 --- /dev/null +++ b/src/types/console.ts @@ -0,0 +1,41 @@ +import type { ConsoleLogEvent as BaseConsoleLogEvent } from '@hawk.so/types'; + +/** + * Extended ConsoleLogEvent with styles support for local usage + */ +export interface ExtendedConsoleLogEvent extends Omit { + /** + * Log method used (e.g., "log", "warn", "error") + */ + method: string; + + /** + * Timestamp of the log event + */ + timestamp: Date; + + /** + * Type of the log message (e.g., "error", "warning", "info") + */ + type: string; + + /** + * The main log message + */ + message: string; + + /** + * Stack trace if available + */ + stack?: string; + + /** + * File and line number where the log occurred + */ + fileLine?: string; + + /** + * CSS styles for %c formatting + */ + styles?: string[]; +} From b0eca8e65eede42d2c6cdea26dc282fea252bb40 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:55:20 +0300 Subject: [PATCH 3/7] isProcessing --- src/addons/consoleCatcher.ts | 44 ++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index b97f1177..174a7948 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -11,6 +11,7 @@ export class ConsoleCatcher { private readonly MAX_LOGS = 20; private readonly consoleOutput: ConsoleLogEvent[] = []; private isInitialized = false; + private isProcessing = false; /** * Converts any argument to its string representation @@ -131,6 +132,11 @@ export class ConsoleCatcher { const oldFunction = window.console[method].bind(window.console); window.console[method] = (...args: unknown[]): void => { + // Prevent recursive calls + if (this.isProcessing) { + return oldFunction(...args); + } + /** * If the console call originates from Vue's internal runtime bundle, skip interception * to avoid capturing Vue-internal warnings and causing recursive loops. @@ -140,20 +146,34 @@ export class ConsoleCatcher { return oldFunction(...args); } - const stack = new Error().stack?.split('\n').slice(2).join('\n') || ''; - const { message, styles } = this.formatConsoleArgs(args); + // Additional protection against Hawk internal calls + if (rawStack.includes('hawk.javascript') || rawStack.includes('@hawk.so')) { + return oldFunction(...args); + } - const logEvent: ConsoleLogEvent = { - method, - timestamp: new Date(), - type: method, - message, - stack, - fileLine: stack.split('\n')[0]?.trim(), - styles, - }; + this.isProcessing = true; + + try { + const stack = new Error().stack?.split('\n').slice(2).join('\n') || ''; + const { message, styles } = this.formatConsoleArgs(args); + + const logEvent: ConsoleLogEvent = { + method, + timestamp: new Date(), + type: method, + message, + stack, + fileLine: stack.split('\n')[0]?.trim(), + styles, + }; + + this.addToConsoleOutput(logEvent); + } catch (error) { + // Silently ignore errors in console processing to prevent infinite loops + } finally { + this.isProcessing = false; + } - this.addToConsoleOutput(logEvent); oldFunction(...args); }; }); From 879c184a669ecdcb1a44b04bec7ff6faac6938f2 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:20:16 +0300 Subject: [PATCH 4/7] dev --- package.json | 2 +- src/addons/consoleCatcher.ts | 2 +- src/types/console.ts | 41 ------------------------------------ yarn.lock | 8 +++---- 4 files changed, 6 insertions(+), 47 deletions(-) delete mode 100644 src/types/console.ts diff --git a/package.json b/package.json index 87ea539f..e3358547 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "vue": "^2" }, "dependencies": { - "@hawk.so/types": "^0.1.20", + "@hawk.so/types": "^0.1.30", "error-stack-parser": "^2.1.4", "safe-stringify": "^1.1.1", "vite-plugin-dts": "^4.2.4" diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index 174a7948..f5ff1896 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -2,7 +2,7 @@ * @file Module for intercepting console logs with stack trace capture */ import safeStringify from 'safe-stringify'; -import type { ExtendedConsoleLogEvent as ConsoleLogEvent } from '../types/console'; +import { ConsoleLogEvent } from '@hawk.so/types'; /** * Console interceptor that captures and formats console output diff --git a/src/types/console.ts b/src/types/console.ts deleted file mode 100644 index 776582e9..00000000 --- a/src/types/console.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { ConsoleLogEvent as BaseConsoleLogEvent } from '@hawk.so/types'; - -/** - * Extended ConsoleLogEvent with styles support for local usage - */ -export interface ExtendedConsoleLogEvent extends Omit { - /** - * Log method used (e.g., "log", "warn", "error") - */ - method: string; - - /** - * Timestamp of the log event - */ - timestamp: Date; - - /** - * Type of the log message (e.g., "error", "warning", "info") - */ - type: string; - - /** - * The main log message - */ - message: string; - - /** - * Stack trace if available - */ - stack?: string; - - /** - * File and line number where the log occurred - */ - fileLine?: string; - - /** - * CSS styles for %c formatting - */ - styles?: string[]; -} diff --git a/yarn.lock b/yarn.lock index cf1e3673..863fc5f6 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.20": - version "0.1.20" - resolved "https://registry.yarnpkg.com/@hawk.so/types/-/types-0.1.20.tgz#90f4b3998ef5f025f5b99dae31da264d5bbe3450" - integrity sha512-3a07TekmgqOT9OKeMkqcV73NxzK1dS06pG66VaHO0f5DEEH2+SNfZErqe1v8hkLQIk+GkgZVZLtHqnskjQabuw== +"@hawk.so/types@^0.1.30": + version "0.1.30" + resolved "https://registry.yarnpkg.com/@hawk.so/types/-/types-0.1.30.tgz#0002fe4854bd48d050ded00195a3935d082cef20" + integrity sha512-2elLi5HM1/g5Xs6t9c2/iEd1pkT1fL+oFv9iSs+xZPFCxKHJLDgzoNB5dAEuSuiNmJ7bc4byNDrSd88e2GBhWw== dependencies: "@types/mongodb" "^3.5.34" From 93de9e4d8ce1250fed9abcbbbb087a42a0e55a03 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Sun, 31 Aug 2025 13:37:17 +0300 Subject: [PATCH 5/7] Update version to 3.2.9 in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 31fe142e..eb848a98 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@hawk.so/javascript", "type": "commonjs", - "version": "3.2.8", + "version": "3.2.9", "description": "JavaScript errors tracking for Hawk.so", "files": [ "dist" From bd53ce9019796f892f4f472deb4db083b0eae7da Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Sun, 31 Aug 2025 14:12:04 +0300 Subject: [PATCH 6/7] Update version to 3.2.9-rc.1 in package.json and refactor consoleCatcher to improve console method interception and prevent infinite loops. --- package.json | 2 +- src/addons/consoleCatcher.ts | 134 +++++++++++++++++------------------ 2 files changed, 68 insertions(+), 68 deletions(-) diff --git a/package.json b/package.json index eb848a98..47c562bd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@hawk.so/javascript", "type": "commonjs", - "version": "3.2.9", + "version": "3.2.9-rc.1", "description": "JavaScript errors tracking for Hawk.so", "files": [ "dist" diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index f5ff1896..af9f1719 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -2,7 +2,7 @@ * @file Module for intercepting console logs with stack trace capture */ import safeStringify from 'safe-stringify'; -import { ConsoleLogEvent } from '@hawk.so/types'; +import type { ConsoleLogEvent } from '@hawk.so/types'; /** * Console interceptor that captures and formats console output @@ -13,6 +13,72 @@ export class ConsoleCatcher { private isInitialized = false; private isProcessing = false; + /** + * Initializes the console interceptor by overriding default console methods + */ + public init(): void { + if (this.isInitialized) { + return; + } + + this.isInitialized = true; + const consoleMethods: string[] = ['log', 'warn', 'error', 'info', 'debug']; + + consoleMethods.forEach((method) => { + if (typeof window.console[method] !== 'function') { + return; + } + + const oldFunction = window.console[method].bind(window.console); + + window.console[method] = (...args: unknown[]): void => { + // Prevent recursive calls + if (this.isProcessing) { + return oldFunction(...args); + } + + /** + * If the console call originates from Vue's internal runtime bundle, skip interception + * to avoid capturing Vue-internal warnings and causing recursive loops. + */ + const rawStack = new Error().stack || ''; + if (rawStack.includes('runtime-core.esm-bundler.js')) { + return oldFunction(...args); + } + + // Additional protection against Hawk internal calls + if (rawStack.includes('hawk.javascript') || rawStack.includes('@hawk.so')) { + return oldFunction(...args); + } + + this.isProcessing = true; + + try { + const stack = new Error().stack?.split('\n').slice(2).join('\n') || ''; + const { message, styles } = this.formatConsoleArgs(args); + + const logEvent: ConsoleLogEvent = { + method, + timestamp: new Date(), + type: method, + message, + stack, + fileLine: stack.split('\n')[0]?.trim(), + styles, + }; + + this.addToConsoleOutput(logEvent); + } catch (error) { + // Silently ignore errors in console processing to prevent infinite loops + } finally { + this.isProcessing = false; + } + + oldFunction(...args); + }; + }); + } + /** * Converts any argument to its string representation */ @@ -113,72 +179,6 @@ export class ConsoleCatcher { }; } - /** - * Initializes the console interceptor by overriding default console methods - */ - public init(): void { - if (this.isInitialized) { - return; - } - - this.isInitialized = true; - const consoleMethods: string[] = ['log', 'warn', 'error', 'info', 'debug']; - - consoleMethods.forEach((method) => { - if (typeof window.console[method] !== 'function') { - return; - } - - const oldFunction = window.console[method].bind(window.console); - - window.console[method] = (...args: unknown[]): void => { - // Prevent recursive calls - if (this.isProcessing) { - return oldFunction(...args); - } - - /** - * If the console call originates from Vue's internal runtime bundle, skip interception - * to avoid capturing Vue-internal warnings and causing recursive loops. - */ - const rawStack = new Error().stack || ''; - if (rawStack.includes('runtime-core.esm-bundler.js')) { - return oldFunction(...args); - } - - // Additional protection against Hawk internal calls - if (rawStack.includes('hawk.javascript') || rawStack.includes('@hawk.so')) { - return oldFunction(...args); - } - - this.isProcessing = true; - - try { - const stack = new Error().stack?.split('\n').slice(2).join('\n') || ''; - const { message, styles } = this.formatConsoleArgs(args); - - const logEvent: ConsoleLogEvent = { - method, - timestamp: new Date(), - type: method, - message, - stack, - fileLine: stack.split('\n')[0]?.trim(), - styles, - }; - - this.addToConsoleOutput(logEvent); - } catch (error) { - // Silently ignore errors in console processing to prevent infinite loops - } finally { - this.isProcessing = false; - } - - oldFunction(...args); - }; - }); - } - /** * Handles error events by converting them to console log events */ From 8d66d944d2d31b196d552b66bd7249706925ff04 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Sun, 31 Aug 2025 14:16:15 +0300 Subject: [PATCH 7/7] fix lint --- src/addons/consoleCatcher.ts | 30 +++++++++++++++--------------- src/catcher.ts | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index af9f1719..b3d0ac7d 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -79,6 +79,21 @@ export class ConsoleCatcher { }); } + /** + * Handles error events by converting them to console log events + */ + public addErrorEvent(event: ErrorEvent | PromiseRejectionEvent): void { + const logEvent = this.createConsoleEventFromError(event); + this.addToConsoleOutput(logEvent); + } + + /** + * Returns the current console output buffer + */ + public getConsoleLogStack(): ConsoleLogEvent[] { + return [...this.consoleOutput]; + } + /** * Converts any argument to its string representation */ @@ -178,19 +193,4 @@ export class ConsoleCatcher { fileLine: '', }; } - - /** - * Handles error events by converting them to console log events - */ - public addErrorEvent(event: ErrorEvent | PromiseRejectionEvent): void { - const logEvent = this.createConsoleEventFromError(event); - this.addToConsoleOutput(logEvent); - } - - /** - * Returns the current console output buffer - */ - public getConsoleLogStack(): ConsoleLogEvent[] { - return [...this.consoleOutput]; - } } diff --git a/src/catcher.ts b/src/catcher.ts index 442f224c..692e3839 100644 --- a/src/catcher.ts +++ b/src/catcher.ts @@ -539,7 +539,7 @@ export default class Catcher { } if (consoleLogs && consoleLogs.length > 0) { - (addons as any).consoleOutput = consoleLogs; + addons.consoleOutput = consoleLogs; } return addons;