Skip to content

safeStringify & %c support #121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Apr 8, 2025
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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"
}
}
198 changes: 150 additions & 48 deletions src/addons/consoleCatcher.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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;
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down