diff --git a/src/addons/consoleCatcher.ts b/src/addons/consoleCatcher.ts index 77de467..fc8fa13 100644 --- a/src/addons/consoleCatcher.ts +++ b/src/addons/consoleCatcher.ts @@ -1,48 +1,96 @@ /** - * @file Integration for catching console logs and other info + * @file Module for intercepting console logs with stack trace capture */ -interface ConsoleLogEvent { - /** - * Window.console object method (i.e. log, info, warn) - */ - method: string; +import type { ConsoleLogEvent } from '@hawk.so/types'; - /** - * Time when the log was occurred - */ - timestamp: Date; +const createConsoleCatcher = (): { + initConsoleCatcher: () => void; + addErrorEvent: (event: ErrorEvent | PromiseRejectionEvent) => void; + getConsoleLogStack: () => ConsoleLogEvent[]; +} => { + const MAX_LOGS = 20; + const consoleOutput: ConsoleLogEvent[] = []; + let isInitialized = false; - /** - * Log argument - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - args: any; -} - -/** - * Contains all data that will be logged by window.console - */ -const consoleOutput: ConsoleLogEvent[] = []; + const addToConsoleOutput = (logEvent: ConsoleLogEvent): void => { + if (consoleOutput.length >= MAX_LOGS) { + consoleOutput.shift(); + } + consoleOutput.push(logEvent); + }; -// Override console methods -Object.keys(window.console).forEach(key => { - const oldFunction = window.console[key]; + const createConsoleEventFromError = ( + event: ErrorEvent | PromiseRejectionEvent + ): ConsoleLogEvent => { + if (event instanceof ErrorEvent) { + return { + method: 'error', + timestamp: new Date(), + type: event.error?.name || 'Error', + message: event.error?.message || event.message, + stack: event.error?.stack || '', + fileLine: event.filename + ? `${event.filename}:${event.lineno}:${event.colno}` + : '', + }; + } - window.console[key] = function (...args): void { - consoleOutput.push({ - method: key, + return { + method: 'error', timestamp: new Date(), - args, - }); - oldFunction.apply(window.console, args); + type: 'UnhandledRejection', + message: event.reason?.message || String(event.reason), + stack: event.reason?.stack || '', + fileLine: '', + }; }; -}); -/** - * @param event - event to modify - * @param data - event data - */ -export default function (event, data): void { - data.payload.consoleOutput = consoleOutput; -} + return { + initConsoleCatcher(): void { + if (isInitialized) { + 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); + }; + }); + }, + + addErrorEvent(event: ErrorEvent | PromiseRejectionEvent): void { + const logEvent = createConsoleEventFromError(event); + + addToConsoleOutput(logEvent); + }, + + getConsoleLogStack(): ConsoleLogEvent[] { + return [ ...consoleOutput ]; + }, + }; +}; + +const consoleCatcher = createConsoleCatcher(); +export const { initConsoleCatcher, getConsoleLogStack, addErrorEvent } = consoleCatcher; diff --git a/src/catcher.ts b/src/catcher.ts index fb2207c..400ac6d 100644 --- a/src/catcher.ts +++ b/src/catcher.ts @@ -16,6 +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'; /** * Allow to use global VERSION, that will be overwritten by Webpack @@ -130,6 +131,8 @@ export default class Catcher { }, }); + initConsoleCatcher(); + /** * Set global handlers */ @@ -225,6 +228,12 @@ export default class Catcher { * @param {ErrorEvent|PromiseRejectionEvent} event — (!) both for Error and Promise Rejection */ private async handleEvent(event: ErrorEvent | PromiseRejectionEvent): Promise { + /** + * Add error to console logs + */ + + addErrorEvent(event); + /** * Promise rejection reason is recommended to be an Error, but it can be a string: * - Promise.reject(new Error('Reason message')) ——— recommended @@ -489,6 +498,7 @@ export default class Catcher { const userAgent = window.navigator.userAgent; const location = window.location.href; const getParams = this.getGetParams(); + const consoleLogs = getConsoleLogStack(); const addons: JavaScriptAddons = { window: { @@ -507,6 +517,10 @@ export default class Catcher { addons.RAW_EVENT_DATA = this.getRawData(error); } + if (consoleLogs.length > 0) { + addons.consoleOutput = consoleLogs; + } + return addons; }