Skip to content

fix(catcher-js): prevent infinite cycle issue #126

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
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
154 changes: 69 additions & 85 deletions src/addons/consoleCatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,21 @@
* @file Module for intercepting console logs with stack trace capture
*/
import safeStringify from 'safe-stringify';
import type { ConsoleLogEvent } from '@hawk.so/types';
import { ConsoleLogEvent } from '@hawk.so/types';

Check failure on line 5 in src/addons/consoleCatcher.ts

View workflow job for this annotation

GitHub Actions / lint

All imports in the declaration are only used as types. Use `import type`

/**
* 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;

/**

Check warning on line 16 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 +27,10 @@
return safeStringify(arg);
}

/**

Check warning on line 30 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 +45,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 +69,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 +78,28 @@
};
}

/**

Check warning on line 81 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 91 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 @@ -145,68 +116,81 @@
/**
* Initializes the console interceptor by overriding default console methods
*/
function initConsoleCatcher(): void {
if (isInitialized) {
public init(): void {

Check failure on line 119 in src/addons/consoleCatcher.ts

View workflow job for this annotation

GitHub Actions / lint

Member init should be declared before all private instance method definitions
if (this.isInitialized) {
return;
}

isInitialized = true;
this.isInitialized = true;
const consoleMethods: string[] = ['log', 'warn', 'error', 'info', 'debug'];

consoleMethods.forEach(function overrideConsoleMethod(method) {
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 { message, styles } = formatConsoleArgs(args);

const logEvent: ConsoleLogEvent = {
method,
timestamp: new Date(),
type: method,
message,
stack,
fileLine: stack.split('\n')[0]?.trim(),
styles,
};

addToConsoleOutput(logEvent);
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 145 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 157 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 182 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
*
* @param event - The error or promise rejection event to handle
*/
function addErrorEvent(event: ErrorEvent | PromiseRejectionEvent): void {
const logEvent = createConsoleEventFromError(event);

addToConsoleOutput(logEvent);
public addErrorEvent(event: ErrorEvent | PromiseRejectionEvent): void {

Check failure on line 185 in src/addons/consoleCatcher.ts

View workflow job for this annotation

GitHub Actions / lint

Member addErrorEvent should be declared before all private instance method definitions
const logEvent = this.createConsoleEventFromError(event);
this.addToConsoleOutput(logEvent);

Check warning on line 187 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
*/
function getConsoleLogStack(): ConsoleLogEvent[] {
return [ ...consoleOutput ];
public getConsoleLogStack(): ConsoleLogEvent[] {

Check failure on line 193 in src/addons/consoleCatcher.ts

View workflow job for this annotation

GitHub Actions / lint

Member getConsoleLogStack should be declared before all private instance method definitions
return [...this.consoleOutput];

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

View workflow job for this annotation

GitHub Actions / lint

A space is required before ']'

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

View workflow job for this annotation

GitHub Actions / lint

A space is required after '['
}

return {
initConsoleCatcher,
addErrorEvent,
getConsoleLogStack,
};
}

const consoleCatcher = createConsoleCatcher();

export const { initConsoleCatcher, getConsoleLogStack, addErrorEvent } =
consoleCatcher;
20 changes: 13 additions & 7 deletions src/catcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
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 @@
*/
private readonly consoleTracking: boolean;

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

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

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

/**
Expand Down Expand Up @@ -244,9 +250,8 @@
/**
* 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 @@
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 All @@ -533,7 +539,7 @@
}

if (consoleLogs && consoleLogs.length > 0) {
addons.consoleOutput = consoleLogs;
(addons as any).consoleOutput = consoleLogs;

Check failure on line 542 in src/catcher.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
}

return addons;
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
Loading