diff --git a/package.json b/package.json index d0c3358..47c562b 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-rc.1", "description": "JavaScript errors tracking for Hawk.so", "files": [ "dist" @@ -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 3f02cd9..b3d0ac7 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -5,25 +5,99 @@ import safeStringify from 'safe-stringify'; import type { ConsoleLogEvent } from '@hawk.so/types'; /** - * 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; + 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); + }; + }); + } + + /** + * 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 - * - * @param arg - Value to convert to string - * @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; } @@ -36,10 +110,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[]; } { @@ -54,13 +126,7 @@ function createConsoleCatcher(): { if (typeof firstArg !== 'string' || !firstArg.includes('%c')) { return { - message: args.map(arg => { - try { - return stringifyArg(arg); - } catch (error) { - return '[Error stringifying argument: ' + (error instanceof Error ? error.message : String(error)) + ']'; - } - }).join(' '), + message: args.map((arg) => this.stringifyArg(arg)).join(' '), styles: [], }; } @@ -84,13 +150,7 @@ function createConsoleCatcher(): { // Add remaining arguments that aren't styles const remainingArgs = args .slice(styles.length + 1) - .map(arg => { - try { - return stringifyArg(arg); - } catch (error) { - return '[Error stringifying argument: ' + (error instanceof Error ? error.message : String(error)) + ']'; - } - }) + .map((arg) => this.stringifyArg(arg)) .join(' '); return { @@ -101,24 +161,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', @@ -126,9 +180,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}` : '', }; } @@ -141,72 +193,4 @@ function createConsoleCatcher(): { fileLine: '', }; } - - /** - * Initializes the console interceptor by overriding default console methods - */ - function initConsoleCatcher(): void { - if (isInitialized) { - return; - } - - isInitialized = true; - const consoleMethods: string[] = ['log', 'warn', 'error', 'info', 'debug']; - - consoleMethods.forEach(function overrideConsoleMethod(method) { - if (typeof window.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); - - const logEvent: ConsoleLogEvent = { - method, - timestamp: new Date(), - type: method, - message, - stack, - fileLine: stack.split('\n')[0]?.trim(), - styles, - }; - - addToConsoleOutput(logEvent); - oldFunction(...args); - }; - }); - } - - /** - * 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); - } - - /** - * Returns the current console output buffer - */ - function getConsoleLogStack(): ConsoleLogEvent[] { - return [ ...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 18a629f..692e383 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: { diff --git a/yarn.lock b/yarn.lock index cf1e367..863fc5f 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"