@dynamicu/chromedebug-mcp 2.6.7 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CLAUDE.md +17 -1
  2. package/README.md +1 -1
  3. package/chrome-extension/activation-manager.js +10 -10
  4. package/chrome-extension/background.js +1045 -736
  5. package/chrome-extension/browser-recording-manager.js +1 -1
  6. package/chrome-extension/chrome-debug-logger.js +168 -0
  7. package/chrome-extension/chrome-session-manager.js +5 -5
  8. package/chrome-extension/console-interception-library.js +430 -0
  9. package/chrome-extension/content.css +16 -16
  10. package/chrome-extension/content.js +739 -221
  11. package/chrome-extension/data-buffer.js +5 -5
  12. package/chrome-extension/dom-tracker.js +9 -9
  13. package/chrome-extension/extension-config.js +1 -1
  14. package/chrome-extension/firebase-client.js +13 -13
  15. package/chrome-extension/frame-capture.js +20 -38
  16. package/chrome-extension/license-helper.js +33 -7
  17. package/chrome-extension/manifest.free.json +3 -6
  18. package/chrome-extension/network-tracker.js +9 -9
  19. package/chrome-extension/options.html +10 -0
  20. package/chrome-extension/options.js +21 -8
  21. package/chrome-extension/performance-monitor.js +17 -17
  22. package/chrome-extension/popup.html +230 -193
  23. package/chrome-extension/popup.js +146 -458
  24. package/chrome-extension/pro/enhanced-capture.js +406 -0
  25. package/chrome-extension/pro/frame-editor.html +433 -0
  26. package/chrome-extension/pro/frame-editor.js +1567 -0
  27. package/chrome-extension/pro/function-tracker.js +843 -0
  28. package/chrome-extension/pro/jszip.min.js +13 -0
  29. package/chrome-extension/upload-manager.js +7 -7
  30. package/dist/chromedebug-extension-free.zip +0 -0
  31. package/package.json +3 -1
  32. package/scripts/webpack.config.free.cjs +8 -8
  33. package/scripts/webpack.config.pro.cjs +2 -0
  34. package/src/cli.js +2 -2
  35. package/src/database.js +55 -7
  36. package/src/index.js +9 -6
  37. package/src/mcp/server.js +2 -2
  38. package/src/services/process-manager.js +10 -6
  39. package/src/services/process-tracker.js +10 -5
  40. package/src/services/profile-manager.js +17 -2
  41. package/src/validation/schemas.js +12 -11
  42. package/src/index-direct.js +0 -157
  43. package/src/index-modular.js +0 -219
  44. package/src/index-monolithic-backup.js +0 -2230
  45. package/src/legacy/chrome-controller-old.js +0 -1406
  46. package/src/legacy/index-express.js +0 -625
  47. package/src/legacy/index-old.js +0 -977
  48. package/src/legacy/routes.js +0 -260
  49. package/src/legacy/shared-storage.js +0 -101
