diff --git a/package.json b/package.json index 894332a..87e029b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@hawk.so/javascript", "type": "commonjs", - "version": "3.2.2", + "version": "3.2.3", "description": "JavaScript errors tracking for Hawk.so", "files": [ "dist" @@ -49,6 +49,7 @@ "dependencies": { "@hawk.so/types": "^0.1.20", "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 fc8fa13..997a30c 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -1,28 +1,110 @@ /** * @file Module for intercepting console logs with stack trace capture */ - +import safeStringify from 'safe-stringify'; import type { ConsoleLogEvent } from '@hawk.so/types'; -const createConsoleCatcher = (): { +/** + * Creates a 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; - const addToConsoleOutput = (logEvent: ConsoleLogEvent): void => { + /** + * Converts any argument to its string representation + * + * @param arg - Value to convert to string + */ + function stringifyArg(arg: unknown): string { + if (typeof arg === 'string') { + return arg; + } + if (typeof arg === 'number' || typeof arg === 'boolean') { + return String(arg); + } + + return safeStringify(arg); + } + + /** + * Formats console arguments handling %c directives + * + * @param args - Console arguments that may include style directives + */ + function formatConsoleArgs(args: unknown[]): { + message: string; + styles: string[]; + } { + if (args.length === 0) { + return { + message: '', + styles: [], + }; + } + + const firstArg = args[0]; + + if (typeof firstArg !== 'string' || !firstArg.includes('%c')) { + return { + message: args.map(stringifyArg).join(' '), + styles: [], + }; + } + + // Handle %c formatting + const message = args[0] as string; + const styles: string[] = []; + + // Extract styles from arguments + let styleIndex = 0; + + for (let i = 1; i < args.length; i++) { + const arg = args[i]; + + if (typeof arg === 'string' && message.indexOf('%c', styleIndex) !== -1) { + styles.push(arg); + styleIndex = message.indexOf('%c', styleIndex) + 2; + } + } + + // Add remaining arguments that aren't styles + const remainingArgs = args + .slice(styles.length + 1) + .map(stringifyArg) + .join(' '); + + return { + message: message + (remainingArgs ? ' ' + remainingArgs : ''), + styles, + }; + } + + /** + * 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(); } consoleOutput.push(logEvent); - }; - - const createConsoleEventFromError = ( + } + + /** + * 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 => { + ): ConsoleLogEvent { if (event instanceof ErrorEvent) { return { method: 'error', @@ -44,53 +126,73 @@ const createConsoleCatcher = (): { stack: event.reason?.stack || '', fileLine: '', }; - }; + } + + /** + * Initializes the console interceptor by overriding default console methods + */ + function initConsoleCatcher(): void { + if (isInitialized) { + return; + } - return { - initConsoleCatcher(): void { - if (isInitialized) { + isInitialized = true; + const consoleMethods: string[] = ['log', 'warn', 'error', 'info', 'debug']; + + consoleMethods.forEach(function overrideConsoleMethod(method) { + if (typeof window.console[method] !== 'function') { return; } - 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] = function (...args: unknown[]): void { - const stack = new Error().stack?.split('\n').slice(2).join('\n') || ''; - - const logEvent: ConsoleLogEvent = { - method, - timestamp: new Date(), - type: method, - message: args.map((arg) => typeof arg === 'string' ? arg : JSON.stringify(arg)).join(' '), - stack, - fileLine: stack.split('\n')[0]?.trim(), - }; - - addToConsoleOutput(logEvent); - oldFunction(...args); + 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, }; - }); - }, - addErrorEvent(event: ErrorEvent | PromiseRejectionEvent): void { - const logEvent = createConsoleEventFromError(event); - - addToConsoleOutput(logEvent); - }, + 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 ]; + } - getConsoleLogStack(): ConsoleLogEvent[] { - return [ ...consoleOutput ]; - }, + return { + initConsoleCatcher, + addErrorEvent, + getConsoleLogStack, }; -}; +} const consoleCatcher = createConsoleCatcher(); -export const { initConsoleCatcher, getConsoleLogStack, addErrorEvent } = consoleCatcher; + +export const { initConsoleCatcher, getConsoleLogStack, addErrorEvent } = + consoleCatcher; diff --git a/yarn.lock b/yarn.lock index a3a87c2..cf1e367 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2619,6 +2619,11 @@ safe-regex-test@^1.0.3: es-errors "^1.3.0" is-regex "^1.1.4" +safe-stringify@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/safe-stringify/-/safe-stringify-1.1.1.tgz#f4240f506d041f58374d6106e2a5850f6b1ce576" + integrity sha512-YSzQLuwp06fuvJD1h6+vVNFYZoXmDs5UUNPUbTvQK7Ap+L0qD4Vp+sN434C+pdS3prVVlUfQdNeiEIgxox/kUQ== + semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"