Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions 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.8",
"version": "3.2.9-rc.1",
"description": "JavaScript errors tracking for Hawk.so",
"files": [
"dist"
Expand Down Expand Up @@ -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"
Expand Down
210 changes: 97 additions & 113 deletions src/addons/consoleCatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,99 @@
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')) {

Check warning on line 45 in src/addons/consoleCatcher.ts

View workflow job for this annotation

GitHub Actions / lint

Expected blank line before this statement
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') || '';

Check warning on line 57 in src/addons/consoleCatcher.ts

View workflow job for this annotation

GitHub Actions / lint

Expected line break before `.join`
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);
};
});
}

/**

Check warning on line 82 in src/addons/consoleCatcher.ts

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @param "event" declaration
* Handles error events by converting them to console log events
*/
public addErrorEvent(event: ErrorEvent | PromiseRejectionEvent): void {
const logEvent = this.createConsoleEventFromError(event);
this.addToConsoleOutput(logEvent);

Check warning on line 87 in src/addons/consoleCatcher.ts

View workflow job for this annotation

GitHub Actions / lint

Expected blank line before this statement
}

/**
* Returns the current console output buffer
*/
public getConsoleLogStack(): ConsoleLogEvent[] {
return [...this.consoleOutput];

Check warning on line 94 in src/addons/consoleCatcher.ts

View workflow job for this annotation

GitHub Actions / lint

A space is required before ']'

Check warning on line 94 in src/addons/consoleCatcher.ts

View workflow job for this annotation

GitHub Actions / lint

A space is required after '['
}

/**

Check warning on line 97 in src/addons/consoleCatcher.ts

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @param "arg" declaration
* 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;
}
Expand All @@ -34,12 +108,10 @@
return safeStringify(arg);
}

/**

Check warning on line 111 in src/addons/consoleCatcher.ts

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @param "args" declaration
* 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[];
} {
Expand All @@ -54,13 +126,7 @@

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: [],
};
}
Expand All @@ -84,13 +150,7 @@
// 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 {
Expand All @@ -99,36 +159,28 @@
};
}

/**

Check warning on line 162 in src/addons/consoleCatcher.ts

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @param "logEvent" declaration
* 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);
}

/**

Check warning on line 172 in src/addons/consoleCatcher.ts

View workflow job for this annotation

GitHub Actions / lint

Missing JSDoc @param "event" declaration
* 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',
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}`
: '',
fileLine: event.filename ? `${event.filename}:${event.lineno}:${event.colno}` : '',
};
}

Expand All @@ -141,72 +193,4 @@
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;
18 changes: 12 additions & 6 deletions src/catcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -97,6 +97,11 @@ export default class Catcher {
*/
private readonly consoleTracking: boolean;

/**
* Console catcher instance
*/
private consoleCatcher: ConsoleCatcher | null = null;

/**
* Catcher constructor
*
Expand Down Expand Up @@ -143,7 +148,8 @@ export default class Catcher {
});

if (this.consoleTracking) {
initConsoleCatcher();
this.consoleCatcher = new ConsoleCatcher();
this.consoleCatcher.init();
}

/**
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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: {
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down