Skip to content

Commit d6c1221

Browse files
authored
Merge pull request #121 from codex-team/fix/safe-stringify
safeStringify & %c support
2 parents e213af2 + 8ba75e1 commit d6c1221

File tree

3 files changed

+156
-48
lines changed

3 files changed

+156
-48
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"dependencies": {
5050
"@hawk.so/types": "^0.1.20",
5151
"error-stack-parser": "^2.1.4",
52+
"safe-stringify": "^1.1.1",
5253
"vite-plugin-dts": "^4.2.4"
5354
}
5455
}

src/addons/consoleCatcher.ts

+150-48
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,110 @@
11
/**
22
* @file Module for intercepting console logs with stack trace capture
33
*/
4-
4+
import safeStringify from 'safe-stringify';
55
import type { ConsoleLogEvent } from '@hawk.so/types';
66

7-
const createConsoleCatcher = (): {
7+
/**
8+
* Creates a console interceptor that captures and formats console output
9+
*/
10+
function createConsoleCatcher(): {
811
initConsoleCatcher: () => void;
912
addErrorEvent: (event: ErrorEvent | PromiseRejectionEvent) => void;
1013
getConsoleLogStack: () => ConsoleLogEvent[];
11-
} => {
14+
} {
1215
const MAX_LOGS = 20;
1316
const consoleOutput: ConsoleLogEvent[] = [];
1417
let isInitialized = false;
1518

16-
const addToConsoleOutput = (logEvent: ConsoleLogEvent): void => {
19+
/**
20+
* Converts any argument to its string representation
21+
*
22+
* @param arg - Value to convert to string
23+
*/
24+
function stringifyArg(arg: unknown): string {
25+
if (typeof arg === 'string') {
26+
return arg;
27+
}
28+
if (typeof arg === 'number' || typeof arg === 'boolean') {
29+
return String(arg);
30+
}
31+
32+
return safeStringify(arg);
33+
}
34+
35+
/**
36+
* Formats console arguments handling %c directives
37+
*
38+
* @param args - Console arguments that may include style directives
39+
*/
40+
function formatConsoleArgs(args: unknown[]): {
41+
message: string;
42+
styles: string[];
43+
} {
44+
if (args.length === 0) {
45+
return {
46+
message: '',
47+
styles: [],
48+
};
49+
}
50+
51+
const firstArg = args[0];
52+
53+
if (typeof firstArg !== 'string' || !firstArg.includes('%c')) {
54+
return {
55+
message: args.map(stringifyArg).join(' '),
56+
styles: [],
57+
};
58+
}
59+
60+
// Handle %c formatting
61+
const message = args[0] as string;
62+
const styles: string[] = [];
63+
64+
// Extract styles from arguments
65+
let styleIndex = 0;
66+
67+
for (let i = 1; i < args.length; i++) {
68+
const arg = args[i];
69+
70+
if (typeof arg === 'string' && message.indexOf('%c', styleIndex) !== -1) {
71+
styles.push(arg);
72+
styleIndex = message.indexOf('%c', styleIndex) + 2;
73+
}
74+
}
75+
76+
// Add remaining arguments that aren't styles
77+
const remainingArgs = args
78+
.slice(styles.length + 1)
79+
.map(stringifyArg)
80+
.join(' ');
81+
82+
return {
83+
message: message + (remainingArgs ? ' ' + remainingArgs : ''),
84+
styles,
85+
};
86+
}
87+
88+
/**
89+
* Adds a console log event to the output buffer
90+
*
91+
* @param logEvent - The console log event to be added to the output buffer
92+
*/
93+
function addToConsoleOutput(logEvent: ConsoleLogEvent): void {
1794
if (consoleOutput.length >= MAX_LOGS) {
1895
consoleOutput.shift();
1996
}
2097
consoleOutput.push(logEvent);
21-
};
22-
23-
const createConsoleEventFromError = (
98+
}
99+
100+
/**
101+
* Creates a console log event from an error or promise rejection
102+
*
103+
* @param event - The error event or promise rejection event to convert
104+
*/
105+
function createConsoleEventFromError(
24106
event: ErrorEvent | PromiseRejectionEvent
25-
): ConsoleLogEvent => {
107+
): ConsoleLogEvent {
26108
if (event instanceof ErrorEvent) {
27109
return {
28110
method: 'error',
@@ -44,53 +126,73 @@ const createConsoleCatcher = (): {
44126
stack: event.reason?.stack || '',
45127
fileLine: '',
46128
};
47-
};
129+
}
130+
131+
/**
132+
* Initializes the console interceptor by overriding default console methods
133+
*/
134+
function initConsoleCatcher(): void {
135+
if (isInitialized) {
136+
return;
137+
}
48138

49-
return {
50-
initConsoleCatcher(): void {
51-
if (isInitialized) {
139+
isInitialized = true;
140+
const consoleMethods: string[] = ['log', 'warn', 'error', 'info', 'debug'];
141+
142+
consoleMethods.forEach(function overrideConsoleMethod(method) {
143+
if (typeof window.console[method] !== 'function') {
52144
return;
53145
}
54146

55-
isInitialized = true;
56-
const consoleMethods: string[] = ['log', 'warn', 'error', 'info', 'debug'];
57-
58-
consoleMethods.forEach((method) => {
59-
if (typeof window.console[method] !== 'function') {
60-
return;
61-
}
62-
63-
const oldFunction = window.console[method].bind(window.console);
64-
65-
window.console[method] = function (...args: unknown[]): void {
66-
const stack = new Error().stack?.split('\n').slice(2).join('\n') || '';
67-
68-
const logEvent: ConsoleLogEvent = {
69-
method,
70-
timestamp: new Date(),
71-
type: method,
72-
message: args.map((arg) => typeof arg === 'string' ? arg : JSON.stringify(arg)).join(' '),
73-
stack,
74-
fileLine: stack.split('\n')[0]?.trim(),
75-
};
76-
77-
addToConsoleOutput(logEvent);
78-
oldFunction(...args);
147+
const oldFunction = window.console[method].bind(window.console);
148+
149+
window.console[method] = function (...args: unknown[]): void {
150+
const stack = new Error().stack?.split('\n').slice(2)
151+
.join('\n') || '';
152+
const { message, styles } = formatConsoleArgs(args);
153+
154+
const logEvent: ConsoleLogEvent = {
155+
method,
156+
timestamp: new Date(),
157+
type: method,
158+
message,
159+
stack,
160+
fileLine: stack.split('\n')[0]?.trim(),
161+
styles,
79162
};
80-
});
81-
},
82163

83-
addErrorEvent(event: ErrorEvent | PromiseRejectionEvent): void {
84-
const logEvent = createConsoleEventFromError(event);
85-
86-
addToConsoleOutput(logEvent);
87-
},
164+
addToConsoleOutput(logEvent);
165+
oldFunction(...args);
166+
};
167+
});
168+
}
169+
170+
/**
171+
* Handles error events by converting them to console log events
172+
*
173+
* @param event - The error or promise rejection event to handle
174+
*/
175+
function addErrorEvent(event: ErrorEvent | PromiseRejectionEvent): void {
176+
const logEvent = createConsoleEventFromError(event);
177+
178+
addToConsoleOutput(logEvent);
179+
}
180+
181+
/**
182+
* Returns the current console output buffer
183+
*/
184+
function getConsoleLogStack(): ConsoleLogEvent[] {
185+
return [ ...consoleOutput ];
186+
}
88187

89-
getConsoleLogStack(): ConsoleLogEvent[] {
90-
return [ ...consoleOutput ];
91-
},
188+
return {
189+
initConsoleCatcher,
190+
addErrorEvent,
191+
getConsoleLogStack,
92192
};
93-
};
193+
}
94194

95195
const consoleCatcher = createConsoleCatcher();
96-
export const { initConsoleCatcher, getConsoleLogStack, addErrorEvent } = consoleCatcher;
196+
197+
export const { initConsoleCatcher, getConsoleLogStack, addErrorEvent } =
198+
consoleCatcher;

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -2619,6 +2619,11 @@ safe-regex-test@^1.0.3:
26192619
es-errors "^1.3.0"
26202620
is-regex "^1.1.4"
26212621

2622+
safe-stringify@^1.1.1:
2623+
version "1.1.1"
2624+
resolved "https://registry.yarnpkg.com/safe-stringify/-/safe-stringify-1.1.1.tgz#f4240f506d041f58374d6106e2a5850f6b1ce576"
2625+
integrity sha512-YSzQLuwp06fuvJD1h6+vVNFYZoXmDs5UUNPUbTvQK7Ap+L0qD4Vp+sN434C+pdS3prVVlUfQdNeiEIgxox/kUQ==
2626+
26222627
semver@^6.3.1:
26232628
version "6.3.1"
26242629
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"

0 commit comments

Comments
 (0)