@@ -15,7 +15,11 @@ const MAX_LOGS = 20;
1515const CONSOLE_METHODS : string [ ] = [ 'log' , 'warn' , 'error' , 'info' , 'debug' ] ;
1616
1717/**
18- * Console catcher class for intercepting and capturing console logs
18+ * Console catcher class for intercepting and capturing console logs.
19+ *
20+ * This singleton class wraps native console methods to capture all console output with accurate
21+ * stack traces. When developers click on console messages in DevTools, they are taken to the
22+ * original call site in their code, not to the interceptor's code.
1923 */
2024export class ConsoleCatcher {
2125 /**
@@ -178,7 +182,10 @@ export class ConsoleCatcher {
178182 }
179183
180184 /**
181- * Initializes the console interceptor by overriding default console methods
185+ * Initializes the console interceptor by overriding default console methods.
186+ *
187+ * Wraps native console methods to intercept all calls, capture their context, and generate
188+ * accurate stack traces that point to the original call site (not the interceptor).
182189 */
183190 // eslint-disable-next-line @typescript-eslint/member-ordering
184191 public init ( ) : void {
@@ -193,13 +200,40 @@ export class ConsoleCatcher {
193200 return ;
194201 }
195202
203+ // Store original function to forward calls after interception
196204 const oldFunction = window . console [ method ] . bind ( window . console ) ;
197205
206+ /**
207+ * Override console method to intercept all calls.
208+ *
209+ * For each intercepted call, we:
210+ * 1. Generate a stack trace to find the original call site
211+ * 2. Format the console arguments into a structured message
212+ * 3. Create a ConsoleLogEvent with metadata
213+ * 4. Store it in the buffer
214+ * 5. Forward the call to the native console (so output still appears in DevTools)
215+ */
198216 window . console [ method ] = ( ...args : unknown [ ] ) : void => {
217+ // Capture full stack trace
199218 const errorStack = new Error ( 'Console log stack trace' ) . stack ;
200219 const stackLines = errorStack ?. split ( '\n' ) || [ ] ;
201220
202- // Skip first line (Error message) and find first stack frame outside of consoleCatcher module
221+ /**
222+ * Dynamic stack frame identification.
223+ *
224+ * Problem: Fixed slice(2) doesn't work reliably because the number of internal frames
225+ * varies based on code structure (arrow functions, class methods, TS→JS transforms, etc.).
226+ *
227+ * Solution: Find the first stack frame that doesn't belong to consoleCatcher module.
228+ * This ensures DevTools will navigate to the user's code, not the interceptor's code.
229+ *
230+ * Process:
231+ * 1. Skip the first line (Error message: "Error: Console log stack trace")
232+ * 2. Iterate through stack frames
233+ * 3. Find first frame that doesn't contain "consoleCatcher" in its path
234+ * 4. Use that frame as fileLine (clickable link in DevTools)
235+ * 5. Use all subsequent frames as the full stack trace
236+ */
203237 const consoleCatcherPattern = / c o n s o l e C a t c h e r / i;
204238 let userFrameIndex = 1 ; // Skip Error message line
205239
@@ -210,7 +244,9 @@ export class ConsoleCatcher {
210244 }
211245 }
212246
247+ // Extract user code stack (everything from the first non-consoleCatcher frame)
213248 const userStack = stackLines . slice ( userFrameIndex ) . join ( '\n' ) ;
249+ // First frame is used as fileLine - this is what DevTools shows as clickable link
214250 const fileLine = stackLines [ userFrameIndex ] ?. trim ( ) || '' ;
215251 const { message, styles } = this . formatConsoleArgs ( args ) ;
216252
@@ -225,6 +261,7 @@ export class ConsoleCatcher {
225261 } ;
226262
227263 this . addToConsoleOutput ( logEvent ) ;
264+ // Forward to native console so output still appears in DevTools
228265 oldFunction ( ...args ) ;
229266 } ;
230267 } ) ;
0 commit comments