@@ -0,0 +1,843 @@
1
+ // Function Tracker for Chrome Debug Full Data Recording
2
+ // Wraps JavaScript functions to track execution flow and variable states
3
+
4
+ // Constants for batching and flushing
5
+ const DEFAULT_BATCH_SIZE = 100;
6
+ const FLUSH_INTERVAL_MS = 5000;
7
+
8
+ class FunctionTracker {
9
+ constructor(performanceMonitor, piiRedactor, onDataReady) {
10
+ this.isTracking = false;
11
+ this.trackedFunctions = new Map(); // Original function references
12
+ this.executionBuffer = [];
13
+ this.bufferFlushDelay = 3000; // Flush every 3 seconds
14
+ this.flushTimer = null;
15
+ this.recordingId = null;
16
+ this.callStack = []; // Track call hierarchy
17
+
18
+ // Configuration - LIGHTWEIGHT MODE BY DEFAULT
19
+ this.maxCallDepth = 5; // Reduced from 10
20
+ this.maxArgumentSize = 50; // DRASTICALLY reduced from 1024 - just capture basic info
21
+ this.trackReturnValues = false; // DON'T track return values
22
+ this.trackExecutionTime = true;
23
+ this.trackCallStack = false; // DON'T track full call stacks
24
+ this.lightweightMode = true; // NEW: Enable lightweight mode
25
+ this.batchSize = DEFAULT_BATCH_SIZE;
26
+
27
+ // Sensitive data patterns to redact
28
+ this.sensitivePatterns = [
29
+ 'password', 'passwd', 'pwd',
30
+ 'token', 'apikey', 'api_key', 'apiKey',
31
+ 'secret', 'private', 'credential',
32
+ 'authorization', 'auth',
33
+ 'ssn', 'social_security',
34
+ 'credit_card', 'creditcard', 'cc_number',
35
+ 'cvv', 'cvc',
36
+ 'pin', 'passcode'
37
+ ];
38
+
39
+ // Callbacks and dependencies
40
+ this.onDataReady = onDataReady || null;
41
+ this.piiRedactor = piiRedactor || null;
42
+ this.performanceMonitor = performanceMonitor || null;
43
+
44
+ // Create bound flush method for event listeners
45
+ this.boundFlush = this.flush.bind(this);
46
+ this.flushInterval = null;
47
+
48
+ // Function categories to wrap
49
+ this.targetCategories = {
50
+ userDefined: true, // User-defined functions
51
+ eventHandlers: true, // Event handlers
52
+ asyncFunctions: true, // Promises, async/await
53
+ domMethods: false, // DOM manipulation (too noisy by default)
54
+ nativeAPIs: false // Native browser APIs (very noisy)
55
+ };
56
+
57
+ // Patterns for function identification
58
+ this.wrappingPatterns = {
59
+ // User-defined functions (detected by scope)
60
+ userFunctions: [],
61
+
62
+ // Event handling
63
+ eventMethods: [
64
+ 'addEventListener',
65
+ 'removeEventListener',
66
+ 'onclick',
67
+ 'onload',
68
+ 'onchange',
69
+ 'onsubmit'
70
+ ],
71
+
72
+ // Async patterns
73
+ asyncMethods: [
74
+ 'fetch',
75
+ 'setTimeout',
76
+ 'setInterval',
77
+ 'requestAnimationFrame'
78
+ ],
79
+
80
+ // DOM methods (optional)
81
+ domMethods: [
82
+ 'querySelector',
83
+ 'getElementById',
84
+ 'createElement',
85
+ 'appendChild',
86
+ 'removeChild'
87
+ ]
88
+ };
89
+ }
90
+
91
+ init(options = {}) {
92
+ this.recordingId = options.recordingId || 'default';
93
+ this.onDataReady = options.onDataReady;
94
+ this.piiRedactor = options.piiRedactor;
95
+ this.performanceMonitor = options.performanceMonitor;
96
+
97
+ // Override settings
98
+ if (options.maxCallDepth) this.maxCallDepth = options.maxCallDepth;
99
+ if (options.trackReturnValues !== undefined) this.trackReturnValues = options.trackReturnValues;
100
+ if (options.trackExecutionTime !== undefined) this.trackExecutionTime = options.trackExecutionTime;
101
+ if (options.bufferFlushDelay) this.bufferFlushDelay = options.bufferFlushDelay;
102
+
103
+ // Update target categories
104
+ if (options.targetCategories) {
105
+ this.targetCategories = { ...this.targetCategories, ...options.targetCategories };
106
+ }
107
+
108
+ // console.log('[FunctionTracker] Initialized with options:', {
109
+ // recordingId: this.recordingId,
110
+ // maxCallDepth: this.maxCallDepth,
111
+ // trackReturnValues: this.trackReturnValues,
112
+ // targetCategories: this.targetCategories
113
+ // });
114
+ }
115
+
116
+ startTracking() {
117
+ if (this.isTracking) {
118
+ console.warn('[FunctionTracker] Already tracking function calls');
119
+ return;
120
+ }
121
+
122
+ try {
123
+ // Wrap different categories of functions
124
+ if (this.targetCategories.eventHandlers) {
125
+ this.wrapEventHandlers();
126
+ }
127
+
128
+ if (this.targetCategories.asyncFunctions) {
129
+ this.wrapAsyncFunctions();
130
+ }
131
+
132
+ if (this.targetCategories.domMethods) {
133
+ this.wrapDOMMethods();
134
+ }
135
+
136
+ if (this.targetCategories.userDefined) {
137
+ this.discoverAndWrapUserFunctions();
138
+ }
139
+
140
+ this.isTracking = true;
141
+ this.startBufferFlusher();
142
+
143
+ // Set up periodic flushing
144
+ this.flushInterval = setInterval(() => {
145
+ if (this.executionBuffer.length > 0) {
146
+ this.flush();
147
+ }
148
+ }, FLUSH_INTERVAL_MS);
149
+
150
+ // Set up pagehide listener for final flush
151
+ window.addEventListener('pagehide', this.boundFlush);
152
+
153
+ // console.log('[FunctionTracker] Started function execution tracking');
154
+
155
+ // Record performance event
156
+ if (this.performanceMonitor) {
157
+ this.performanceMonitor.recordEvent('function_tracking_started', {
158
+ timestamp: Date.now(),
159
+ categories: this.targetCategories
160
+ });
161
+ }
162
+
163
+ } catch (error) {
164
+ console.error('[FunctionTracker] Failed to start tracking:', error);
165
+ }
166
+ }
167
+
168
+ stopTracking() {
169
+ if (!this.isTracking) return;
170
+
171
+ this.restoreAllFunctions();
172
+ this.stopBufferFlusher();
173
+
174
+ // Clear periodic flush interval
175
+ if (this.flushInterval) {
176
+ clearInterval(this.flushInterval);
177
+ this.flushInterval = null;
178
+ }
179
+
180
+ // Remove pagehide listener
181
+ window.removeEventListener('pagehide', this.boundFlush);
182
+
183
+ this.flushBuffer(); // Flush any remaining data
184
+
185
+ this.isTracking = false;
186
+ // console.log('[FunctionTracker] Stopped function execution tracking');
187
+
188
+ // Record performance event
189
+ if (this.performanceMonitor) {
190
+ this.performanceMonitor.recordEvent('function_tracking_stopped', {
191
+ timestamp: Date.now(),
192
+ totalCalls: this.executionBuffer.length
193
+ });
194
+ }
195
+ }
196
+
197
+ wrapEventHandlers() {
198
+ // Wrap addEventListener to track event handlers
199
+ if (window.EventTarget && EventTarget.prototype.addEventListener) {
200
+ const originalAddEventListener = EventTarget.prototype.addEventListener;
201
+ const self = this; // Capture FunctionTracker instance
202
+
203
+ EventTarget.prototype.addEventListener = function(type, listener, options) {
204
+ if (typeof listener === 'function') {
205
+ const wrappedListener = self.wrapFunction(
206
+ listener,
207
+ `EventHandler:${type}`,
208
+ { isEventHandler: true, eventType: type }
209
+ );
210
+
211
+ // Store reference for cleanup
212
+ if (!listener._chromePilotWrapped) {
213
+ listener._chromePilotWrapped = wrappedListener;
214
+ }
215
+
216
+ return originalAddEventListener.call(this, type, wrappedListener, options);
217
+ }
218
+
219
+ return originalAddEventListener.call(this, type, listener, options);
220
+ };
221
+
222
+ this.trackedFunctions.set('addEventListener', originalAddEventListener);
223
+ }
224
+ }
225
+
226
+ wrapAsyncFunctions() {
227
+ // Wrap fetch API
228
+ if (window.fetch) {
229
+ const originalFetch = window.fetch;
230
+
231
+ window.fetch = async (...args) => {
232
+ const callId = this.generateCallId();
233
+ const startTime = Date.now();
234
+
235
+ this.recordFunctionCall({
236
+ callId,
237
+ functionName: 'fetch',
238
+ arguments: this.serializeArguments(args),
239
+ startTime,
240
+ category: 'async'
241
+ });
242
+
243
+ try {
244
+ const result = await originalFetch.apply(window, args);
245
+
246
+ this.recordFunctionReturn({
247
+ callId,
248
+ returnValue: this.serializeReturnValue({
249
+ status: result.status,
250
+ url: result.url,
251
+ headers: Object.fromEntries(result.headers.entries())
252
+ }),
253
+ executionTime: Date.now() - startTime,
254
+ success: true
255
+ });
256
+
257
+ return result;
258
+ } catch (error) {
259
+ this.recordFunctionReturn({
260
+ callId,
261
+ error: error.message,
262
+ executionTime: Date.now() - startTime,
263
+ success: false
264
+ });
265
+ throw error;
266
+ }
267
+ };
268
+
269
+ this.trackedFunctions.set('fetch', originalFetch);
270
+ }
271
+
272
+ // Wrap setTimeout
273
+ if (window.setTimeout) {
274
+ const originalSetTimeout = window.setTimeout;
275
+
276
+ window.setTimeout = (callback, delay, ...args) => {
277
+ if (typeof callback === 'function') {
278
+ const wrappedCallback = this.wrapFunction(
279
+ callback,
280
+ 'setTimeout_callback',
281
+ { isAsync: true, delay }
282
+ );
283
+ return originalSetTimeout(wrappedCallback, delay, ...args);
284
+ }
285
+ return originalSetTimeout(callback, delay, ...args);
286
+ };
287
+
288
+ this.trackedFunctions.set('setTimeout', originalSetTimeout);
289
+ }
290
+
291
+ // Wrap setInterval
292
+ if (window.setInterval) {
293
+ const originalSetInterval = window.setInterval;
294
+
295
+ window.setInterval = (callback, delay, ...args) => {
296
+ if (typeof callback === 'function') {
297
+ const wrappedCallback = this.wrapFunction(
298
+ callback,
299
+ 'setInterval_callback',
300
+ { isAsync: true, delay, repeating: true }
301
+ );
302
+ return originalSetInterval(wrappedCallback, delay, ...args);
303
+ }
304
+ return originalSetInterval(callback, delay, ...args);
305
+ };
306
+
307
+ this.trackedFunctions.set('setInterval', originalSetInterval);
308
+ }
309
+ }
310
+
311
+ wrapDOMMethods() {
312
+ // Wrap commonly used DOM methods
313
+ const domMethods = ['querySelector', 'getElementById', 'createElement'];
314
+
315
+ domMethods.forEach(methodName => {
316
+ if (document[methodName]) {
317
+ const original = document[methodName];
318
+
319
+ document[methodName] = (...args) => {
320
+ const callId = this.generateCallId();
321
+ const startTime = Date.now();
322
+
323
+ this.recordFunctionCall({
324
+ callId,
325
+ functionName: `document.${methodName}`,
326
+ arguments: this.serializeArguments(args),
327
+ startTime,
328
+ category: 'dom'
329
+ });
330
+
331
+ try {
332
+ const result = original.apply(document, args);
333
+
334
+ this.recordFunctionReturn({
335
+ callId,
336
+ returnValue: this.serializeDOMElement(result),
337
+ executionTime: Date.now() - startTime,
338
+ success: true
339
+ });
340
+
341
+ return result;
342
+ } catch (error) {
343
+ this.recordFunctionReturn({
344
+ callId,
345
+ error: error.message,
346
+ executionTime: Date.now() - startTime,
347
+ success: false
348
+ });
349
+ throw error;
350
+ }
351
+ };
352
+
353
+ this.trackedFunctions.set(methodName, original);
354
+ }
355
+ });
356
+ }
357
+
358
+ discoverAndWrapUserFunctions() {
359
+ // Discover user-defined functions in global scope
360
+ const globalObj = window;
361
+
362
+ for (const prop in globalObj) {
363
+ try {
364
+ if (typeof globalObj[prop] === 'function' &&
365
+ !prop.startsWith('_') &&
366
+ !this.isNativeFunction(globalObj[prop]) &&
367
+ !this.trackedFunctions.has(prop)) {
368
+
369
+ const original = globalObj[prop];
370
+ const wrapped = this.wrapFunction(original, prop, { isUserDefined: true });
371
+
372
+ globalObj[prop] = wrapped;
373
+ this.trackedFunctions.set(prop, original);
374
+ }
375
+ } catch (error) {
376
+ // Skip properties that can't be accessed
377
+ continue;
378
+ }
379
+ }
380
+ }
381
+
382
+ wrapFunction(originalFunction, functionName, metadata = {}) {
383
+ const tracker = this;
384
+
385
+ return function wrappedFunction(...args) {
386
+ // Prevent deep recursion tracking
387
+ if (tracker.callStack.length >= tracker.maxCallDepth) {
388
+ return originalFunction.apply(this, args);
389
+ }
390
+
391
+ const callId = tracker.generateCallId();
392
+ const startTime = Date.now();
393
+
394
+ // Add to call stack
395
+ tracker.callStack.push({
396
+ callId,
397
+ functionName,
398
+ startTime
399
+ });
400
+
401
+ // Record function call
402
+ tracker.recordFunctionCall({
403
+ callId,
404
+ functionName,
405
+ arguments: tracker.serializeArguments(args),
406
+ startTime,
407
+ category: metadata.category || 'user',
408
+ metadata,
409
+ callDepth: tracker.callStack.length,
410
+ parentCallId: tracker.callStack.length > 1 ?
411
+ tracker.callStack[tracker.callStack.length - 2].callId : null
412
+ });
413
+
414
+ try {
415
+ const result = originalFunction.apply(this, args);
416
+
417
+ // Handle promises
418
+ if (result && typeof result.then === 'function') {
419
+ return result
420
+ .then(value => {
421
+ tracker.recordFunctionReturn({
422
+ callId,
423
+ returnValue: tracker.serializeReturnValue(value),
424
+ executionTime: Date.now() - startTime,
425
+ success: true,
426
+ isAsync: true
427
+ });
428
+
429
+ tracker.callStack.pop();
430
+ return value;
431
+ })
432
+ .catch(error => {
433
+ tracker.recordFunctionReturn({
434
+ callId,
435
+ error: error.message,
436
+ executionTime: Date.now() - startTime,
437
+ success: false,
438
+ isAsync: true
439
+ });
440
+
441
+ tracker.callStack.pop();
442
+ throw error;
443
+ });
444
+ }
445
+
446
+ // Handle synchronous return
447
+ tracker.recordFunctionReturn({
448
+ callId,
449
+ returnValue: tracker.serializeReturnValue(result),
450
+ executionTime: Date.now() - startTime,
451
+ success: true
452
+ });
453
+
454
+ tracker.callStack.pop();
455
+ return result;
456
+
457
+ } catch (error) {
458
+ tracker.recordFunctionReturn({
459
+ callId,
460
+ error: error.message,
461
+ executionTime: Date.now() - startTime,
462
+ success: false
463
+ });
464
+
465
+ tracker.callStack.pop();
466
+ throw error;
467
+ }
468
+ };
469
+ }
470
+
471
+ recordFunctionCall(callData) {
472
+ // LIGHTWEIGHT MODE: Only essential info
473
+ let executionEvent;
474
+
475
+ if (this.lightweightMode) {
476
+ executionEvent = {
477
+ type: 'execution_trace',
478
+ timestamp: callData.startTime,
479
+ recording_id: this.recordingId,
480
+ function_name: callData.functionName,
481
+ // Just argument summary, not full values
482
+ arg_summary: callData.arguments ? `${callData.arguments.length} args` : '0 args'
483
+ };
484
+ } else {
485
+ // Original full tracking
486
+ executionEvent = {
487
+ type: 'execution_trace',
488
+ timestamp: callData.startTime,
489
+ recording_id: this.recordingId,
490
+ call_id: callData.callId,
491
+ function_name: callData.functionName,
492
+ arguments: callData.arguments,
493
+ call_depth: callData.callDepth || 0,
494
+ parent_call_id: callData.parentCallId,
495
+ category: callData.category,
496
+ metadata: callData.metadata
497
+ };
498
+ }
499
+
500
+ // Apply PII redaction (only if not in lightweight mode)
501
+ if (this.piiRedactor && !this.lightweightMode) {
502
+ executionEvent = this.piiRedactor.redactData(executionEvent, 'execution_trace');
503
+ }
504
+
505
+ this.executionBuffer.push(executionEvent);
506
+
507
+ // Check if we should flush based on batch size
508
+ if (this.executionBuffer.length >= this.batchSize) {
509
+ this.flush();
510
+ }
511
+ }
512
+
513
+ recordFunctionReturn(returnData) {
514
+ // In lightweight mode, don't track returns at all unless there's an error
515
+ if (this.lightweightMode && !returnData.error) {
516
+ return;
517
+ }
518
+
519
+ if (!this.trackReturnValues && !returnData.error) {
520
+ return; // Skip return value tracking if disabled
521
+ }
522
+
523
+ let returnEvent = {
524
+ type: 'execution_trace',
525
+ timestamp: Date.now(),
526
+ recording_id: this.recordingId,
527
+ call_id: returnData.callId,
528
+ return_value: returnData.returnValue,
529
+ execution_time_ms: returnData.executionTime,
530
+ success: returnData.success,
531
+ error: returnData.error,
532
+ is_async: returnData.isAsync || false
533
+ };
534
+
535
+ // Apply PII redaction
536
+ if (this.piiRedactor) {
537
+ returnEvent = this.piiRedactor.redactData(returnEvent, 'execution_trace');
538
+ }
539
+
540
+ this.executionBuffer.push(returnEvent);
541
+
542
+ // Check if we should flush based on batch size
543
+ if (this.executionBuffer.length >= this.batchSize) {
544
+ this.flush();
545
+ }
546
+ }
547
+
548
+ serializeArguments(args) {
549
+ if (!args || args.length === 0) return [];
550
+
551
+ // LIGHTWEIGHT MODE: Just capture argument count and types
552
+ if (this.lightweightMode) {
553
+ try {
554
+ return Array.from(args).map(arg => {
555
+ if (arg === null) return 'null';
556
+ if (arg === undefined) return 'undefined';
557
+ if (typeof arg === 'function') return '[Function]';
558
+ if (arg instanceof Element) return `[DOM:${arg.tagName}]`;
559
+ if (typeof arg === 'object') return `[Object:${arg.constructor?.name || 'Object'}]`;
560
+ if (typeof arg === 'string') return arg.length > 20 ? `"${arg.substring(0, 20)}..."` : `"${arg}"`;
561
+ return String(arg).substring(0, 50);
562
+ });
563
+ } catch (error) {
564
+ return ['[Error]'];
565
+ }
566
+ }
567
+
568
+ // Original deep serialization (only if not in lightweight mode)
569
+ try {
570
+ return Array.from(args).map(arg => {
571
+ const serialized = this.serializeValue(arg);
572
+ return this.truncateValue(serialized, this.maxArgumentSize);
573
+ });
574
+ } catch (error) {
575
+ return ['[Serialization Error]'];
576
+ }
577
+ }
578
+
579
+ serializeReturnValue(value) {
580
+ if (!this.trackReturnValues) return null;
581
+
582
+ try {
583
+ const serialized = this.serializeValue(value);
584
+ return this.truncateValue(serialized, this.maxArgumentSize);
585
+ } catch (error) {
586
+ return '[Serialization Error]';
587
+ }
588
+ }
589
+
590
+ serializeValue(value) {
591
+ if (value === null || value === undefined) {
592
+ return value;
593
+ }
594
+
595
+ if (typeof value === 'function') {
596
+ return '[Function]';
597
+ }
598
+
599
+ if (value instanceof Error) {
600
+ return {
601
+ name: value.name,
602
+ message: value.message,
603
+ stack: value.stack?.substring(0, 500)
604
+ };
605
+ }
606
+
607
+ if (value instanceof Element) {
608
+ return this.serializeDOMElement(value);
609
+ }
610
+
611
+ if (value && typeof value === 'object') {
612
+ // Handle circular references and sanitize
613
+ try {
614
+ const sanitized = this.sanitizeObject(value);
615
+ return JSON.parse(JSON.stringify(sanitized));
616
+ } catch (error) {
617
+ return '[Circular Reference]';
618
+ }
619
+ }
620
+
621
+ // Sanitize string values
622
+ if (typeof value === 'string') {
623
+ return this.sanitizeString(value);
624
+ }
625
+
626
+ return value;
627
+ }
628
+
629
+ sanitizeObject(obj, depth = 0) {
630
+ if (depth > 5) {
631
+ return '[Max Depth Exceeded]';
632
+ }
633
+
634
+ if (Array.isArray(obj)) {
635
+ return obj.map(item => this.sanitizeValue(item, depth + 1));
636
+ }
637
+
638
+ const sanitized = {};
639
+ for (const [key, value] of Object.entries(obj)) {
640
+ // Check if key contains sensitive pattern
641
+ const lowerKey = key.toLowerCase();
642
+ const isSensitive = this.sensitivePatterns.some(pattern =>
643
+ lowerKey.includes(pattern.toLowerCase())
644
+ );
645
+
646
+ if (isSensitive) {
647
+ sanitized[key] = '[REDACTED]';
648
+ } else {
649
+ sanitized[key] = this.sanitizeValue(value, depth + 1);
650
+ }
651
+ }
652
+
653
+ return sanitized;
654
+ }
655
+
656
+ sanitizeValue(value, depth = 0) {
657
+ if (value === null || value === undefined) {
658
+ return value;
659
+ }
660
+
661
+ if (typeof value === 'string') {
662
+ return this.sanitizeString(value);
663
+ }
664
+
665
+ if (typeof value === 'object') {
666
+ return this.sanitizeObject(value, depth);
667
+ }
668
+
669
+ return value;
670
+ }
671
+
672
+ sanitizeString(str) {
673
+ // Check for common sensitive patterns in strings
674
+ if (str.length > 100) {
675
+ // Check if it looks like a token or key
676
+ if (/^[A-Za-z0-9+/=_-]{40,}$/.test(str)) {
677
+ return '[POSSIBLE TOKEN REDACTED]';
678
+ }
679
+
680
+ // Check for JWT pattern
681
+ if (/^eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(str)) {
682
+ return '[JWT TOKEN REDACTED]';
683
+ }
684
+ }
685
+
686
+ // Check for credit card pattern
687
+ if (/\b(?:\d{4}[-\s]?){3}\d{4}\b/.test(str)) {
688
+ return str.replace(/\b(?:\d{4}[-\s]?){3}\d{4}\b/g, '[CREDIT CARD REDACTED]');
689
+ }
690
+
691
+ // Check for SSN pattern
692
+ if (/\b\d{3}-\d{2}-\d{4}\b/.test(str)) {
693
+ return str.replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[SSN REDACTED]');
694
+ }
695
+
696
+ return str;
697
+ }
698
+
699
+ serializeDOMElement(element) {
700
+ if (!element || !element.tagName) return element;
701
+
702
+ return {
703
+ tagName: element.tagName.toLowerCase(),
704
+ id: element.id,
705
+ className: element.className,
706
+ textContent: element.textContent?.substring(0, 100)
707
+ };
708
+ }
709
+
710
+ truncateValue(value, maxSize) {
711
+ if (typeof value === 'string' && value.length > maxSize) {
712
+ return value.substring(0, maxSize) + '... [truncated]';
713
+ }
714
+
715
+ if (typeof value === 'object' && value !== null) {
716
+ const stringified = JSON.stringify(value);
717
+ if (stringified.length > maxSize) {
718
+ return JSON.stringify(value, null, 0).substring(0, maxSize) + '... [truncated]';
719
+ }
720
+ }
721
+
722
+ return value;
723
+ }
724
+
725
+ isNativeFunction(func) {
726
+ const funcStr = func.toString();
727
+ return funcStr.includes('[native code]') ||
728
+ funcStr.includes('[object ') ||
729
+ func.name.startsWith('bound ');
730
+ }
731
+
732
+ generateCallId() {
733
+ return `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
734
+ }
735
+
736
+ restoreAllFunctions() {
737
+ // Restore all wrapped functions
738
+ for (const [name, original] of this.trackedFunctions) {
739
+ try {
740
+ if (name === 'addEventListener' && EventTarget.prototype.addEventListener) {
741
+ EventTarget.prototype.addEventListener = original;
742
+ } else if (name === 'fetch' && window.fetch) {
743
+ window.fetch = original;
744
+ } else if (name === 'setTimeout' && window.setTimeout) {
745
+ window.setTimeout = original;
746
+ } else if (name === 'setInterval' && window.setInterval) {
747
+ window.setInterval = original;
748
+ } else if (document[name]) {
749
+ document[name] = original;
750
+ } else if (window[name]) {
751
+ window[name] = original;
752
+ }
753
+ } catch (error) {
754
+ console.error(`[FunctionTracker] Failed to restore ${name}:`, error);
755
+ }
756
+ }
757
+
758
+ this.trackedFunctions.clear();
759
+ }
760
+
761
+ startBufferFlusher() {
762
+ if (this.flushTimer) return;
763
+
764
+ this.flushTimer = setInterval(() => {
765
+ if (this.executionBuffer.length > 0) {
766
+ this.flushBuffer();
767
+ }
768
+ }, this.bufferFlushDelay);
769
+ }
770
+
771
+ stopBufferFlusher() {
772
+ if (this.flushTimer) {
773
+ clearInterval(this.flushTimer);
774
+ this.flushTimer = null;
775
+ }
776
+ }
777
+
778
+ flushBuffer() {
779
+ if (this.executionBuffer.length === 0) return;
780
+
781
+ const events = [...this.executionBuffer];
782
+ this.executionBuffer = [];
783
+
784
+ // console.log(`[FunctionTracker] Flushing ${events.length} execution traces`);
785
+
786
+ // Send data via callback
787
+ if (this.onDataReady) {
788
+ this.onDataReady(events);
789
+ }
790
+
791
+ // Record performance metrics
792
+ if (this.performanceMonitor) {
793
+ this.performanceMonitor.recordEvent('function_calls_flushed', {
794
+ count: events.length,
795
+ timestamp: Date.now()
796
+ });
797
+ }
798
+ }
799
+
800
+ getStats() {
801
+ return {
802
+ isTracking: this.isTracking,
803
+ bufferSize: this.executionBuffer.length,
804
+ trackedFunctionCount: this.trackedFunctions.size,
805
+ currentCallDepth: this.callStack.length,
806
+ recordingId: this.recordingId,
807
+ targetCategories: this.targetCategories
808
+ };
809
+ }
810
+
811
+ flush() {
812
+ if (this.executionBuffer.length === 0 || !this.onDataReady) {
813
+ return;
814
+ }
815
+
816
+ // Pass a copy of the buffer to the callback and reset the original
817
+ const eventsToFlush = [...this.executionBuffer];
818
+ this.executionBuffer = [];
819
+
820
+ try {
821
+ this.onDataReady(eventsToFlush);
822
+ // console.log(`[FunctionTracker] Flushed ${eventsToFlush.length} execution traces`);
823
+ } catch (error) {
824
+ console.error('[FunctionTracker] Error in onDataReady callback for function traces:', error);
825
+ // Optionally add events back for retry, but be careful of infinite loops
826
+ // this.executionBuffer.unshift(...eventsToFlush);
827
+ }
828
+ }
829
+
830
+ destroy() {
831
+ this.stopTracking();
832
+ this.executionBuffer = [];
833
+ this.callStack = [];
834
+ this.onDataReady = null;
835
+ this.piiRedactor = null;
836
+ this.performanceMonitor = null;
837
+ }
838
+ }
839
+
840
+ // Export for use in content script
841
+ if (typeof module !== 'undefined' && module.exports) {
842
+ module.exports = FunctionTracker;
843
+ }