@dynamicu/chromedebug-mcp 2.2.0

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 (95) hide show
  1. package/CLAUDE.md +344 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/chrome-extension/README.md +41 -0
  5. package/chrome-extension/background.js +3917 -0
  6. package/chrome-extension/chrome-session-manager.js +706 -0
  7. package/chrome-extension/content.css +181 -0
  8. package/chrome-extension/content.js +3022 -0
  9. package/chrome-extension/data-buffer.js +435 -0
  10. package/chrome-extension/dom-tracker.js +411 -0
  11. package/chrome-extension/extension-config.js +78 -0
  12. package/chrome-extension/firebase-client.js +278 -0
  13. package/chrome-extension/firebase-config.js +32 -0
  14. package/chrome-extension/firebase-config.module.js +22 -0
  15. package/chrome-extension/firebase-config.module.template.js +27 -0
  16. package/chrome-extension/firebase-config.template.js +36 -0
  17. package/chrome-extension/frame-capture.js +407 -0
  18. package/chrome-extension/icon128.png +1 -0
  19. package/chrome-extension/icon16.png +1 -0
  20. package/chrome-extension/icon48.png +1 -0
  21. package/chrome-extension/license-helper.js +181 -0
  22. package/chrome-extension/logger.js +23 -0
  23. package/chrome-extension/manifest.json +73 -0
  24. package/chrome-extension/network-tracker.js +510 -0
  25. package/chrome-extension/offscreen.html +10 -0
  26. package/chrome-extension/options.html +203 -0
  27. package/chrome-extension/options.js +282 -0
  28. package/chrome-extension/pako.min.js +2 -0
  29. package/chrome-extension/performance-monitor.js +533 -0
  30. package/chrome-extension/pii-redactor.js +405 -0
  31. package/chrome-extension/popup.html +532 -0
  32. package/chrome-extension/popup.js +2446 -0
  33. package/chrome-extension/upload-manager.js +323 -0
  34. package/chrome-extension/web-vitals.iife.js +1 -0
  35. package/config/api-keys.json +11 -0
  36. package/config/chrome-pilot-config.json +45 -0
  37. package/package.json +126 -0
  38. package/scripts/cleanup-processes.js +109 -0
  39. package/scripts/config-manager.js +280 -0
  40. package/scripts/generate-extension-config.js +53 -0
  41. package/scripts/setup-security.js +64 -0
  42. package/src/capture/architecture.js +426 -0
  43. package/src/capture/error-handling-tests.md +38 -0
  44. package/src/capture/error-handling-types.ts +360 -0
  45. package/src/capture/index.js +508 -0
  46. package/src/capture/interfaces.js +625 -0
  47. package/src/capture/memory-manager.js +713 -0
  48. package/src/capture/types.js +342 -0
  49. package/src/chrome-controller.js +2658 -0
  50. package/src/cli.js +19 -0
  51. package/src/config-loader.js +303 -0
  52. package/src/database.js +2178 -0
  53. package/src/firebase-license-manager.js +462 -0
  54. package/src/firebase-privacy-guard.js +397 -0
  55. package/src/http-server.js +1516 -0
  56. package/src/index-direct.js +157 -0
  57. package/src/index-modular.js +219 -0
  58. package/src/index-monolithic-backup.js +2230 -0
  59. package/src/index.js +305 -0
  60. package/src/legacy/chrome-controller-old.js +1406 -0
  61. package/src/legacy/index-express.js +625 -0
  62. package/src/legacy/index-old.js +977 -0
  63. package/src/legacy/routes.js +260 -0
  64. package/src/legacy/shared-storage.js +101 -0
  65. package/src/logger.js +10 -0
  66. package/src/mcp/handlers/chrome-tool-handler.js +306 -0
  67. package/src/mcp/handlers/element-tool-handler.js +51 -0
  68. package/src/mcp/handlers/frame-tool-handler.js +957 -0
  69. package/src/mcp/handlers/request-handler.js +104 -0
  70. package/src/mcp/handlers/workflow-tool-handler.js +636 -0
  71. package/src/mcp/server.js +68 -0
  72. package/src/mcp/tools/index.js +701 -0
  73. package/src/middleware/auth.js +371 -0
  74. package/src/middleware/security.js +267 -0
  75. package/src/port-discovery.js +258 -0
  76. package/src/routes/admin.js +182 -0
  77. package/src/services/browser-daemon.js +494 -0
  78. package/src/services/chrome-service.js +375 -0
  79. package/src/services/failover-manager.js +412 -0
  80. package/src/services/git-safety-service.js +675 -0
  81. package/src/services/heartbeat-manager.js +200 -0
  82. package/src/services/http-client.js +195 -0
  83. package/src/services/process-manager.js +318 -0
  84. package/src/services/process-tracker.js +574 -0
  85. package/src/services/profile-manager.js +449 -0
  86. package/src/services/project-manager.js +415 -0
  87. package/src/services/session-manager.js +497 -0
  88. package/src/services/session-registry.js +491 -0
  89. package/src/services/unified-session-manager.js +678 -0
  90. package/src/shared-storage-old.js +267 -0
  91. package/src/standalone-server.js +53 -0
  92. package/src/utils/extension-path.js +145 -0
  93. package/src/utils.js +187 -0
  94. package/src/validation/log-transformer.js +125 -0
  95. package/src/validation/schemas.js +391 -0
@@ -0,0 +1,3917 @@
1
+ // Background script to handle server communication and recording
2
+ const EXTENSION_VERSION = '2.0.4-BUILD-20250119';
3
+ console.log(`[background.js] Loaded version: ${EXTENSION_VERSION}`);
4
+
5
+ // Import configuration and libraries
6
+ importScripts('extension-config.js');
7
+ importScripts('pako.min.js');
8
+ importScripts('data-buffer.js');
9
+ importScripts('upload-manager.js');
10
+ importScripts('chrome-session-manager.js');
11
+ importScripts('firebase-config.js');
12
+ importScripts('license-helper.js');
13
+
14
+ const CONFIG_PORTS = CHROMEDEBUG_CONFIG.ports;
15
+ const DISCOVERY_TIMEOUT = CHROMEDEBUG_CONFIG.discoveryTimeout;
16
+
17
+ // Frame queue for handling validation race conditions
18
+ // v2.1.2: Prevents frame loss when lease renewal hasn't propagated yet
19
+ const pendingFrameQueue = new Map(); // sessionId -> array of frame batches
20
+
21
+ // LogStreamer class for batched real-time log streaming
22
+ class LogStreamer {
23
+ constructor() {
24
+ this.buffer = [];
25
+ this.sequenceNumber = 0;
26
+ this.serverAvailable = false;
27
+ this.streamInterval = null;
28
+ this.retryQueue = [];
29
+ this.maxBufferSize = 50; // Much smaller than session storage
30
+ this.streamIntervalMs = 500; // Stream every 500ms (slower to align with frame generation)
31
+ this.currentSessionId = null;
32
+ this.serverPorts = CONFIG_PORTS;
33
+ this.lastFrameCapture = 0; // Track when last frame was captured
34
+ this.frameCheckInterval = null;
35
+ }
36
+
37
+ async init() {
38
+ // Test server availability
39
+ await this.checkServerAvailability();
40
+
41
+ // Start periodic streaming
42
+ this.startPeriodicStreaming();
43
+
44
+ console.log('[LogStreamer] Initialized with server availability:', this.serverAvailable);
45
+ }
46
+
47
+ async checkServerAvailability() {
48
+ for (const port of this.serverPorts) {
49
+ try {
50
+ const response = await fetch(`http://localhost:${port}/health`, {
51
+ method: 'GET',
52
+ timeout: 1000
53
+ });
54
+ if (response.ok) {
55
+ this.serverAvailable = true;
56
+ console.log(`[LogStreamer] Server available on port ${port}`);
57
+ return;
58
+ }
59
+ } catch (error) {
60
+ // Continue checking other ports
61
+ }
62
+ }
63
+ this.serverAvailable = false;
64
+ console.log('[LogStreamer] No server available');
65
+ }
66
+
67
+ startPeriodicStreaming() {
68
+ if (this.streamInterval) {
69
+ clearInterval(this.streamInterval);
70
+ }
71
+
72
+ this.streamInterval = setInterval(async () => {
73
+ await this.streamLogs();
74
+ await this.processRetryQueue();
75
+ }, this.streamIntervalMs);
76
+ }
77
+
78
+ async addLog(log, sessionId) {
79
+ log.sequence = ++this.sequenceNumber;
80
+ log.timestamp = Date.now();
81
+
82
+ this.currentSessionId = sessionId;
83
+ this.buffer.push(log);
84
+
85
+ // If buffer is getting full, force immediate stream
86
+ if (this.buffer.length >= this.maxBufferSize) {
87
+ await this.streamLogs();
88
+ }
89
+ }
90
+
91
+ async streamLogs() {
92
+ if (this.buffer.length === 0 || !this.currentSessionId) return;
93
+
94
+ // Check if we have waited at least 100ms since last frame capture
95
+ // This gives frames time to be processed before logs are associated
96
+ const timeSinceLastFrame = Date.now() - this.lastFrameCapture;
97
+ if (this.lastFrameCapture > 0 && timeSinceLastFrame < 100) {
98
+ console.log('[LogStreamer] Waiting for frame processing before streaming logs');
99
+ return; // Wait for next cycle
100
+ }
101
+
102
+ const batch = this.buffer.splice(0, this.maxBufferSize);
103
+
104
+ try {
105
+ await this.sendToServer(batch);
106
+ this.serverAvailable = true;
107
+ } catch (error) {
108
+ console.error('[LogStreamer] Failed to stream logs:', error);
109
+ this.serverAvailable = false;
110
+
111
+ // Add to retry queue with exponential backoff
112
+ this.retryQueue.push({
113
+ batch,
114
+ attempts: 0,
115
+ nextRetry: Date.now() + 1000,
116
+ sessionId: this.currentSessionId
117
+ });
118
+
119
+ // Emergency fallback to session storage (limited)
120
+ this.fallbackToSessionStorage(batch);
121
+ }
122
+ }
123
+
124
+ // Method to notify LogStreamer when frame is captured
125
+ notifyFrameCapture() {
126
+ this.lastFrameCapture = Date.now();
127
+ console.log('[LogStreamer] Frame capture notification received');
128
+ }
129
+
130
+ async sendToServer(batch) {
131
+ const payload = {
132
+ sessionId: this.currentSessionId,
133
+ logs: batch
134
+ };
135
+
136
+ let lastError = null;
137
+ for (const port of this.serverPorts) {
138
+ try {
139
+ const response = await fetch(`http://localhost:${port}/chromedebug/logs/stream`, {
140
+ method: 'POST',
141
+ headers: {
142
+ 'Content-Type': 'application/json'
143
+ },
144
+ body: JSON.stringify(payload)
145
+ });
146
+
147
+ if (response.ok) {
148
+ const result = await response.json();
149
+ // Only log if we had to try multiple ports (first port failed)
150
+ if (port !== this.serverPorts[0]) {
151
+ console.log(`[LogStreamer] Connected to server on port ${port}`);
152
+ }
153
+ return result;
154
+ }
155
+ } catch (error) {
156
+ lastError = error;
157
+ // Silent during discovery - port attempts are expected to fail until we find the right one
158
+ }
159
+ }
160
+
161
+ // Only log error when ALL ports fail
162
+ console.error('[LogStreamer] No ChromeDebug server available on any configured port:', lastError);
163
+ throw new Error('All server ports failed');
164
+ }
165
+
166
+ fallbackToSessionStorage(batch) {
167
+ // Only use session storage as emergency backup with aggressive cleanup
168
+ const key = `emergency_logs_${Date.now()}`;
169
+ const limitedBatch = batch.slice(-10); // Only keep last 10 logs
170
+
171
+ chrome.storage.session.set({ [key]: limitedBatch }).catch(error => {
172
+ console.error('[LogStreamer] Emergency fallback failed:', error);
173
+ });
174
+
175
+ // Clean up old emergency logs
176
+ this.cleanupEmergencyLogs();
177
+ }
178
+
179
+ async cleanupEmergencyLogs() {
180
+ try {
181
+ const allData = await chrome.storage.session.get();
182
+ const emergencyKeys = Object.keys(allData).filter(key => key.startsWith('emergency_logs_'));
183
+
184
+ // Keep only the most recent 3 emergency batches
185
+ if (emergencyKeys.length > 3) {
186
+ const oldKeys = emergencyKeys.sort().slice(0, -3);
187
+ await chrome.storage.session.remove(oldKeys);
188
+ }
189
+ } catch (error) {
190
+ console.error('[LogStreamer] Failed to cleanup emergency logs:', error);
191
+ }
192
+ }
193
+
194
+ async processRetryQueue() {
195
+ const now = Date.now();
196
+ const retryableItems = [];
197
+
198
+ for (const item of this.retryQueue) {
199
+ if (now >= item.nextRetry && item.attempts < 3) {
200
+ try {
201
+ await this.sendToServer(item.batch);
202
+ console.log(`[LogStreamer] Retry successful for batch with ${item.batch.length} logs`);
203
+ } catch (error) {
204
+ item.attempts++;
205
+ item.nextRetry = now + (1000 * Math.pow(2, item.attempts)); // Exponential backoff
206
+ retryableItems.push(item);
207
+ }
208
+ } else if (item.attempts < 3) {
209
+ retryableItems.push(item);
210
+ }
211
+ // Drop items that have exceeded max attempts
212
+ }
213
+
214
+ this.retryQueue = retryableItems;
215
+ }
216
+
217
+ setSessionId(sessionId) {
218
+ this.currentSessionId = sessionId;
219
+ }
220
+
221
+ destroy() {
222
+ if (this.streamInterval) {
223
+ clearInterval(this.streamInterval);
224
+ this.streamInterval = null;
225
+ }
226
+
227
+ // Final attempt to stream remaining logs
228
+ if (this.buffer.length > 0) {
229
+ this.streamLogs();
230
+ }
231
+ }
232
+ }
233
+
234
+ // LogBuffer class for sequential log storage to prevent race conditions
235
+ class LogBuffer {
236
+ constructor() {
237
+ this.tabBuffers = new Map(); // Map of tabId to LogTabBuffer
238
+ this.flushInProgress = false;
239
+ this.storageQuotaLimit = 500000; // ~500KB per tab to stay under 1MB chrome.storage.session limit
240
+ this.maxLogsPerTab = 10000; // Maximum logs to prevent memory issues
241
+ }
242
+
243
+ // Add a log entry to the buffer for a specific tab
244
+ async addLog(tabId, logEntry) {
245
+ try {
246
+ if (!this.tabBuffers.has(tabId)) {
247
+ this.tabBuffers.set(tabId, new LogTabBuffer(tabId));
248
+ }
249
+
250
+ const tabBuffer = this.tabBuffers.get(tabId);
251
+ await tabBuffer.addLog(logEntry);
252
+ } catch (error) {
253
+ console.error(`[LogBuffer] Failed to add log for tab ${tabId}:`, error);
254
+ }
255
+ }
256
+
257
+ // Flush all buffers to Chrome storage
258
+ async flushAll() {
259
+ if (this.flushInProgress) {
260
+ console.log('[LogBuffer] Flush already in progress, waiting for completion...');
261
+ // Wait for current flush to complete instead of skipping
262
+ while (this.flushInProgress) {
263
+ await new Promise(resolve => setTimeout(resolve, 10));
264
+ }
265
+ return;
266
+ }
267
+
268
+ this.flushInProgress = true;
269
+ const flushPromises = [];
270
+ const startTime = Date.now();
271
+
272
+ try {
273
+ console.log(`[LogBuffer] Starting flushAll for ${this.tabBuffers.size} tabs`);
274
+
275
+ for (const [tabId, tabBuffer] of this.tabBuffers) {
276
+ flushPromises.push(tabBuffer.flush());
277
+ }
278
+
279
+ await Promise.all(flushPromises);
280
+ const duration = Date.now() - startTime;
281
+ console.log(`[LogBuffer] Successfully flushed all buffers for ${this.tabBuffers.size} tabs in ${duration}ms`);
282
+ } catch (error) {
283
+ console.error('[LogBuffer] Error during flush operation:', error);
284
+ throw error; // Re-throw to ensure proper error handling upstream
285
+ } finally {
286
+ this.flushInProgress = false;
287
+ }
288
+ }
289
+
290
+ // Flush buffer for a specific tab
291
+ async flushTab(tabId) {
292
+ const tabBuffer = this.tabBuffers.get(tabId);
293
+ if (tabBuffer) {
294
+ await tabBuffer.flush();
295
+ }
296
+ }
297
+
298
+ // Clear buffer for a specific tab
299
+ clearTab(tabId) {
300
+ if (this.tabBuffers.has(tabId)) {
301
+ this.tabBuffers.delete(tabId);
302
+ console.log(`[LogBuffer] Cleared buffer for tab ${tabId}`);
303
+ }
304
+ }
305
+
306
+ // Get buffer statistics
307
+ getStats() {
308
+ const stats = {
309
+ totalTabs: this.tabBuffers.size,
310
+ tabStats: {},
311
+ totalLogsAcrossAllTabs: 0,
312
+ totalFlushedAcrossAllTabs: 0
313
+ };
314
+
315
+ for (const [tabId, tabBuffer] of this.tabBuffers) {
316
+ const tabStats = tabBuffer.getStats();
317
+ stats.tabStats[tabId] = tabStats;
318
+ stats.totalLogsAcrossAllTabs += tabStats.totalAdded;
319
+ stats.totalFlushedAcrossAllTabs += tabStats.totalFlushed;
320
+ }
321
+
322
+ return stats;
323
+ }
324
+
325
+ // Debug method to log current buffer state
326
+ logStats() {
327
+ const stats = this.getStats();
328
+ console.log('[LogBuffer] Current statistics:', stats);
329
+ return stats;
330
+ }
331
+ }
332
+
333
+ // Individual tab buffer for sequential processing
334
+ class LogTabBuffer {
335
+ constructor(tabId) {
336
+ this.tabId = tabId;
337
+ this.buffer = [];
338
+ this.processing = false;
339
+ this.processingQueue = [];
340
+ this.totalLogsAdded = 0;
341
+ this.totalLogsFlushed = 0;
342
+ this.lastFlushTime = 0;
343
+ }
344
+
345
+ // Add log to buffer with queuing to prevent race conditions
346
+ async addLog(logEntry) {
347
+ return new Promise((resolve, reject) => {
348
+ this.processingQueue.push({
349
+ type: 'add',
350
+ logEntry,
351
+ resolve,
352
+ reject
353
+ });
354
+
355
+ // Process queue if not already processing
356
+ if (!this.processing) {
357
+ this.processQueue();
358
+ }
359
+ });
360
+ }
361
+
362
+ // Flush buffer to Chrome storage
363
+ async flush() {
364
+ return new Promise((resolve, reject) => {
365
+ this.processingQueue.push({
366
+ type: 'flush',
367
+ resolve,
368
+ reject
369
+ });
370
+
371
+ // Process queue if not already processing
372
+ if (!this.processing) {
373
+ this.processQueue();
374
+ }
375
+ });
376
+ }
377
+
378
+ // Sequential processing of queue to prevent race conditions
379
+ async processQueue() {
380
+ if (this.processing) {
381
+ return; // Already processing
382
+ }
383
+
384
+ this.processing = true;
385
+
386
+ try {
387
+ while (this.processingQueue.length > 0) {
388
+ const operation = this.processingQueue.shift();
389
+
390
+ try {
391
+ if (operation.type === 'add') {
392
+ this.buffer.push(operation.logEntry);
393
+ this.totalLogsAdded++;
394
+
395
+ // Auto-flush if buffer gets too large
396
+ if (this.buffer.length >= 100) {
397
+ await this.performFlush();
398
+ }
399
+
400
+ operation.resolve();
401
+ } else if (operation.type === 'flush') {
402
+ await this.performFlush();
403
+ operation.resolve();
404
+ }
405
+ } catch (error) {
406
+ operation.reject(error);
407
+ }
408
+ }
409
+ } finally {
410
+ this.processing = false;
411
+ }
412
+ }
413
+
414
+ // Actual flush operation to Chrome storage
415
+ async performFlush() {
416
+ if (this.buffer.length === 0) {
417
+ return; // Nothing to flush
418
+ }
419
+
420
+ try {
421
+ // Read current logs from storage
422
+ const result = await chrome.storage.session.get(String(this.tabId));
423
+ const existingLogs = result[String(this.tabId)] || [];
424
+
425
+ // Append new logs
426
+ const updatedLogs = [...existingLogs, ...this.buffer];
427
+
428
+ // Check storage size limit
429
+ const estimatedSize = JSON.stringify(updatedLogs).length;
430
+ if (estimatedSize > 500000) { // ~500KB limit
431
+ const beforeCount = updatedLogs.length;
432
+ const retentionPercentage = 0.95; // Increased from 0.8 to preserve more logs
433
+ const afterCount = Math.floor(updatedLogs.length * retentionPercentage);
434
+ console.warn(`[LogBuffer] Storage size limit approached for tab ${this.tabId}, truncating from ${beforeCount} to ${afterCount} logs (${Math.round(retentionPercentage * 100)}% retention)`);
435
+ // Keep only the most recent logs that fit within the limit
436
+ const recentLogs = updatedLogs.slice(-afterCount);
437
+ await chrome.storage.session.set({ [String(this.tabId)]: recentLogs });
438
+ } else {
439
+ await chrome.storage.session.set({ [String(this.tabId)]: updatedLogs });
440
+ }
441
+
442
+ this.totalLogsFlushed += this.buffer.length;
443
+ console.log(`[LogBuffer] Flushed ${this.buffer.length} logs for tab ${this.tabId} (total: ${this.totalLogsFlushed})`);
444
+
445
+ // Clear the buffer
446
+ this.buffer = [];
447
+ this.lastFlushTime = Date.now();
448
+
449
+ } catch (error) {
450
+ console.error(`[LogBuffer] Failed to flush logs for tab ${this.tabId}:`, error);
451
+ throw error;
452
+ }
453
+ }
454
+
455
+ // Get buffer statistics
456
+ getStats() {
457
+ return {
458
+ bufferedLogs: this.buffer.length,
459
+ totalAdded: this.totalLogsAdded,
460
+ totalFlushed: this.totalLogsFlushed,
461
+ processing: this.processing,
462
+ queueLength: this.processingQueue.length,
463
+ lastFlushTime: this.lastFlushTime
464
+ };
465
+ }
466
+ }
467
+
468
+ // Initialize data buffer and upload manager
469
+ let dataBuffer = null;
470
+ let uploadManager = null;
471
+ let logStreamer = null;
472
+ let logBuffer = null;
473
+
474
+ // Error handling state and configuration
475
+ // IMPORTANT: These must be declared before initializeServices()
476
+ // as they're used in initializeErrorHandling() which is called during startup
477
+ let errorHandlingState = {
478
+ currentState: 'NORMAL_OPERATION',
479
+ failedSaveCount: 0,
480
+ lastNotificationTime: 0,
481
+ retainedInteractions: [],
482
+ lastError: null,
483
+ circuitBreakerState: 'CLOSED',
484
+ sessionId: null,
485
+ isInitialized: false
486
+ };
487
+
488
+ // Configuration constants (based on TypeScript interfaces)
489
+ const ERROR_HANDLING_CONFIG = {
490
+ maxDataLossTime: 30000, // 30 seconds max data loss
491
+ maxRetainedInteractions: 100, // Memory limit
492
+ notificationThrottleMs: 5000, // User experience
493
+ maxRetryAttempts: 3,
494
+ backupRetentionDays: 7,
495
+ failureThreshold: 3, // Circuit breaker
496
+ resetTimeout: 30000 // Circuit breaker reset
497
+ };
498
+
499
+ // Data protection tiers
500
+ const DATA_PROTECTION_TIERS = {
501
+ CRITICAL_DATA: {
502
+ maxLoss: 0,
503
+ includes: ['session_metadata', 'user_authentication'],
504
+ storage: 'PERSISTENT_BACKUP'
505
+ },
506
+ HIGH_VALUE_DATA: {
507
+ maxLoss: 30000,
508
+ includes: ['user_interactions', 'form_data'],
509
+ storage: 'REDUNDANT_STORAGE'
510
+ },
511
+ RECOVERABLE_DATA: {
512
+ maxLoss: 300000,
513
+ includes: ['performance_metrics', 'debug_logs'],
514
+ storage: 'SINGLE_STORAGE'
515
+ }
516
+ };
517
+
518
+ // Initialize on startup
519
+ async function initializeServices() {
520
+ try {
521
+ // FIRST: Initialize comprehensive error handling system
522
+ console.log('[Background] Initializing comprehensive error handling...');
523
+ const errorHandlingSuccess = await initializeErrorHandling();
524
+ if (errorHandlingSuccess) {
525
+ console.log('[Background] Comprehensive error handling initialized successfully');
526
+ } else {
527
+ console.warn('[Background] Error handling initialization failed - continuing with basic functionality');
528
+ }
529
+
530
+ // Initialize the data buffer
531
+ dataBuffer = new DataBuffer();
532
+ await dataBuffer.init();
533
+ console.log('[Background] Data buffer initialized');
534
+
535
+ // Only create upload manager if data buffer initialized successfully
536
+ try {
537
+ uploadManager = new UploadManager(dataBuffer);
538
+ const serverAvailable = await uploadManager.init();
539
+ if (serverAvailable) {
540
+ console.log('[Background] Upload manager initialized with server');
541
+ } else {
542
+ console.log('[Background] Upload manager initialized but no server available yet');
543
+ }
544
+ } catch (uploadErr) {
545
+ console.error('[Background] Failed to initialize upload manager:', uploadErr);
546
+ // Upload manager failed but data buffer is still usable
547
+ uploadManager = null;
548
+ }
549
+
550
+ // Initialize log streamer
551
+ try {
552
+ logStreamer = new LogStreamer();
553
+ await logStreamer.init();
554
+ console.log('[Background] Log streamer initialized');
555
+ } catch (streamErr) {
556
+ console.error('[Background] Failed to initialize log streamer:', streamErr);
557
+ logStreamer = null;
558
+ }
559
+
560
+ // Initialize log buffer for race-condition-free storage
561
+ try {
562
+ logBuffer = new LogBuffer();
563
+ console.log('[Background] Log buffer initialized');
564
+ } catch (bufferErr) {
565
+ console.error('[Background] Failed to initialize log buffer:', bufferErr);
566
+ logBuffer = null;
567
+ }
568
+
569
+ // Initialize session manager with recovery
570
+ try {
571
+ if (!sessionManager) {
572
+ sessionManager = new ChromeExtensionSessionManager();
573
+ console.log('[Background] Session manager initialized');
574
+ }
575
+
576
+ // Recover any stuck sessions on startup
577
+ await sessionManager.recoverSessions();
578
+ console.log('[Background] Session recovery completed');
579
+ } catch (sessionErr) {
580
+ console.error('[Background] Failed to initialize session manager:', sessionErr);
581
+ // Session manager critical for recording - but don't fail completely
582
+ }
583
+ } catch (err) {
584
+ console.error('[Background] Failed to initialize data buffer:', err);
585
+ // Both failed - system won't work properly
586
+ dataBuffer = null;
587
+ uploadManager = null;
588
+ logStreamer = null;
589
+ }
590
+ }
591
+
592
+ // Start initialization
593
+ initializeServices();
594
+
595
+ // Listen for messages from content scripts
596
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
597
+ // Handle messages with different structures
598
+ if (!message) {
599
+ console.warn('[Background] Received null/undefined message');
600
+ return;
601
+ }
602
+
603
+ // Some messages use 'action' instead of 'type'
604
+ const messageType = message.type || message.action;
605
+
606
+ if (!messageType) {
607
+ // Don't warn for messages handled by other listeners
608
+ if (!message.recordingId && !message.sessionId) {
609
+ console.warn('[Background] Received message without type/action:', message);
610
+ }
611
+ return; // Don't send response for messages handled elsewhere
612
+ }
613
+
614
+ // Check if this message should be handled by the other listener (v2.0.6 fix)
615
+ const handledByOtherListener = [
616
+ 'startWorkflowRecording',
617
+ 'stopWorkflowRecording',
618
+ 'sendToServer',
619
+ 'getScreenshotFormats',
620
+ 'saveWorkflowRecording',
621
+ 'getWorkflowRecordings',
622
+ 'playWorkflowRecording',
623
+ 'playWorkflowByName',
624
+ 'deleteWorkflowRecording',
625
+ 'editWorkflowRecording',
626
+ 'saveRestorePoint',
627
+ 'getRestorePoints',
628
+ 'restoreFromPoint',
629
+ 'getWebSocketPort',
630
+ 'startRecording',
631
+ 'checkConnection',
632
+ 'workflowConsoleLog', // v2.0.8: handle workflow console logs
633
+ 'workflowAction', // v2.0.8: handle workflow actions
634
+ 'renewLease' // v2.1.2: session manager lease renewal for frame capture
635
+ ];
636
+
637
+ if (handledByOtherListener.includes(messageType)) {
638
+ // Let the other listener handle this message
639
+ return;
640
+ }
641
+
642
+ switch (messageType) {
643
+ case 'START_UPLOAD_MANAGER':
644
+ // Upload manager is already initialized and running
645
+ sendResponse({ status: 'success', message: 'Upload manager already running' });
646
+ break;
647
+
648
+ case 'FINALIZE_UPLOADS':
649
+ // Force upload and stop periodic uploads
650
+ const recordingId = message.recordingId || currentRecordingSessionId;
651
+ if (uploadManager) {
652
+ if (recordingId) {
653
+ uploadManager.forceUpload(recordingId).then(() => {
654
+ uploadManager.stopPeriodicUpload();
655
+ sendResponse({ status: 'success', message: 'Uploads finalized' });
656
+ }).catch(error => {
657
+ console.error('[Background] Failed to finalize uploads:', error);
658
+ sendResponse({ status: 'error', message: error.message });
659
+ });
660
+ } else {
661
+ // No recording ID, just stop periodic uploads
662
+ uploadManager.stopPeriodicUpload();
663
+ sendResponse({ status: 'success', message: 'No pending uploads' });
664
+ }
665
+ } else {
666
+ sendResponse({ status: 'success', message: 'Upload manager not initialized' });
667
+ }
668
+ // Return true to indicate async response
669
+ return true;
670
+
671
+ case 'GET_UPLOAD_STATS':
672
+ try {
673
+ const stats = uploadManager ? uploadManager.getStats() : { uploadQueue: 0, serverUrl: null };
674
+ sendResponse({ status: 'success', stats: stats });
675
+ } catch (error) {
676
+ console.error('[Background] Failed to get upload stats:', error);
677
+ sendResponse({ status: 'error', message: error.message });
678
+ }
679
+ break;
680
+
681
+ case 'UPLOAD_BATCH':
682
+ // Add events to the background's DataBuffer for upload
683
+ if (message.events && message.recordingId) {
684
+ if (dataBuffer) {
685
+ // Ensure all events have the recording_id set
686
+ const eventsWithRecordingId = message.events.map(event => ({
687
+ ...event,
688
+ recording_id: message.recordingId
689
+ }));
690
+
691
+ dataBuffer.addBatch(eventsWithRecordingId).then(() => {
692
+ console.log(`[Background] Added ${eventsWithRecordingId.length} events to buffer for recording ${message.recordingId}`);
693
+ // Create a batch if we have enough events
694
+ return dataBuffer.createBatch(message.recordingId);
695
+ }).then(batch => {
696
+ if (batch) {
697
+ console.log(`[Background] Created batch ${batch.id} with ${batch.events.length} events`);
698
+ // Process upload queue if upload manager is available
699
+ if (uploadManager) {
700
+ uploadManager.processUploadQueue();
701
+ }
702
+ }
703
+ sendResponse({ status: 'success' });
704
+ }).catch(error => {
705
+ console.error('[Background] Failed to process batch:', error);
706
+ sendResponse({ status: 'error', message: error.message });
707
+ });
708
+ // Return true to indicate async response
709
+ return true;
710
+ } else {
711
+ sendResponse({ status: 'error', message: 'Data buffer not initialized' });
712
+ }
713
+ } else {
714
+ sendResponse({ status: 'error', message: 'Missing events or recordingId' });
715
+ }
716
+ break;
717
+
718
+ case 'QUEUE_DATA_FOR_UPLOAD':
719
+ if (message.payload && uploadManager) {
720
+ uploadManager.queueForUpload(message.payload.recordingId, message.payload.events).then(() => {
721
+ sendResponse({ status: 'success', message: 'Data queued for upload' });
722
+ }).catch(error => {
723
+ console.error('[Background] Failed to queue data:', error);
724
+ sendResponse({ status: 'error', message: error.message });
725
+ });
726
+ return true;
727
+ } else {
728
+ sendResponse({ status: 'error', message: 'No payload provided' });
729
+ }
730
+ break;
731
+
732
+ case 'show-frame-flash':
733
+ // Handle frame flash indicator - already handled elsewhere
734
+ // Don't send a response to avoid warnings
735
+ break;
736
+
737
+ case 'frame-batch-ready':
738
+ // Handle frame batch ready - already handled elsewhere
739
+ // Don't send a response to avoid warnings
740
+ break;
741
+
742
+ case 'frame-capture-complete':
743
+ // Handle frame capture complete - already handled elsewhere
744
+ // Don't send a response to avoid warnings
745
+ break;
746
+
747
+ case 'consoleLog':
748
+ // Handle console log - already handled elsewhere
749
+ // Don't send a response to avoid warnings
750
+ break;
751
+
752
+ case 'stopRecording':
753
+ // Handle stop recording - already handled elsewhere
754
+ // Don't send a response to avoid warnings
755
+ break;
756
+
757
+ // Visual feedback system message handlers
758
+ case 'start-screen-capture-tracking':
759
+ // Forward to content script for visual feedback
760
+ if (message.sessionId && sender.tab) {
761
+ chrome.tabs.sendMessage(sender.tab.id, {
762
+ type: 'start-screen-capture-tracking',
763
+ sessionId: message.sessionId
764
+ }).catch(() => {
765
+ // Content script might not be available
766
+ console.log('[Background] Could not forward start-screen-capture-tracking to content script');
767
+ });
768
+ }
769
+ sendResponse({ success: true });
770
+ break;
771
+
772
+ case 'stop-screen-capture-tracking':
773
+ // Forward to content script
774
+ if (sender.tab) {
775
+ chrome.tabs.sendMessage(sender.tab.id, {
776
+ type: 'stop-screen-capture-tracking'
777
+ }).catch(() => {
778
+ console.log('[Background] Could not forward stop-screen-capture-tracking to content script');
779
+ });
780
+ }
781
+ sendResponse({ success: true });
782
+ break;
783
+
784
+ case 'screen-capture-interaction-logged':
785
+ // Handle interaction logged from content script visual feedback
786
+ if (message.interaction && sender.tab) {
787
+ const tabId = sender.tab.id;
788
+
789
+ // Use session manager for validation if available
790
+ if (sessionManager) {
791
+ isCurrentlyRecordingAsync().then(async (recording) => {
792
+ const currentTabId = await getCurrentTabIdAsync();
793
+ if (recording && tabId === currentTabId) {
794
+ const sessionId = await getCurrentSessionIdAsync();
795
+ let frameIndex = 0;
796
+
797
+ // Get frame count from session manager
798
+ const state = await sessionManager.getCurrentState();
799
+ if (state && state.sessionId === sessionId) {
800
+ frameIndex = state.frameCount || 0;
801
+ }
802
+
803
+ const interaction = {
804
+ ...message.interaction,
805
+ frameIndex: frameIndex
806
+ };
807
+ screenInteractions.push(interaction);
808
+ console.log('[Background] Visual feedback interaction recorded:', interaction.type, 'Total:', screenInteractions.length);
809
+ }
810
+ }).catch(error => {
811
+ console.error('Error validating recording state for interaction:', error);
812
+ });
813
+ } else {
814
+ // Legacy fallback
815
+ if (tabId === recordingTabId && isCurrentlyRecording) {
816
+ const interaction = {
817
+ ...message.interaction,
818
+ frameIndex: frameCounter.get(currentRecordingSessionId) || 0
819
+ };
820
+ screenInteractions.push(interaction);
821
+ console.log('[Background] Visual feedback interaction recorded:', interaction.type, 'Total:', screenInteractions.length);
822
+ }
823
+ }
824
+ }
825
+ // Don't send response to avoid warnings
826
+ break;
827
+
828
+ case 'show-screen-capture-flash':
829
+ // Forward flash trigger to content script
830
+ if (sender.tab) {
831
+ chrome.tabs.sendMessage(sender.tab.id, {
832
+ type: 'screen-capture-frame-captured'
833
+ }).catch(() => {
834
+ console.log('[Background] Could not forward frame flash to content script');
835
+ });
836
+ }
837
+ // Don't send response to avoid warnings
838
+ break;
839
+
840
+ case 'test-logbuffer-race-condition':
841
+ // DEBUG: Test LogBuffer race condition fix
842
+ if (logBuffer && sender.tab) {
843
+ const tabId = sender.tab.id;
844
+ const testLogsCount = message.count || 1000;
845
+ console.log(`[Background] Starting LogBuffer race condition test with ${testLogsCount} logs for tab ${tabId}`);
846
+
847
+ // Simulate rapid log generation like a real high-volume scenario
848
+ const startTime = Date.now();
849
+ const logPromises = [];
850
+
851
+ for (let i = 0; i < testLogsCount; i++) {
852
+ const testLog = {
853
+ type: 'log',
854
+ message: `Test log ${i + 1}/${testLogsCount}`,
855
+ timestamp: Date.now(),
856
+ level: 'info',
857
+ source: 'test-harness'
858
+ };
859
+
860
+ // Add logs concurrently to test race condition handling
861
+ logPromises.push(logBuffer.addLog(tabId, testLog));
862
+ }
863
+
864
+ // Wait for all logs to be added and then check stats
865
+ Promise.all(logPromises).then(() => {
866
+ const endTime = Date.now();
867
+ const stats = logBuffer.getStats();
868
+ console.log(`[Background] LogBuffer test completed in ${endTime - startTime}ms`);
869
+ console.log(`[Background] Test results:`, stats);
870
+
871
+ sendResponse({
872
+ success: true,
873
+ duration: endTime - startTime,
874
+ logsAdded: testLogsCount,
875
+ stats: stats
876
+ });
877
+ }).catch(error => {
878
+ console.error('[Background] LogBuffer test failed:', error);
879
+ sendResponse({
880
+ success: false,
881
+ error: error.message,
882
+ stats: logBuffer.getStats()
883
+ });
884
+ });
885
+
886
+ return true; // Indicates async response
887
+ } else {
888
+ sendResponse({
889
+ success: false,
890
+ error: 'LogBuffer not available or no tab context'
891
+ });
892
+ }
893
+ break;
894
+
895
+ case 'get-logbuffer-stats':
896
+ // DEBUG: Get current LogBuffer statistics
897
+ if (logBuffer) {
898
+ const stats = logBuffer.getStats();
899
+ console.log('[Background] LogBuffer stats requested:', stats);
900
+ sendResponse({
901
+ success: true,
902
+ stats: stats
903
+ });
904
+ } else {
905
+ sendResponse({
906
+ success: false,
907
+ error: 'LogBuffer not available'
908
+ });
909
+ }
910
+ break;
911
+
912
+ default:
913
+ // Don't warn or respond for messages that should be handled by other listener (v2.0.6)
914
+ // This prevents intercepting workflow recording messages
915
+ console.log('[Background v2.0.6] Message not handled by this listener:', messageType);
916
+ // Don't send response - let other listener handle it
917
+ return;
918
+ }
919
+ });
920
+
921
+ // Session recovery on startup - handle any stuck recordings from previous session
922
+ chrome.runtime.onStartup.addListener(async () => {
923
+ console.log('[Background] Extension startup detected - checking for stuck sessions');
924
+
925
+ if (sessionManager) {
926
+ try {
927
+ // Use existing recovery method from session manager
928
+ await sessionManager.recoverSessions();
929
+ console.log('[Background] Startup session recovery completed');
930
+ } catch (error) {
931
+ console.error('[Background] Failed to recover sessions on startup:', error);
932
+ }
933
+ } else {
934
+ console.warn('[Background] Session manager not available for startup recovery');
935
+ }
936
+ });
937
+
938
+ // Listen for tab updates to handle restore points
939
+ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
940
+ // Inject console logging on navigation during recording
941
+ if (changeInfo.status === 'loading') {
942
+ // Use session manager validation if available
943
+ if (sessionManager) {
944
+ isCurrentlyRecordingAsync().then(async (recording) => {
945
+ const currentTabId = await getCurrentTabIdAsync();
946
+ if (recording && tabId === currentTabId && tab.url && !tab.url.startsWith('chrome://') && !tab.url.startsWith('chrome-extension://')) {
947
+ console.log('Re-injecting console logging for navigation during recording');
948
+ startCapturingLogs(tabId).catch(err => {
949
+ console.error('Failed to re-inject console logging:', err);
950
+ });
951
+ }
952
+ }).catch(error => {
953
+ console.error('Error checking recording state for navigation:', error);
954
+ });
955
+ } else {
956
+ // Legacy fallback
957
+ if (tabId === recordingTabId && isCurrentlyRecording && tab.url && !tab.url.startsWith('chrome://') && !tab.url.startsWith('chrome-extension://')) {
958
+ console.log('Re-injecting console logging for navigation during recording');
959
+ startCapturingLogs(tabId).catch(err => {
960
+ console.error('Failed to re-inject console logging:', err);
961
+ });
962
+ }
963
+ }
964
+ }
965
+
966
+ if (changeInfo.status === 'complete' && tab.url && !tab.url.startsWith('chrome://')) {
967
+ // Check for pending restore point
968
+ chrome.storage.session.get(`pendingRestore_${tabId}`).then(async (result) => {
969
+ if (result[`pendingRestore_${tabId}`]) {
970
+ const { restorePointId, url } = result[`pendingRestore_${tabId}`];
971
+
972
+ // Clear the pending restore
973
+ chrome.storage.session.remove(`pendingRestore_${tabId}`);
974
+
975
+ // Wait a bit for page to fully load
976
+ setTimeout(async () => {
977
+ // Get restore point data from server
978
+ const ports = CONFIG_PORTS;
979
+ let restorePointData = null;
980
+
981
+ // Use comprehensive error handling for restore point fetch
982
+ const fetchRestorePoint = async (data, context) => {
983
+ for (const port of ports) {
984
+ try {
985
+ const response = await fetch(`http://localhost:${port}/chromedebug/restore-point/${restorePointId}`, {
986
+ method: 'GET',
987
+ mode: 'cors'
988
+ });
989
+
990
+ if (response.ok) {
991
+ const result = await response.json();
992
+ return { success: true, result };
993
+ } else {
994
+ throw new Error(`HTTP ${response.status}: Failed to fetch restore point from port ${port}`);
995
+ }
996
+ } catch (error) {
997
+ console.error(`[ErrorHandling] Restore point fetch failed on port ${port}:`, error);
998
+ // Continue to next port instead of silent failure
999
+ }
1000
+ }
1001
+ throw new Error('All server ports failed for restore point fetch');
1002
+ };
1003
+
1004
+ const restoreResult = await handleSaveAttempt(
1005
+ fetchRestorePoint,
1006
+ { restorePointId },
1007
+ { operation: 'restore_point_fetch', restorePointId }
1008
+ );
1009
+
1010
+ if (restoreResult.success) {
1011
+ restorePointData = restoreResult.result.result;
1012
+ } else {
1013
+ console.error('[ErrorHandling] Failed to fetch restore point after comprehensive error handling');
1014
+ }
1015
+
1016
+ if (restorePointData) {
1017
+ // Send restore message to content script
1018
+ chrome.tabs.sendMessage(tabId, {
1019
+ action: 'restoreFromPoint',
1020
+ restorePointData: restorePointData
1021
+ }, (response) => {
1022
+ if (chrome.runtime.lastError) {
1023
+ console.error('Error restoring from point after navigation:', chrome.runtime.lastError);
1024
+ }
1025
+ });
1026
+ }
1027
+ }, 1000); // Wait 1 second for page to stabilize
1028
+ }
1029
+ });
1030
+ }
1031
+ });
1032
+
1033
+ // Session Manager for robust recording state management
1034
+ let sessionManager = null;
1035
+ let currentSession = null; // Cache current session state
1036
+ let currentOwnerId = null; // Track current session owner
1037
+
1038
+ // Essential recording state (non-session related)
1039
+ let hasOffscreenDocument = false;
1040
+ let screenInteractions = [];
1041
+ let recordingServerPort = null; // Track which port is being used for current recording
1042
+
1043
+ // Legacy compatibility variables (DEPRECATED - remove after migration is complete)
1044
+ let recordingTabId = null; // DEPRECATED: Use getCurrentTabIdAsync() instead
1045
+ let isCurrentlyRecording = false; // DEPRECATED: Use isCurrentlyRecordingAsync() instead
1046
+ let currentRecordingSessionId = null; // DEPRECATED: Use getCurrentSessionIdAsync() instead
1047
+ let frameCounter = new Map(); // DEPRECATED: Session manager handles frame counting
1048
+
1049
+ // Initialize session manager
1050
+ (async function initializeSessionManager() {
1051
+ try {
1052
+ sessionManager = new ChromeExtensionSessionManager();
1053
+ console.log('[ChromeSessionManager] Initialized successfully');
1054
+ } catch (error) {
1055
+ console.error('[ChromeSessionManager] Failed to initialize:', error);
1056
+ }
1057
+ })();
1058
+
1059
+ //=============================================================================
1060
+ // SESSION MANAGER ADAPTER FUNCTIONS
1061
+ // Provides compatibility layer between legacy API and new session manager
1062
+ //=============================================================================
1063
+
1064
+ /**
1065
+ * Check if currently recording (adapter function)
1066
+ * @returns {Promise<boolean>} Recording status
1067
+ */
1068
+ async function isCurrentlyRecordingAsync() {
1069
+ if (!sessionManager) return false;
1070
+ try {
1071
+ const state = await sessionManager.getCurrentState();
1072
+ return state?.isRecording || false;
1073
+ } catch (error) {
1074
+ console.error('[SessionAdapter] Failed to check recording status:', error);
1075
+ return false;
1076
+ }
1077
+ }
1078
+
1079
+ /**
1080
+ * Get current session ID (adapter function)
1081
+ * @returns {Promise<string|null>} Session ID
1082
+ */
1083
+ async function getCurrentSessionIdAsync() {
1084
+ if (!sessionManager) return null;
1085
+ try {
1086
+ const state = await sessionManager.getCurrentState();
1087
+ return state?.sessionId || null;
1088
+ } catch (error) {
1089
+ console.error('[SessionAdapter] Failed to get session ID:', error);
1090
+ return null;
1091
+ }
1092
+ }
1093
+
1094
+ /**
1095
+ * Get current recording tab ID (adapter function)
1096
+ * @returns {Promise<number|null>} Tab ID
1097
+ */
1098
+ async function getCurrentTabIdAsync() {
1099
+ if (!sessionManager) return null;
1100
+ try {
1101
+ const state = await sessionManager.getCurrentState();
1102
+ return state?.settings?.tabId || null;
1103
+ } catch (error) {
1104
+ console.error('[SessionAdapter] Failed to get tab ID:', error);
1105
+ return null;
1106
+ }
1107
+ }
1108
+
1109
+ /**
1110
+ * Get complete current recording state (new wrapper function)
1111
+ * Provides both session manager state and legacy fallback
1112
+ * @returns {Promise<Object>} Recording state object
1113
+ */
1114
+ async function getCurrentRecordingState() {
1115
+ if (sessionManager?.isInitialized && await sessionManager.isInitialized()) {
1116
+ try {
1117
+ const sessionState = await sessionManager.getCurrentState();
1118
+ if (sessionState) {
1119
+ return {
1120
+ isRecording: sessionState.isRecording,
1121
+ tabId: sessionState.settings?.tabId,
1122
+ sessionId: sessionState.sessionId,
1123
+ startTime: sessionState.startTime,
1124
+ frameCount: sessionState.frameCount,
1125
+ status: sessionState.status,
1126
+ source: 'sessionManager'
1127
+ };
1128
+ }
1129
+ } catch (error) {
1130
+ console.error('[SessionAdapter] Failed to get session manager state:', error);
1131
+ }
1132
+ }
1133
+
1134
+ // Fallback to legacy state for backward compatibility
1135
+ return {
1136
+ isRecording: isCurrentlyRecording,
1137
+ tabId: recordingTabId,
1138
+ sessionId: currentRecordingSessionId,
1139
+ source: 'legacy'
1140
+ };
1141
+ }
1142
+
1143
+ /**
1144
+ * Validate current session (adapter function)
1145
+ * @returns {Promise<boolean>} Session validity
1146
+ */
1147
+ async function isSessionValidAsync() {
1148
+ if (!sessionManager || !currentSession?.sessionId) return false;
1149
+ try {
1150
+ const result = await sessionManager.isSessionValid(currentSession.sessionId);
1151
+ return result.valid;
1152
+ } catch (error) {
1153
+ console.error('[SessionAdapter] Failed to validate session:', error);
1154
+ return false;
1155
+ }
1156
+ }
1157
+
1158
+ /**
1159
+ * Synchronous fallback for legacy compatibility (DEPRECATED)
1160
+ * Use async versions above for new code
1161
+ */
1162
+ function getCurrentSessionIdSync() {
1163
+ return currentSession?.sessionId || currentRecordingSessionId;
1164
+ }
1165
+
1166
+ function isCurrentlyRecordingSync() {
1167
+ return currentSession?.isRecording || isCurrentlyRecording;
1168
+ }
1169
+
1170
+ // Workflow recording state
1171
+ let workflowRecordingTabs = new Map(); // Map of tabId to recording state
1172
+ let workflowIncludeLogs = new Map(); // Map of tabId to includeLogsInExport setting
1173
+ let workflowScreenshotSettings = new Map(); // Map of tabId to screenshot settings
1174
+ let workflowSessionNames = new Map(); // Map of tabId to session name
1175
+ let workflowUserIds = new Map(); // Map of tabId to userId for license tracking
1176
+
1177
+ //=============================================================================
1178
+ // COMPREHENSIVE ERROR HANDLING SYSTEM
1179
+ // Designed collaboratively with Second Opinion Analyst and Testing Expert
1180
+ // Replaces silent failures with robust error management and data protection
1181
+ //=============================================================================
1182
+
1183
+ /**
1184
+ * Initialize the comprehensive error handling system
1185
+ * Called on extension startup - replaces silent failure patterns
1186
+ */
1187
+ async function initializeErrorHandling() {
1188
+ try {
1189
+ console.log('[ErrorHandling] Initializing comprehensive error handling system...');
1190
+
1191
+ // Generate session ID if not exists
1192
+ if (!errorHandlingState.sessionId) {
1193
+ errorHandlingState.sessionId = `error_session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1194
+ }
1195
+
1196
+ // Initialize IndexedDB for backup storage
1197
+ await initializeBackupStorage();
1198
+
1199
+ // Initialize circuit breaker
1200
+ initializeCircuitBreaker();
1201
+
1202
+ // Set up notification system
1203
+ await setupNotificationSystem();
1204
+
1205
+ // Clean up old backups
1206
+ await cleanupOldBackups();
1207
+
1208
+ // Mark as initialized
1209
+ errorHandlingState.isInitialized = true;
1210
+ errorHandlingState.currentState = 'NORMAL_OPERATION';
1211
+
1212
+ console.log('[ErrorHandling] System initialization complete:', {
1213
+ sessionId: errorHandlingState.sessionId,
1214
+ state: errorHandlingState.currentState,
1215
+ circuitBreaker: errorHandlingState.circuitBreakerState
1216
+ });
1217
+
1218
+ return true;
1219
+ } catch (error) {
1220
+ console.error('[ErrorHandling] CRITICAL: Failed to initialize error handling system:', error);
1221
+ // Even if initialization fails, ensure we have basic error state
1222
+ errorHandlingState.isInitialized = false;
1223
+ errorHandlingState.currentState = 'CRITICAL_FAILURE';
1224
+ return false;
1225
+ }
1226
+ }
1227
+
1228
+ /**
1229
+ * Initialize IndexedDB for persistent backup storage
1230
+ */
1231
+ async function initializeBackupStorage() {
1232
+ return new Promise((resolve, reject) => {
1233
+ const request = indexedDB.open('ChromePilotErrorRecovery', 1);
1234
+
1235
+ request.onerror = () => {
1236
+ console.error('[ErrorHandling] Failed to open IndexedDB:', request.error);
1237
+ reject(request.error);
1238
+ };
1239
+
1240
+ request.onsuccess = () => {
1241
+ console.log('[ErrorHandling] IndexedDB initialized successfully');
1242
+ resolve(request.result);
1243
+ };
1244
+
1245
+ request.onupgradeneeded = (event) => {
1246
+ const db = event.target.result;
1247
+
1248
+ // Create interactions store
1249
+ if (!db.objectStoreNames.contains('interactions')) {
1250
+ const interactionsStore = db.createObjectStore('interactions', { keyPath: 'id', autoIncrement: true });
1251
+ interactionsStore.createIndex('sessionId', 'sessionId', { unique: false });
1252
+ interactionsStore.createIndex('timestamp', 'timestamp', { unique: false });
1253
+ interactionsStore.createIndex('type', 'type', { unique: false });
1254
+ }
1255
+
1256
+ // Create session backups store
1257
+ if (!db.objectStoreNames.contains('sessionBackups')) {
1258
+ const backupsStore = db.createObjectStore('sessionBackups', { keyPath: 'sessionId' });
1259
+ backupsStore.createIndex('timestamp', 'timestamp', { unique: false });
1260
+ backupsStore.createIndex('retryCount', 'retryCount', { unique: false });
1261
+ }
1262
+
1263
+ // Create error states store
1264
+ if (!db.objectStoreNames.contains('errorStates')) {
1265
+ const errorStore = db.createObjectStore('errorStates', { keyPath: 'id', autoIncrement: true });
1266
+ errorStore.createIndex('sessionId', 'sessionId', { unique: false });
1267
+ errorStore.createIndex('errorType', 'errorType', { unique: false });
1268
+ errorStore.createIndex('timestamp', 'timestamp', { unique: false });
1269
+ }
1270
+
1271
+ console.log('[ErrorHandling] IndexedDB schema created');
1272
+ };
1273
+ });
1274
+ }
1275
+
1276
+ /**
1277
+ * Initialize circuit breaker pattern
1278
+ */
1279
+ function initializeCircuitBreaker() {
1280
+ errorHandlingState.circuitBreakerState = 'CLOSED';
1281
+ errorHandlingState.failedSaveCount = 0;
1282
+ console.log('[ErrorHandling] Circuit breaker initialized in CLOSED state');
1283
+ }
1284
+
1285
+ /**
1286
+ * Set up notification system with throttling
1287
+ */
1288
+ async function setupNotificationSystem() {
1289
+ // Clear any existing notifications
1290
+ const notifications = await chrome.notifications.getAll();
1291
+ for (const notificationId in notifications) {
1292
+ await chrome.notifications.clear(notificationId);
1293
+ }
1294
+
1295
+ console.log('[ErrorHandling] Notification system ready');
1296
+ }
1297
+
1298
+ /**
1299
+ * Clean up old backup data (retention policy)
1300
+ */
1301
+ async function cleanupOldBackups() {
1302
+ try {
1303
+ const cutoffTime = Date.now() - (ERROR_HANDLING_CONFIG.backupRetentionDays * 24 * 60 * 60 * 1000);
1304
+
1305
+ const request = indexedDB.open('ChromePilotErrorRecovery', 1);
1306
+ request.onsuccess = () => {
1307
+ const db = request.result;
1308
+ const transaction = db.transaction(['sessionBackups', 'errorStates'], 'readwrite');
1309
+
1310
+ // Clean up old session backups
1311
+ const backupsStore = transaction.objectStore('sessionBackups');
1312
+ const backupsIndex = backupsStore.index('timestamp');
1313
+ const backupsRange = IDBKeyRange.upperBound(cutoffTime);
1314
+ backupsIndex.openCursor(backupsRange).onsuccess = (event) => {
1315
+ const cursor = event.target.result;
1316
+ if (cursor) {
1317
+ cursor.delete();
1318
+ cursor.continue();
1319
+ }
1320
+ };
1321
+
1322
+ // Clean up old error states
1323
+ const errorStore = transaction.objectStore('errorStates');
1324
+ const errorIndex = errorStore.index('timestamp');
1325
+ const errorRange = IDBKeyRange.upperBound(cutoffTime);
1326
+ errorIndex.openCursor(errorRange).onsuccess = (event) => {
1327
+ const cursor = event.target.result;
1328
+ if (cursor) {
1329
+ cursor.delete();
1330
+ cursor.continue();
1331
+ }
1332
+ };
1333
+
1334
+ console.log('[ErrorHandling] Cleanup completed for data older than', ERROR_HANDLING_CONFIG.backupRetentionDays, 'days');
1335
+ };
1336
+ } catch (error) {
1337
+ console.error('[ErrorHandling] Failed to cleanup old backups:', error);
1338
+ }
1339
+ }
1340
+
1341
+ /**
1342
+ * Handle save attempt with comprehensive error management
1343
+ * Replaces silent failure patterns with circuit breaker and retry logic
1344
+ */
1345
+ async function handleSaveAttempt(saveFunction, data, context = {}) {
1346
+ if (!errorHandlingState.isInitialized) {
1347
+ console.warn('[ErrorHandling] System not initialized, attempting save anyway');
1348
+ }
1349
+
1350
+ const attempt = {
1351
+ timestamp: Date.now(),
1352
+ context: context,
1353
+ data: data
1354
+ };
1355
+
1356
+ try {
1357
+ // Check circuit breaker state
1358
+ if (errorHandlingState.circuitBreakerState === 'OPEN') {
1359
+ const timeSinceLastFailure = Date.now() - (errorHandlingState.lastError?.timestamp || 0);
1360
+ if (timeSinceLastFailure < ERROR_HANDLING_CONFIG.resetTimeout) {
1361
+ console.log('[ErrorHandling] Circuit breaker OPEN, attempting backup instead');
1362
+ await backupFailedData(data, context);
1363
+ return { success: false, circuitBreakerOpen: true };
1364
+ } else {
1365
+ // Try half-open
1366
+ errorHandlingState.circuitBreakerState = 'HALF_OPEN';
1367
+ console.log('[ErrorHandling] Circuit breaker moving to HALF_OPEN state');
1368
+ }
1369
+ }
1370
+
1371
+ // Attempt the save operation
1372
+ const result = await saveFunction(data, context);
1373
+
1374
+ // Success - reset circuit breaker
1375
+ if (result && result.success !== false) {
1376
+ errorHandlingState.circuitBreakerState = 'CLOSED';
1377
+ errorHandlingState.failedSaveCount = 0;
1378
+ console.log('[ErrorHandling] Save successful, circuit breaker CLOSED');
1379
+ return { success: true, result };
1380
+ } else {
1381
+ throw new Error('Save function returned failure');
1382
+ }
1383
+
1384
+ } catch (error) {
1385
+ return await handleSaveFailure(error, data, context, attempt);
1386
+ }
1387
+ }
1388
+
1389
+ /**
1390
+ * Handle save failure with retry logic and escalation
1391
+ */
1392
+ async function handleSaveFailure(error, data, context, attempt) {
1393
+ console.error('[ErrorHandling] Save attempt failed:', error);
1394
+
1395
+ // Update error state
1396
+ errorHandlingState.failedSaveCount++;
1397
+ errorHandlingState.lastError = {
1398
+ type: determineErrorType(error),
1399
+ message: error.message || 'Unknown error',
1400
+ timestamp: Date.now(),
1401
+ retryCount: errorHandlingState.failedSaveCount,
1402
+ stackTrace: error.stack,
1403
+ context: context
1404
+ };
1405
+
1406
+ // Store error state
1407
+ await storeErrorState(errorHandlingState.lastError);
1408
+
1409
+ // Manage circuit breaker
1410
+ if (errorHandlingState.failedSaveCount >= ERROR_HANDLING_CONFIG.failureThreshold) {
1411
+ errorHandlingState.circuitBreakerState = 'OPEN';
1412
+ console.warn('[ErrorHandling] Circuit breaker OPEN due to repeated failures');
1413
+ }
1414
+
1415
+ // Attempt backup
1416
+ try {
1417
+ await backupFailedData(data, context);
1418
+ console.log('[ErrorHandling] Data backed up successfully');
1419
+ } catch (backupError) {
1420
+ console.error('[ErrorHandling] CRITICAL: Backup also failed:', backupError);
1421
+ await createUserNotification('CRITICAL', 'Data loss imminent - manual intervention required');
1422
+ }
1423
+
1424
+ // Create user notification if throttling allows
1425
+ if (shouldCreateNotification()) {
1426
+ await createUserNotification('ERROR', `Save failed: ${error.message}`);
1427
+ }
1428
+
1429
+ return {
1430
+ success: false,
1431
+ error: error.message,
1432
+ backedUp: true,
1433
+ circuitBreakerState: errorHandlingState.circuitBreakerState
1434
+ };
1435
+ }
1436
+
1437
+ /**
1438
+ * Determine error type for categorization and handling
1439
+ */
1440
+ function determineErrorType(error) {
1441
+ const message = error.message?.toLowerCase() || '';
1442
+
1443
+ if (message.includes('network') || message.includes('fetch') || message.includes('timeout')) {
1444
+ return 'NETWORK_FAILURE';
1445
+ } else if (message.includes('server') || message.includes('http')) {
1446
+ return 'SERVER_ERROR';
1447
+ } else if (message.includes('storage') || message.includes('indexeddb')) {
1448
+ return 'STORAGE_FAILURE';
1449
+ } else if (message.includes('browser') || message.includes('extension')) {
1450
+ return 'BROWSER_CRASH';
1451
+ } else {
1452
+ return 'NOTIFICATION_FAILURE';
1453
+ }
1454
+ }
1455
+
1456
+ /**
1457
+ * Store error state in IndexedDB for analysis
1458
+ */
1459
+ async function storeErrorState(errorDetails) {
1460
+ try {
1461
+ const request = indexedDB.open('ChromePilotErrorRecovery', 1);
1462
+ request.onsuccess = () => {
1463
+ const db = request.result;
1464
+ const transaction = db.transaction(['errorStates'], 'readwrite');
1465
+ const store = transaction.objectStore('errorStates');
1466
+
1467
+ store.add({
1468
+ ...errorDetails,
1469
+ sessionId: errorHandlingState.sessionId,
1470
+ errorType: errorDetails.type
1471
+ });
1472
+ };
1473
+ } catch (error) {
1474
+ console.error('[ErrorHandling] Failed to store error state:', error);
1475
+ }
1476
+ }
1477
+
1478
+ /**
1479
+ * Check if user notification should be created (throttling)
1480
+ */
1481
+ function shouldCreateNotification() {
1482
+ const now = Date.now();
1483
+ const timeSinceLastNotification = now - errorHandlingState.lastNotificationTime;
1484
+
1485
+ if (timeSinceLastNotification >= ERROR_HANDLING_CONFIG.notificationThrottleMs) {
1486
+ errorHandlingState.lastNotificationTime = now;
1487
+ return true;
1488
+ }
1489
+
1490
+ return false;
1491
+ }
1492
+
1493
+ /**
1494
+ * Backup failed data to IndexedDB
1495
+ */
1496
+ async function backupFailedData(data, context) {
1497
+ try {
1498
+ const backupData = {
1499
+ sessionId: errorHandlingState.sessionId,
1500
+ timestamp: Date.now(),
1501
+ data: data,
1502
+ context: context,
1503
+ retryCount: errorHandlingState.failedSaveCount,
1504
+ backupType: 'AUTOMATIC'
1505
+ };
1506
+
1507
+ const request = indexedDB.open('ChromePilotErrorRecovery', 1);
1508
+ request.onsuccess = () => {
1509
+ const db = request.result;
1510
+ const transaction = db.transaction(['sessionBackups'], 'readwrite');
1511
+ const store = transaction.objectStore('sessionBackups');
1512
+
1513
+ store.add(backupData);
1514
+ console.log('[ErrorHandling] Data backed up with session ID:', errorHandlingState.sessionId);
1515
+ };
1516
+
1517
+ // Also add to retained interactions if relevant
1518
+ if (data.interactions) {
1519
+ errorHandlingState.retainedInteractions = [
1520
+ ...errorHandlingState.retainedInteractions,
1521
+ ...data.interactions
1522
+ ].slice(-ERROR_HANDLING_CONFIG.maxRetainedInteractions); // Keep only recent ones
1523
+ }
1524
+
1525
+ } catch (error) {
1526
+ console.error('[ErrorHandling] Failed to backup data:', error);
1527
+ throw error;
1528
+ }
1529
+ }
1530
+
1531
+ /**
1532
+ * Create user notification with action buttons
1533
+ */
1534
+ async function createUserNotification(priority, message) {
1535
+ try {
1536
+ const notificationId = `chromepilot_error_${Date.now()}`;
1537
+
1538
+ const notificationOptions = {
1539
+ type: 'basic',
1540
+ iconUrl: 'icons/icon48.png',
1541
+ title: 'Chrome Debug Error',
1542
+ message: message,
1543
+ priority: priority === 'CRITICAL' ? 2 : 1,
1544
+ buttons: [
1545
+ { title: 'Retry Save' },
1546
+ { title: 'View Details' }
1547
+ ]
1548
+ };
1549
+
1550
+ if (priority === 'CRITICAL') {
1551
+ notificationOptions.buttons.push({ title: 'Download Backup' });
1552
+ }
1553
+
1554
+ await chrome.notifications.create(notificationId, notificationOptions);
1555
+ console.log('[ErrorHandling] User notification created:', notificationId);
1556
+
1557
+ // Set up notification click handlers
1558
+ chrome.notifications.onButtonClicked.addListener((notifId, buttonIndex) => {
1559
+ if (notifId === notificationId) {
1560
+ handleNotificationAction(buttonIndex, priority);
1561
+ }
1562
+ });
1563
+
1564
+ } catch (error) {
1565
+ console.error('[ErrorHandling] Failed to create notification:', error);
1566
+ }
1567
+ }
1568
+
1569
+ /**
1570
+ * Handle user notification button clicks
1571
+ */
1572
+ async function handleNotificationAction(buttonIndex, priority) {
1573
+ switch (buttonIndex) {
1574
+ case 0: // Retry Save
1575
+ console.log('[ErrorHandling] User requested retry');
1576
+ await attemptRecovery();
1577
+ break;
1578
+ case 1: // View Details
1579
+ console.log('[ErrorHandling] User requested error details');
1580
+ // Could open an options page or log details
1581
+ break;
1582
+ case 2: // Download Backup (for critical errors)
1583
+ if (priority === 'CRITICAL') {
1584
+ console.log('[ErrorHandling] User requested backup download');
1585
+ await downloadBackupData();
1586
+ }
1587
+ break;
1588
+ }
1589
+ }
1590
+
1591
+ /**
1592
+ * Attempt to recover and upload backed up data
1593
+ */
1594
+ async function attemptRecovery() {
1595
+ try {
1596
+ const request = indexedDB.open('ChromePilotErrorRecovery', 1);
1597
+ request.onsuccess = () => {
1598
+ const db = request.result;
1599
+ const transaction = db.transaction(['sessionBackups'], 'readonly');
1600
+ const store = transaction.objectStore('sessionBackups');
1601
+
1602
+ store.getAll().onsuccess = (event) => {
1603
+ const backups = event.target.result;
1604
+ console.log('[ErrorHandling] Found', backups.length, 'backup records for recovery');
1605
+
1606
+ // Process each backup
1607
+ backups.forEach(async (backup) => {
1608
+ try {
1609
+ // Attempt to save the backed up data
1610
+ // This would call the original save function with the backed up data
1611
+ console.log('[ErrorHandling] Attempting recovery for session:', backup.sessionId);
1612
+ } catch (error) {
1613
+ console.error('[ErrorHandling] Recovery failed for session:', backup.sessionId, error);
1614
+ }
1615
+ });
1616
+ };
1617
+ };
1618
+ } catch (error) {
1619
+ console.error('[ErrorHandling] Recovery attempt failed:', error);
1620
+ }
1621
+ }
1622
+
1623
+ /**
1624
+ * Download backup data as JSON file for manual recovery
1625
+ */
1626
+ async function downloadBackupData() {
1627
+ try {
1628
+ const request = indexedDB.open('ChromePilotErrorRecovery', 1);
1629
+ request.onsuccess = () => {
1630
+ const db = request.result;
1631
+ const transaction = db.transaction(['sessionBackups', 'errorStates'], 'readonly');
1632
+
1633
+ const backupStore = transaction.objectStore('sessionBackups');
1634
+ const errorStore = transaction.objectStore('errorStates');
1635
+
1636
+ const backupData = {};
1637
+
1638
+ backupStore.getAll().onsuccess = (event) => {
1639
+ backupData.sessionBackups = event.target.result;
1640
+
1641
+ errorStore.getAll().onsuccess = (event) => {
1642
+ backupData.errorStates = event.target.result;
1643
+ backupData.exportDate = new Date().toISOString();
1644
+
1645
+ // Create download
1646
+ const blob = new Blob([JSON.stringify(backupData, null, 2)], { type: 'application/json' });
1647
+ const url = URL.createObjectURL(blob);
1648
+
1649
+ chrome.downloads.download({
1650
+ url: url,
1651
+ filename: `chrome-pilot-backup-${Date.now()}.json`
1652
+ });
1653
+
1654
+ console.log('[ErrorHandling] Backup data download initiated');
1655
+ };
1656
+ };
1657
+ };
1658
+ } catch (error) {
1659
+ console.error('[ErrorHandling] Failed to download backup data:', error);
1660
+ }
1661
+ }
1662
+
1663
+ // Create offscreen document if it doesn't exist
1664
+ async function createOffscreenDocument() {
1665
+ try {
1666
+ // Always check if document exists first
1667
+ const hasDocument = await chrome.offscreen.hasDocument();
1668
+ if (hasDocument) {
1669
+ hasOffscreenDocument = true;
1670
+ console.log('Offscreen document already exists');
1671
+ return;
1672
+ }
1673
+
1674
+ // Only create if no document exists
1675
+ await chrome.offscreen.createDocument({
1676
+ url: 'offscreen.html',
1677
+ reasons: ['USER_MEDIA'],
1678
+ justification: 'Recording tab frames for Chrome Debug'
1679
+ });
1680
+ hasOffscreenDocument = true;
1681
+ console.log('Created new offscreen document');
1682
+ } catch (error) {
1683
+ if (error.message.includes('Only a single offscreen document may be created')) {
1684
+ // Document already exists, just mark it as available
1685
+ hasOffscreenDocument = true;
1686
+ console.log('Offscreen document already exists (caught error)');
1687
+ } else {
1688
+ throw error;
1689
+ }
1690
+ }
1691
+ }
1692
+
1693
+ // Close offscreen document
1694
+ async function closeOffscreenDocument() {
1695
+ if (!hasOffscreenDocument) return;
1696
+
1697
+ if (await chrome.offscreen.hasDocument()) {
1698
+ await chrome.offscreen.closeDocument();
1699
+ }
1700
+ hasOffscreenDocument = false;
1701
+ }
1702
+
1703
+ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
1704
+ console.log('Received message:', request.action || request.type || 'unknown');
1705
+
1706
+ if (request.action === 'sendToServer') {
1707
+ // Primary port is 3028 - try it first, then fallback to others if needed
1708
+ const ports = CONFIG_PORTS; // Use configured ports
1709
+
1710
+ async function tryPorts() {
1711
+ let lastError = null;
1712
+ console.log('Trying to connect to server...');
1713
+
1714
+ for (const port of ports) {
1715
+ try {
1716
+ console.log(`Trying port ${port}...`);
1717
+ const controller = new AbortController();
1718
+ const timeoutId = setTimeout(() => controller.abort(), DISCOVERY_TIMEOUT);
1719
+
1720
+ const response = await fetch(`http://localhost:${port}/chromedebug/dom-intent`, {
1721
+ method: 'POST',
1722
+ headers: {
1723
+ 'Content-Type': 'application/json',
1724
+ },
1725
+ body: JSON.stringify(request.data),
1726
+ signal: controller.signal
1727
+ });
1728
+
1729
+ clearTimeout(timeoutId);
1730
+
1731
+ if (response.ok) {
1732
+ const result = await response.json();
1733
+ console.log(`Success on port ${port}:`, result);
1734
+ sendResponse({ success: true, result, port });
1735
+ return;
1736
+ } else {
1737
+ // Try to get error message from response
1738
+ try {
1739
+ const errorData = await response.json();
1740
+ if (errorData.message || errorData.error) {
1741
+ console.log(`Port ${port} error:`, errorData);
1742
+ sendResponse({
1743
+ success: false,
1744
+ result: errorData,
1745
+ error: errorData.message || errorData.error
1746
+ });
1747
+ return;
1748
+ }
1749
+ } catch (e) {
1750
+ // Response wasn't JSON
1751
+ }
1752
+ lastError = `Port ${port}: HTTP ${response.status}`;
1753
+ console.log(lastError);
1754
+ }
1755
+ } catch (e) {
1756
+ lastError = `Port ${port}: ${e.message}`;
1757
+ console.log(lastError);
1758
+ }
1759
+ }
1760
+
1761
+ console.error('Failed to connect to any port');
1762
+ sendResponse({
1763
+ success: false,
1764
+ error: 'Could not connect to Chrome Debug server on any port',
1765
+ lastError
1766
+ });
1767
+ }
1768
+
1769
+ tryPorts().catch(err => {
1770
+ console.error('Error in tryPorts:', err);
1771
+ sendResponse({
1772
+ success: false,
1773
+ error: err.message
1774
+ });
1775
+ });
1776
+
1777
+ return true; // Keep message channel open for async response
1778
+ }
1779
+
1780
+ if (request.action === 'startRecording') {
1781
+ startRecording(request.tabId, request.settings).then(() => {
1782
+ sendResponse({ success: true });
1783
+ }).catch((error) => {
1784
+ console.error('Error starting recording:', error);
1785
+ sendResponse({ success: false, error: error.message });
1786
+ });
1787
+ return true;
1788
+ }
1789
+
1790
+ if (request.action === 'stopRecording') {
1791
+ stopRecording().then(() => {
1792
+ sendResponse({ success: true });
1793
+ }).catch((error) => {
1794
+ console.error('Error stopping recording:', error);
1795
+ sendResponse({ success: false, error: error.message });
1796
+ });
1797
+ return true;
1798
+ }
1799
+
1800
+ // Session manager lease renewal handler
1801
+ if (request.action === 'renewLease') {
1802
+ console.log('[Background] RENEW LEASE - Session:', request.sessionId, 'Manager exists:', !!sessionManager);
1803
+
1804
+ if (!sessionManager) {
1805
+ console.error('[Background] Session manager not available');
1806
+ sendResponse({ success: false, error: 'Session manager not available' });
1807
+ return true;
1808
+ }
1809
+
1810
+ const leaseRequest = {
1811
+ sessionId: request.sessionId,
1812
+ ownerId: request.ownerId,
1813
+ requestTime: Date.now(),
1814
+ renewalDuration: request.renewalDuration
1815
+ };
1816
+
1817
+ sessionManager.renewLease(leaseRequest).then((result) => {
1818
+ console.log('[Background] Lease renewed - Success:', result.success);
1819
+ sendResponse(result);
1820
+ }).catch((error) => {
1821
+ console.error('[Background] Error renewing lease:', error);
1822
+ sendResponse({
1823
+ success: false,
1824
+ sessionId: request.sessionId,
1825
+ error: error.message
1826
+ });
1827
+ });
1828
+ return true;
1829
+ }
1830
+
1831
+ // Session manager status check handler
1832
+ if (request.action === 'getSessionStatus') {
1833
+ if (!sessionManager) {
1834
+ sendResponse({ success: false, error: 'Session manager not available' });
1835
+ return true;
1836
+ }
1837
+
1838
+ sessionManager.getCurrentState().then((state) => {
1839
+ sendResponse({
1840
+ success: true,
1841
+ state: state,
1842
+ isRecording: state?.isRecording || false,
1843
+ sessionId: state?.sessionId || null
1844
+ });
1845
+ }).catch((error) => {
1846
+ console.error('Error getting session status:', error);
1847
+ sendResponse({ success: false, error: error.message });
1848
+ });
1849
+ return true;
1850
+ }
1851
+
1852
+ // Force stop session handler (emergency)
1853
+ if (request.action === 'forceStopSession') {
1854
+ if (!sessionManager) {
1855
+ sendResponse({ success: false, error: 'Session manager not available' });
1856
+ return true;
1857
+ }
1858
+
1859
+ sessionManager.forceStopAll(request.reason || 'MANUAL_FORCE_STOP').then((result) => {
1860
+ // Clear local session cache
1861
+ currentSession = null;
1862
+ currentOwnerId = null;
1863
+ isCurrentlyRecording = false;
1864
+ recordingTabId = null;
1865
+ currentRecordingSessionId = null;
1866
+
1867
+ sendResponse(result);
1868
+ }).catch((error) => {
1869
+ console.error('Error force stopping session:', error);
1870
+ sendResponse({ success: false, error: error.message });
1871
+ });
1872
+ return true;
1873
+ }
1874
+
1875
+ if (request.action === 'takeManualSnapshot') {
1876
+ // Check recording status via session manager
1877
+ isCurrentlyRecordingAsync().then(async (recording) => {
1878
+ if (recording && offscreenDocumentId) {
1879
+ // Send message to the offscreen document to trigger a manual snapshot
1880
+ chrome.runtime.sendMessage({
1881
+ type: 'manual-snapshot',
1882
+ target: 'offscreen'
1883
+ }).then(() => {
1884
+ sendResponse({ success: true });
1885
+ }).catch((error) => {
1886
+ console.error('Error triggering manual snapshot:', error);
1887
+ sendResponse({ success: false, error: error.message });
1888
+ });
1889
+ } else {
1890
+ sendResponse({ success: false, error: 'Not currently recording' });
1891
+ }
1892
+ }).catch((error) => {
1893
+ console.error('Error checking recording status:', error);
1894
+ sendResponse({ success: false, error: 'Failed to check recording status' });
1895
+ });
1896
+ return true;
1897
+ }
1898
+
1899
+ /*
1900
+ * SNAPSHOT FEATURE DISABLED (2025-10-01)
1901
+ * Standalone snapshot message handler disabled - see SNAPSHOT_FEATURE_DISABLED.md
1902
+ */
1903
+ /*
1904
+ if (request.action === 'takeStandaloneSnapshot') {
1905
+ console.log('Taking standalone snapshot for tab:', request.tabId);
1906
+ takeStandaloneSnapshot(request.tabId, request.note).then((result) => {
1907
+ sendResponse({ success: true, sessionId: result.sessionId });
1908
+ }).catch((error) => {
1909
+ console.error('Error taking standalone snapshot:', error);
1910
+ sendResponse({ success: false, error: error.message });
1911
+ });
1912
+ return true;
1913
+ }
1914
+ */
1915
+
1916
+ if (request.action === 'getRecordingState') {
1917
+ // Use session manager if available
1918
+ if (sessionManager) {
1919
+ sessionManager.getCurrentState().then(async (state) => {
1920
+ const recording = state?.isRecording || false;
1921
+ const sessionId = state?.sessionId || null;
1922
+ const tabId = state?.settings?.tabId || null;
1923
+
1924
+ sendResponse({
1925
+ isRecording: recording,
1926
+ recordingTabId: tabId,
1927
+ sessionId: sessionId,
1928
+ frameCount: state?.frameCount || 0,
1929
+ sessionStatus: state?.status || 'none'
1930
+ });
1931
+ }).catch((error) => {
1932
+ console.error('Error getting session state:', error);
1933
+ // Fallback to legacy state
1934
+ sendResponse({
1935
+ isRecording: isCurrentlyRecording,
1936
+ recordingTabId: recordingTabId,
1937
+ sessionId: currentRecordingSessionId,
1938
+ error: error.message
1939
+ });
1940
+ });
1941
+ } else {
1942
+ // Legacy fallback
1943
+ sendResponse({
1944
+ isRecording: isCurrentlyRecording,
1945
+ recordingTabId: recordingTabId,
1946
+ sessionId: currentRecordingSessionId
1947
+ });
1948
+ }
1949
+ return true;
1950
+ }
1951
+
1952
+ if (request.action === 'startRecordingFromContent') {
1953
+ console.log('Starting recording from content script');
1954
+ const tabId = sender.tab.id;
1955
+
1956
+ // Store recording state and show notification
1957
+ chrome.notifications.create({
1958
+ type: 'basic',
1959
+ iconUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
1960
+ title: 'Start Recording',
1961
+ message: 'Click the Chrome Debug extension icon to start recording this tab',
1962
+ priority: 2,
1963
+ requireInteraction: true
1964
+ });
1965
+
1966
+ // Update storage to signal popup to start recording
1967
+ chrome.storage.local.set({
1968
+ pendingRecording: true,
1969
+ pendingTabId: tabId
1970
+ });
1971
+
1972
+ sendResponse({ success: true, message: 'Click extension icon to start recording' });
1973
+ return true;
1974
+ }
1975
+
1976
+ if (request.action === 'deleteRecording') {
1977
+ console.log('Deleting recording:', request.recordingId);
1978
+ deleteRecordingFromServer(request.recordingId, sendResponse);
1979
+ return true;
1980
+ }
1981
+
1982
+ // License check handler for workflow recording
1983
+ if (request.action === 'checkLicenseForWorkflow') {
1984
+ console.log('[License] popup.js requesting license check for workflow recording');
1985
+ LicenseHelper.checkLicenseBeforeRecording().then((licenseCheck) => {
1986
+ console.log('[License] License check result for popup:', licenseCheck);
1987
+ sendResponse(licenseCheck);
1988
+ }).catch((error) => {
1989
+ console.error('[License] Error checking license:', error);
1990
+ // Fail-open: allow recording on error
1991
+ sendResponse({ allowed: true, error: error.message });
1992
+ });
1993
+ return true;
1994
+ }
1995
+
1996
+ // Workflow recording handlers
1997
+ if (request.action === 'startWorkflowRecording') {
1998
+ console.log('Starting workflow recording for tab:', request.tabId);
1999
+ startWorkflowRecording(
2000
+ request.tabId,
2001
+ request.includeLogsInExport,
2002
+ request.sessionName,
2003
+ request.screenshotSettings
2004
+ ).then(() => {
2005
+ sendResponse({ success: true });
2006
+ }).catch((error) => {
2007
+ console.error('Error starting workflow recording:', error);
2008
+ sendResponse({ success: false, error: error.message });
2009
+ });
2010
+ return true;
2011
+ }
2012
+
2013
+ if (request.action === 'stopWorkflowRecording') {
2014
+ console.log('Stopping workflow recording for tab:', request.tabId);
2015
+ stopWorkflowRecording(request.tabId).then((workflow) => {
2016
+ sendResponse({ success: true, workflow: workflow });
2017
+ }).catch((error) => {
2018
+ console.error('Error stopping workflow recording:', error);
2019
+ sendResponse({ success: false, error: error.message });
2020
+ });
2021
+ return true;
2022
+ }
2023
+
2024
+ // Handle messages from offscreen document
2025
+ if (request.target === 'background') {
2026
+ if (request.type === 'frame-batch-ready') {
2027
+ handleFrameBatch(request.data);
2028
+ } else if (request.type === 'frame-capture-complete') {
2029
+ handleFrameCaptureComplete(request.data);
2030
+ } else if (request.type === 'show-frame-flash') {
2031
+ // Relay flash message to content script
2032
+ chrome.tabs.sendMessage(request.tabId, {
2033
+ action: 'showFrameFlash'
2034
+ }).catch(() => {
2035
+ // Content script might not be available, that's ok
2036
+ });
2037
+ }
2038
+ }
2039
+
2040
+ // Handle console logs with race-condition-free buffering
2041
+ if (request.action === 'consoleLog' && sender.tab) {
2042
+ const tabId = sender.tab.id;
2043
+ if (tabId === recordingTabId && currentRecordingSessionId) {
2044
+ // Use LogBuffer for race-condition-free storage
2045
+ // This replaces the problematic concurrent chrome.storage.session operations
2046
+ if (logBuffer) {
2047
+ logBuffer.addLog(tabId, request.log).catch(bufferErr => {
2048
+ console.error('[Background] Failed to buffer log via LogBuffer:', bufferErr);
2049
+
2050
+ // Fallback to direct storage (original behavior) if LogBuffer fails
2051
+ chrome.storage.session.get(String(tabId)).then(result => {
2052
+ const logs = result[String(tabId)] || [];
2053
+ logs.push(request.log);
2054
+ chrome.storage.session.set({ [String(tabId)]: logs });
2055
+ }).catch(fallbackErr => {
2056
+ console.error('[Background] Fallback storage also failed:', fallbackErr);
2057
+ });
2058
+ });
2059
+ } else {
2060
+ // LogBuffer not available, use fallback storage
2061
+ console.warn('[Background] LogBuffer not available, using fallback storage');
2062
+ chrome.storage.session.get(String(tabId)).then(result => {
2063
+ const logs = result[String(tabId)] || [];
2064
+ logs.push(request.log);
2065
+ chrome.storage.session.set({ [String(tabId)]: logs });
2066
+ }).catch(fallbackErr => {
2067
+ console.error('[Background] Fallback storage failed:', fallbackErr);
2068
+ });
2069
+ }
2070
+
2071
+ // Also attempt real-time streaming if LogStreamer is available
2072
+ if (logStreamer) {
2073
+ logStreamer.addLog(request.log, currentRecordingSessionId).catch(err => {
2074
+ console.error('[Background] Error streaming log:', err);
2075
+ });
2076
+ }
2077
+ }
2078
+ }
2079
+
2080
+ // Capture screen interactions during recording
2081
+ if (request.action === 'screenInteraction' && sender.tab) {
2082
+ const tabId = sender.tab.id;
2083
+
2084
+ // Use session manager validation if available
2085
+ if (sessionManager) {
2086
+ isCurrentlyRecordingAsync().then(async (recording) => {
2087
+ const currentTabId = await getCurrentTabIdAsync();
2088
+ if (recording && tabId === currentTabId) {
2089
+ const sessionId = await getCurrentSessionIdAsync();
2090
+ let frameIndex = 0;
2091
+
2092
+ // Get frame count from session manager
2093
+ const state = await sessionManager.getCurrentState();
2094
+ if (state && state.sessionId === sessionId) {
2095
+ frameIndex = state.frameCount || 0;
2096
+ }
2097
+
2098
+ const interaction = {
2099
+ ...request.interaction,
2100
+ frameIndex: frameIndex
2101
+ };
2102
+ screenInteractions.push(interaction);
2103
+ console.log('Screen interaction recorded:', interaction.type, 'Total:', screenInteractions.length);
2104
+ }
2105
+ }).catch(error => {
2106
+ console.error('Error validating recording state for screen interaction:', error);
2107
+ });
2108
+ } else {
2109
+ // Legacy fallback
2110
+ if (tabId === recordingTabId && isCurrentlyRecording) {
2111
+ const interaction = {
2112
+ ...request.interaction,
2113
+ frameIndex: frameCounter.get(currentRecordingSessionId) || 0
2114
+ };
2115
+ screenInteractions.push(interaction);
2116
+ console.log('Screen interaction recorded:', interaction.type, 'Total:', screenInteractions.length);
2117
+ }
2118
+ }
2119
+ }
2120
+
2121
+ // Buffer workflow console logs
2122
+ if (request.action === 'workflowConsoleLog' && sender.tab) {
2123
+ const tabId = sender.tab.id;
2124
+ if (workflowRecordingTabs.has(tabId) && workflowIncludeLogs.get(tabId)) {
2125
+ // Store logs in session storage for workflow recording
2126
+ chrome.storage.session.get(`workflow_${tabId}`).then(result => {
2127
+ const logs = result[`workflow_${tabId}`] || [];
2128
+ logs.push(request.log);
2129
+ chrome.storage.session.set({ [`workflow_${tabId}`]: logs });
2130
+ }).catch(err => {
2131
+ console.error('Error buffering workflow log:', err);
2132
+ });
2133
+ }
2134
+ }
2135
+
2136
+ // Handle getCookies for restore points
2137
+ if (request.action === 'getCookies' && sender.tab) {
2138
+ chrome.cookies.getAll({ url: request.url }, (cookies) => {
2139
+ sendResponse({ cookies: cookies });
2140
+ });
2141
+ return true;
2142
+ }
2143
+
2144
+ // Handle save restore point
2145
+ if (request.action === 'saveRestorePoint' && sender.tab) {
2146
+ // Get current workflow ID from storage
2147
+ chrome.storage.local.get(['currentWorkflowId'], async (result) => {
2148
+ if (!result.currentWorkflowId) {
2149
+ sendResponse({ success: false, error: 'No active workflow recording' });
2150
+ return;
2151
+ }
2152
+
2153
+ // Add workflow ID to restore point data
2154
+ const restorePointData = {
2155
+ ...request.restorePoint,
2156
+ workflowId: result.currentWorkflowId
2157
+ };
2158
+
2159
+ // Try to save to server
2160
+ const ports = CONFIG_PORTS;
2161
+ let saved = false;
2162
+ let restorePointId = null;
2163
+
2164
+ // Use comprehensive error handling for restore point save
2165
+ const saveRestorePoint = async (data, context) => {
2166
+ for (const port of ports) {
2167
+ try {
2168
+ const response = await fetch(`http://localhost:${port}/chromedebug/restore-point`, {
2169
+ method: 'POST',
2170
+ headers: {
2171
+ 'Content-Type': 'application/json'
2172
+ },
2173
+ body: JSON.stringify(data.restorePointData)
2174
+ });
2175
+
2176
+ if (response.ok) {
2177
+ const result = await response.json();
2178
+ return { success: true, result };
2179
+ } else {
2180
+ throw new Error(`HTTP ${response.status}: Failed to save restore point to port ${port}`);
2181
+ }
2182
+ } catch (error) {
2183
+ console.error(`[ErrorHandling] Restore point save failed on port ${port}:`, error);
2184
+ // Continue to next port instead of silent failure
2185
+ }
2186
+ }
2187
+ throw new Error('All server ports failed for restore point save');
2188
+ };
2189
+
2190
+ const saveResult = await handleSaveAttempt(
2191
+ saveRestorePoint,
2192
+ { restorePointData },
2193
+ { operation: 'restore_point_save', tabId }
2194
+ );
2195
+
2196
+ if (saveResult.success) {
2197
+ saved = true;
2198
+ restorePointId = saveResult.result.result.restorePointId;
2199
+ } else {
2200
+ console.error('[ErrorHandling] Failed to save restore point after comprehensive error handling');
2201
+ }
2202
+
2203
+ if (saved) {
2204
+ sendResponse({ success: true, restorePointId });
2205
+ } else {
2206
+ sendResponse({ success: false, error: 'Could not save restore point to server' });
2207
+ }
2208
+ });
2209
+ return true;
2210
+ }
2211
+
2212
+ // Handle navigate and restore
2213
+ if (request.action === 'navigateAndRestore' && sender.tab) {
2214
+ const tabId = sender.tab.id;
2215
+ const { url, restorePointId } = request;
2216
+
2217
+ // Store restore point ID to apply after navigation
2218
+ chrome.storage.session.set({
2219
+ [`pendingRestore_${tabId}`]: { restorePointId, url }
2220
+ });
2221
+
2222
+ // Navigate to the URL
2223
+ chrome.tabs.update(tabId, { url: url }, () => {
2224
+ sendResponse({ success: true });
2225
+ });
2226
+ return true;
2227
+ }
2228
+
2229
+ // Handle workflow screenshot capture
2230
+ if (request.action === 'captureWorkflowScreenshot' && sender.tab) {
2231
+ const tabId = sender.tab.id;
2232
+ const settings = request.settings || {};
2233
+
2234
+ captureTabScreenshot(tabId, settings).then((screenshotData) => {
2235
+ sendResponse({ success: true, screenshotData });
2236
+ }).catch((error) => {
2237
+ console.error('Error capturing screenshot:', error);
2238
+ sendResponse({ success: false, error: error.message });
2239
+ });
2240
+ return true;
2241
+ }
2242
+ });
2243
+
2244
+ /*
2245
+ * SNAPSHOT FEATURE DISABLED (2025-10-01)
2246
+ *
2247
+ * This function captured a standalone snapshot (screenshot + console logs).
2248
+ *
2249
+ * WHY DISABLED:
2250
+ * - Snapshots without console logs are just screenshots (users can do this natively)
2251
+ * - Console log capture requires always-on monitoring (privacy concern)
2252
+ * - Core value proposition (screenshot + searchable logs) cannot be achieved cleanly
2253
+ *
2254
+ * TO RE-ENABLE:
2255
+ * 1. Implement privacy-conscious always-on log monitoring
2256
+ * 2. Uncomment this function
2257
+ * 3. Re-enable message handler in chrome.runtime.onMessage listener
2258
+ * 4. Re-enable UI elements in popup.html and popup.js
2259
+ *
2260
+ * See: SNAPSHOT_FEATURE_DISABLED.md for full explanation
2261
+ */
2262
+ /*
2263
+ async function takeStandaloneSnapshot(tabId, note = '') {
2264
+ try {
2265
+ console.log('Taking standalone snapshot for tab:', tabId);
2266
+
2267
+ // Create a unique session ID for the snapshot
2268
+ const sessionId = `snapshot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
2269
+
2270
+ // Capture screenshot
2271
+ const screenshotData = await chrome.tabs.captureVisibleTab(null, {
2272
+ format: 'jpeg',
2273
+ quality: 30 // Default quality for snapshots
2274
+ });
2275
+
2276
+ // Get current console logs for this tab
2277
+ let consoleLogs = [];
2278
+ try {
2279
+ // Try to get logs from active recording buffer first
2280
+ if (logBuffer) {
2281
+ await logBuffer.flushTab(tabId);
2282
+ const result = await chrome.storage.session.get(String(tabId));
2283
+ consoleLogs = result[String(tabId)] || [];
2284
+ // Take only the most recent 100 logs for the snapshot
2285
+ consoleLogs = consoleLogs.slice(-100);
2286
+ }
2287
+
2288
+ // If no logs from buffer, try to get from content script
2289
+ if (consoleLogs.length === 0) {
2290
+ try {
2291
+ const response = await chrome.tabs.sendMessage(tabId, {
2292
+ action: 'getConsoleHistory'
2293
+ });
2294
+ if (response && response.logs) {
2295
+ consoleLogs = response.logs.slice(-100);
2296
+ }
2297
+ } catch (contentScriptError) {
2298
+ console.warn('Content script not available for console logs:', contentScriptError);
2299
+ }
2300
+ }
2301
+ } catch (logError) {
2302
+ console.warn('Could not retrieve console logs for snapshot:', logError);
2303
+ }
2304
+
2305
+ // Prepare the snapshot data
2306
+ const snapshotData = {
2307
+ sessionId: sessionId,
2308
+ type: 'snapshot',
2309
+ note: note,
2310
+ timestamp: Date.now(),
2311
+ tabId: tabId,
2312
+ screenshot: screenshotData,
2313
+ logs: consoleLogs,
2314
+ url: null // Will be filled by server if possible
2315
+ };
2316
+
2317
+ // Send to server using the frame-batch endpoint with snapshot flag
2318
+ const ports = CONFIG_PORTS;
2319
+ let uploadSuccess = false;
2320
+
2321
+ for (const port of ports) {
2322
+ try {
2323
+ const response = await fetch(`http://localhost:${port}/chromedebug/frame-batch`, {
2324
+ method: 'POST',
2325
+ headers: {
2326
+ 'Content-Type': 'application/json'
2327
+ },
2328
+ body: JSON.stringify({
2329
+ sessionId: sessionId,
2330
+ frames: [{
2331
+ index: 0,
2332
+ timestamp: Date.now(),
2333
+ imageData: screenshotData,
2334
+ logs: consoleLogs
2335
+ }]
2336
+ })
2337
+ });
2338
+
2339
+ if (response.ok) {
2340
+ const result = await response.json();
2341
+ console.log('Snapshot uploaded successfully:', sessionId);
2342
+ uploadSuccess = true;
2343
+ break;
2344
+ }
2345
+ } catch (error) {
2346
+ console.error(`Failed to upload snapshot to port ${port}:`, error);
2347
+ }
2348
+ }
2349
+
2350
+ if (!uploadSuccess) {
2351
+ throw new Error('Failed to upload snapshot to any server port');
2352
+ }
2353
+
2354
+ return { sessionId: sessionId };
2355
+
2356
+ } catch (error) {
2357
+ console.error('Error taking standalone snapshot:', error);
2358
+ throw error;
2359
+ }
2360
+ }
2361
+ */
2362
+
2363
+ // Capture screenshot for workflow recording
2364
+ async function captureTabScreenshot(tabId, settings) {
2365
+ try {
2366
+ const dataUrl = await chrome.tabs.captureVisibleTab(null, {
2367
+ format: settings.format || 'jpeg',
2368
+ quality: settings.quality || 30
2369
+ });
2370
+
2371
+ // If resolution is specified, resize the image
2372
+ if (settings.maxWidth || settings.maxHeight) {
2373
+ // Create an image element to get dimensions
2374
+ const img = new Image();
2375
+ const canvas = new OffscreenCanvas(1, 1);
2376
+ const ctx = canvas.getContext('2d');
2377
+
2378
+ return new Promise((resolve, reject) => {
2379
+ img.onload = () => {
2380
+ let width = img.width;
2381
+ let height = img.height;
2382
+
2383
+ // Calculate new dimensions maintaining aspect ratio
2384
+ if (settings.maxWidth && width > settings.maxWidth) {
2385
+ height = (height * settings.maxWidth) / width;
2386
+ width = settings.maxWidth;
2387
+ }
2388
+ if (settings.maxHeight && height > settings.maxHeight) {
2389
+ width = (width * settings.maxHeight) / height;
2390
+ height = settings.maxHeight;
2391
+ }
2392
+
2393
+ canvas.width = width;
2394
+ canvas.height = height;
2395
+ ctx.drawImage(img, 0, 0, width, height);
2396
+
2397
+ canvas.convertToBlob({
2398
+ type: `image/${settings.format || 'jpeg'}`,
2399
+ quality: (settings.quality || 30) / 100
2400
+ }).then(blob => {
2401
+ const reader = new FileReader();
2402
+ reader.onloadend = () => resolve(reader.result);
2403
+ reader.onerror = reject;
2404
+ reader.readAsDataURL(blob);
2405
+ }).catch(reject);
2406
+ };
2407
+ img.onerror = reject;
2408
+ img.src = dataUrl;
2409
+ });
2410
+ }
2411
+
2412
+ return dataUrl;
2413
+ } catch (error) {
2414
+ console.error('Error capturing tab screenshot:', error);
2415
+ throw error;
2416
+ }
2417
+ }
2418
+
2419
+ // Start recording
2420
+ async function startRecording(tabId, settings = {}) {
2421
+ try {
2422
+ // STEP 1: Check license and usage limits BEFORE recording
2423
+ console.log('[License] Checking license before recording...');
2424
+ const licenseCheck = await LicenseHelper.checkLicenseBeforeRecording();
2425
+ console.log('[License] License check result:', licenseCheck);
2426
+
2427
+ if (!licenseCheck.allowed) {
2428
+ // Show notification to user
2429
+ chrome.notifications.create({
2430
+ type: 'basic',
2431
+ iconUrl: 'icon.png',
2432
+ title: 'Recording Limit Reached',
2433
+ message: licenseCheck.message || 'Upgrade to Pro for unlimited recordings.',
2434
+ priority: 2
2435
+ });
2436
+
2437
+ throw new Error(licenseCheck.message || 'Recording not allowed');
2438
+ }
2439
+
2440
+ // Store userId for later usage tracking
2441
+ const userId = licenseCheck.userId;
2442
+
2443
+ // Check if session manager is available
2444
+ if (!sessionManager) {
2445
+ throw new Error('Session manager not initialized');
2446
+ }
2447
+
2448
+ // Check if already recording
2449
+ const currentlyRecording = await isCurrentlyRecordingAsync();
2450
+ if (currentlyRecording) {
2451
+ throw new Error('Recording already in progress');
2452
+ }
2453
+
2454
+ // Clear any previous logs for this tab
2455
+ await chrome.storage.session.remove(String(tabId));
2456
+
2457
+ // Clear screen interactions from previous recording
2458
+ screenInteractions = [];
2459
+
2460
+ // Start capturing console logs FIRST, before any other setup
2461
+ const logsStarted = await startCapturingLogs(tabId);
2462
+ console.log('Console log capture started:', logsStarted);
2463
+
2464
+ // Give console interception time to settle (increased from 100ms to 500ms for reliability)
2465
+ if (logsStarted) {
2466
+ await new Promise(resolve => setTimeout(resolve, 500));
2467
+ console.log('[Recording] Console interception ready');
2468
+ } else {
2469
+ console.warn('[Recording] Console capture unavailable for this page - recording will proceed without console logs');
2470
+ }
2471
+
2472
+ // Create offscreen document
2473
+ await createOffscreenDocument();
2474
+
2475
+ // Get stream ID for the tab
2476
+ const streamId = await chrome.tabCapture.getMediaStreamId({
2477
+ targetTabId: tabId
2478
+ });
2479
+
2480
+ console.log('Got stream ID:', streamId);
2481
+
2482
+ // Create session with session manager
2483
+ const sessionConfig = {
2484
+ tabId: tabId,
2485
+ frameRate: settings.frameRate || 1,
2486
+ imageQuality: settings.imageQuality || 30,
2487
+ frameFlash: settings.frameFlash !== false,
2488
+ maxDuration: 30 * 60 * 1000, // 30 minutes
2489
+ includeConsole: true,
2490
+ ...settings
2491
+ };
2492
+
2493
+ const sessionResult = await sessionManager.createSession(sessionConfig);
2494
+ if (!sessionResult.success) {
2495
+ throw new Error(`Failed to create session: ${sessionResult.error?.message || 'Unknown error'}`);
2496
+ }
2497
+
2498
+ // Start recording with session manager
2499
+ const startResult = await sessionManager.startRecording(sessionResult.sessionId, sessionResult.ownerId);
2500
+ if (!startResult.success) {
2501
+ throw new Error(`Failed to start recording: ${startResult.error?.message || 'Unknown error'}`);
2502
+ }
2503
+
2504
+ // Cache session info for quick access
2505
+ currentSession = startResult.state;
2506
+ currentOwnerId = sessionResult.ownerId;
2507
+
2508
+ const sessionId = currentSession.sessionId;
2509
+ const scheduledStartTime = Date.now() + 3000; // Schedule start 3 seconds in future for countdown synchronization
2510
+
2511
+ // Update legacy state for backward compatibility (will be removed)
2512
+ recordingTabId = tabId;
2513
+ isCurrentlyRecording = true;
2514
+ recordingServerPort = null; // Reset port for new recording
2515
+ currentRecordingSessionId = sessionId;
2516
+ frameCounter.set(sessionId, 0); // Initialize frame counter for this session
2517
+
2518
+ // Set session ID in log streamer
2519
+ if (logStreamer) {
2520
+ logStreamer.setSessionId(sessionId);
2521
+ }
2522
+
2523
+ // Update storage
2524
+ chrome.storage.local.set({
2525
+ recordingActive: true,
2526
+ recordingStartTime: scheduledStartTime,
2527
+ recordingTabId: tabId,
2528
+ recordingSessionId: sessionId
2529
+ });
2530
+
2531
+ // Update badge
2532
+ chrome.action.setBadgeText({ text: 'REC' });
2533
+ chrome.action.setBadgeBackgroundColor({ color: '#FF0000' });
2534
+
2535
+ // If on a restricted page, notify user
2536
+ const tab = await chrome.tabs.get(tabId);
2537
+ if (tab.url && (tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://'))) {
2538
+ // Store a warning that console logs aren't available
2539
+ chrome.storage.local.set({
2540
+ [`recording_warning_${sessionId}`]: {
2541
+ type: 'restricted_page',
2542
+ message: 'Console logs cannot be captured on chrome:// or extension pages. Navigate to a regular website to capture logs.',
2543
+ url: tab.url
2544
+ }
2545
+ });
2546
+ }
2547
+
2548
+ // Notify content script that recording has started with scheduled time
2549
+ chrome.tabs.sendMessage(tabId, {
2550
+ action: 'recordingStarted',
2551
+ scheduledStartTime: scheduledStartTime,
2552
+ sessionId: sessionId
2553
+ }).catch(async (error) => {
2554
+ // Content script might not be injected, try to inject it
2555
+ console.log('Could not notify content script, attempting to inject:', error);
2556
+
2557
+ // First check if this tab allows content script injection
2558
+ const tab = await chrome.tabs.get(tabId);
2559
+ if (!tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://') || tab.url.startsWith('moz-extension://')) {
2560
+ console.log('Cannot inject content script into restricted URL:', tab.url);
2561
+ return;
2562
+ }
2563
+
2564
+ try {
2565
+ await chrome.scripting.executeScript({
2566
+ target: { tabId: tabId },
2567
+ files: ['content.js']
2568
+ });
2569
+ await chrome.scripting.insertCSS({
2570
+ target: { tabId: tabId },
2571
+ files: ['content.css']
2572
+ });
2573
+ // Try again after injection
2574
+ setTimeout(() => {
2575
+ chrome.tabs.sendMessage(tabId, {
2576
+ action: 'recordingStarted',
2577
+ scheduledStartTime: scheduledStartTime,
2578
+ sessionId: sessionId
2579
+ }).catch(() => {
2580
+ console.error('Failed to show recording indicator even after injection');
2581
+ });
2582
+ }, 100);
2583
+ } catch (injectionError) {
2584
+ console.error('Failed to inject content script:', injectionError);
2585
+ }
2586
+ });
2587
+
2588
+ // Send stream ID, session info, and settings to offscreen document for frame capture
2589
+ await chrome.runtime.sendMessage({
2590
+ type: 'start-frame-capture',
2591
+ target: 'offscreen',
2592
+ streamId: streamId,
2593
+ tabId: tabId,
2594
+ sessionId: sessionId,
2595
+ ownerId: currentOwnerId,
2596
+ scheduledStartTime: scheduledStartTime,
2597
+ settings: {
2598
+ frameRate: settings.frameRate || 1,
2599
+ imageQuality: settings.imageQuality || 30,
2600
+ frameFlash: settings.frameFlash !== false
2601
+ }
2602
+ });
2603
+
2604
+ console.log('Recording started');
2605
+ } catch (error) {
2606
+ console.error('Error in startRecording:', error);
2607
+ throw error;
2608
+ }
2609
+ }
2610
+
2611
+ // Stop recording
2612
+ async function stopRecording() {
2613
+ try {
2614
+ // Check if session manager is available
2615
+ if (!sessionManager) {
2616
+ console.warn('Session manager not available, stopping recording anyway');
2617
+ return;
2618
+ }
2619
+
2620
+ // Check if currently recording via session manager
2621
+ const currentlyRecording = await isCurrentlyRecordingAsync();
2622
+ if (!currentlyRecording) {
2623
+ console.log('No active recording session to stop');
2624
+ return;
2625
+ }
2626
+
2627
+ // Stop recording with session manager
2628
+ if (currentSession?.sessionId && currentOwnerId) {
2629
+ const stopResult = await sessionManager.stopRecording(currentSession.sessionId, currentOwnerId);
2630
+ if (!stopResult.success) {
2631
+ console.error('Failed to stop session:', stopResult.error?.message);
2632
+ // Continue with cleanup anyway
2633
+ } else {
2634
+ console.log(`Recording stopped. Duration: ${stopResult.sessionDuration}ms, Frames: ${stopResult.frameCount}`);
2635
+
2636
+ // STEP 2: Track usage AFTER successful recording completion
2637
+ console.log('[License] Tracking usage after recording completion...');
2638
+ const usageResult = await LicenseHelper.trackUsageAfterRecording();
2639
+ console.log('[License] Usage tracking result:', usageResult);
2640
+ }
2641
+ }
2642
+
2643
+ // CRITICAL: Flush all buffered logs before stopping recording
2644
+ // This ensures no logs are lost during the stop process
2645
+ let flushCompleted = false;
2646
+ if (logBuffer) {
2647
+ try {
2648
+ console.log('[Background] Flushing all buffered logs before stop...');
2649
+ await logBuffer.flushAll();
2650
+ flushCompleted = true;
2651
+ console.log('[Background] Successfully flushed all buffered logs');
2652
+ } catch (flushError) {
2653
+ console.error('[Background] Failed to flush logs during stop:', flushError);
2654
+ // Continue with stop process even if flush fails
2655
+ }
2656
+ }
2657
+
2658
+ // Store flush completion status for handleFrameCaptureComplete
2659
+ if (recordingTabId) {
2660
+ await chrome.storage.session.set({ [`flush_completed_${recordingTabId}`]: flushCompleted });
2661
+ }
2662
+
2663
+ // Send stop message to offscreen document
2664
+ await chrome.runtime.sendMessage({
2665
+ type: 'stop-frame-capture',
2666
+ target: 'offscreen'
2667
+ });
2668
+
2669
+ // CRITICAL FIX: Send stop-screen-capture-tracking message to ALL content scripts
2670
+ try {
2671
+ const tabs = await chrome.tabs.query({});
2672
+ const cleanupPromises = tabs.map(tab => {
2673
+ if (tab.id && tab.url && !tab.url.startsWith('chrome://') && !tab.url.startsWith('chrome-extension://')) {
2674
+ return chrome.tabs.sendMessage(tab.id, {
2675
+ type: 'stop-screen-capture-tracking'
2676
+ }).catch(() => {
2677
+ // Ignore errors for tabs that don't have content scripts
2678
+ console.log(`[Background] Could not send cleanup message to tab ${tab.id}`);
2679
+ });
2680
+ }
2681
+ return Promise.resolve();
2682
+ });
2683
+ await Promise.all(cleanupPromises);
2684
+ console.log('[Background] Cleanup messages sent to all content scripts');
2685
+ } catch (error) {
2686
+ console.error('[Background] Failed to send cleanup messages to content scripts:', error);
2687
+ }
2688
+
2689
+ // Clear session cache
2690
+ currentSession = null;
2691
+ currentOwnerId = null;
2692
+
2693
+ // Update legacy state for backward compatibility (will be removed)
2694
+ const previousRecordingTabId = recordingTabId; // Store for logBuffer cleanup
2695
+ isCurrentlyRecording = false;
2696
+ recordingTabId = null;
2697
+ currentRecordingSessionId = null;
2698
+
2699
+ // Clean up logBuffer for the specific tab that was being recorded
2700
+ if (logBuffer && previousRecordingTabId) {
2701
+ logBuffer.clearTab(previousRecordingTabId);
2702
+ }
2703
+
2704
+ console.log('Stop message sent to offscreen document');
2705
+ } catch (error) {
2706
+ console.error('Error in stopRecording:', error);
2707
+ // Clear session cache even on error
2708
+ const errorRecordingTabId = recordingTabId; // Store for cleanup
2709
+ currentSession = null;
2710
+ currentOwnerId = null;
2711
+ isCurrentlyRecording = false;
2712
+ recordingTabId = null;
2713
+ currentRecordingSessionId = null;
2714
+
2715
+ // Clean up logBuffer even on error
2716
+ if (logBuffer && errorRecordingTabId) {
2717
+ logBuffer.clearTab(errorRecordingTabId);
2718
+ }
2719
+
2720
+ throw error;
2721
+ }
2722
+ }
2723
+
2724
+
2725
+
2726
+ // Capture console logs from the recording tab
2727
+ async function startCapturingLogs(tabId) {
2728
+ // Check if this tab allows content script injection
2729
+ const tab = await chrome.tabs.get(tabId);
2730
+ if (!tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://') || tab.url.startsWith('moz-extension://')) {
2731
+ console.log('Cannot inject console logger into restricted URL:', tab.url);
2732
+ console.warn('WARNING: Console logs cannot be captured on restricted pages (chrome://, chrome-extension://, etc.)');
2733
+ console.warn('To capture console logs, please navigate to a regular web page before recording.');
2734
+ return false;
2735
+ }
2736
+
2737
+ // First inject the main world console interceptor
2738
+ try {
2739
+ const results = await chrome.scripting.executeScript({
2740
+ target: { tabId: tabId },
2741
+ world: 'MAIN',
2742
+ func: () => {
2743
+ // Check if we've already overridden console methods
2744
+ if (window.__chromePilotConsoleOverridden) {
2745
+ console.log('[Chrome Debug] Console already overridden, skipping');
2746
+ return 'already_installed';
2747
+ }
2748
+ window.__chromePilotConsoleOverridden = true;
2749
+ console.log('[Chrome Debug] Installing console interceptor');
2750
+
2751
+ // Override console methods to capture logs - complete coverage
2752
+ const originalLog = console.log;
2753
+ const originalError = console.error;
2754
+ const originalWarn = console.warn;
2755
+ const originalInfo = console.info;
2756
+ const originalDebug = console.debug;
2757
+ const originalTrace = console.trace;
2758
+ const originalTable = console.table;
2759
+ const originalDir = console.dir;
2760
+ const originalGroup = console.group;
2761
+ const originalGroupEnd = console.groupEnd;
2762
+ const originalTime = console.time;
2763
+ const originalTimeEnd = console.timeEnd;
2764
+ const originalCount = console.count;
2765
+
2766
+ const sendLog = (level, args) => {
2767
+ try {
2768
+ // Pre-serialize arguments to strings to avoid structured clone errors
2769
+ const serializedArgs = args.map(arg => {
2770
+ try {
2771
+ if (arg === null) return 'null';
2772
+ if (arg === undefined) return 'undefined';
2773
+ if (typeof arg === 'function') return '[Function: ' + (arg.name || 'anonymous') + ']';
2774
+ if (arg instanceof Element) return '[DOM Element: ' + arg.tagName + ']';
2775
+ if (typeof arg === 'object') {
2776
+ // Try to stringify, but limit depth to avoid circular references
2777
+ let stringified = JSON.stringify(arg, null, 2);
2778
+
2779
+ // Check if this looks like a base64 image and truncate it
2780
+ if (stringified.includes('data:image/') && stringified.length > 1000) {
2781
+ const match = stringified.match(/data:image\/([^;]+);base64,(.{0,100})/);
2782
+ if (match) {
2783
+ return `[Base64 Image: ${match[1]}, ${stringified.length} bytes total, truncated...]`;
2784
+ }
2785
+ }
2786
+
2787
+ // Truncate any extremely large strings
2788
+ const maxLength = 5000;
2789
+ if (stringified.length > maxLength) {
2790
+ return stringified.substring(0, maxLength) + `... [TRUNCATED: ${stringified.length} total bytes]`;
2791
+ }
2792
+
2793
+ return stringified;
2794
+ }
2795
+
2796
+ // Also check for base64 strings directly
2797
+ const strValue = String(arg);
2798
+ if (strValue.includes('data:image/') && strValue.length > 1000) {
2799
+ const match = strValue.match(/data:image\/([^;]+);base64,(.{0,100})/);
2800
+ if (match) {
2801
+ return `[Base64 Image: ${match[1]}, ${strValue.length} bytes total, truncated...]`;
2802
+ }
2803
+ }
2804
+
2805
+ // Truncate any extremely large strings
2806
+ if (strValue.length > 5000) {
2807
+ return strValue.substring(0, 5000) + `... [TRUNCATED: ${strValue.length} total bytes]`;
2808
+ }
2809
+
2810
+ return strValue;
2811
+ } catch (e) {
2812
+ return '[Object: could not serialize]';
2813
+ }
2814
+ });
2815
+
2816
+ // Post message to content script
2817
+ window.postMessage({
2818
+ type: 'chrome-pilot-console-log',
2819
+ log: {
2820
+ level,
2821
+ message: serializedArgs.join(' '),
2822
+ timestamp: Date.now()
2823
+ }
2824
+ }, '*');
2825
+ } catch (e) {
2826
+ // Ignore errors when sending logs
2827
+ }
2828
+ };
2829
+
2830
+ console.log = (...args) => {
2831
+ sendLog('log', args);
2832
+ originalLog.apply(console, args);
2833
+ };
2834
+
2835
+ console.error = (...args) => {
2836
+ sendLog('error', args);
2837
+ originalError.apply(console, args);
2838
+ };
2839
+
2840
+ console.warn = (...args) => {
2841
+ sendLog('warn', args);
2842
+ originalWarn.apply(console, args);
2843
+ };
2844
+
2845
+ console.info = (...args) => {
2846
+ sendLog('info', args);
2847
+ originalInfo.apply(console, args);
2848
+ };
2849
+
2850
+ console.debug = (...args) => {
2851
+ sendLog('debug', args);
2852
+ originalDebug.apply(console, args);
2853
+ };
2854
+
2855
+ console.trace = (...args) => {
2856
+ sendLog('trace', args);
2857
+ originalTrace.apply(console, args);
2858
+ };
2859
+
2860
+ console.table = (...args) => {
2861
+ sendLog('table', args);
2862
+ originalTable.apply(console, args);
2863
+ };
2864
+
2865
+ console.dir = (...args) => {
2866
+ sendLog('dir', args);
2867
+ originalDir.apply(console, args);
2868
+ };
2869
+
2870
+ console.group = (...args) => {
2871
+ sendLog('group', args);
2872
+ originalGroup.apply(console, args);
2873
+ };
2874
+
2875
+ console.groupEnd = (...args) => {
2876
+ sendLog('groupEnd', args);
2877
+ originalGroupEnd.apply(console, args);
2878
+ };
2879
+
2880
+ console.time = (...args) => {
2881
+ sendLog('time', args);
2882
+ originalTime.apply(console, args);
2883
+ };
2884
+
2885
+ console.timeEnd = (...args) => {
2886
+ sendLog('timeEnd', args);
2887
+ originalTimeEnd.apply(console, args);
2888
+ };
2889
+
2890
+ console.count = (...args) => {
2891
+ sendLog('count', args);
2892
+ originalCount.apply(console, args);
2893
+ };
2894
+
2895
+ return 'console_installed';
2896
+ }
2897
+ });
2898
+
2899
+ console.log('[Console Injection] MAIN world script injected successfully:', results);
2900
+
2901
+ // Then inject a content script to relay messages from main world to background
2902
+ await chrome.scripting.executeScript({
2903
+ target: { tabId: tabId },
2904
+ func: () => {
2905
+ // Listen for messages from main world
2906
+ window.addEventListener('message', (event) => {
2907
+ if (event.data && event.data.type === 'chrome-pilot-console-log') {
2908
+ // Forward to background script
2909
+ chrome.runtime.sendMessage({
2910
+ action: 'consoleLog',
2911
+ log: event.data.log
2912
+ });
2913
+ }
2914
+ });
2915
+ }
2916
+ });
2917
+
2918
+ console.log('[Console Injection] Content script relay injected successfully');
2919
+ return true; // Successfully started capturing logs
2920
+ } catch (error) {
2921
+ console.error('[Console Injection] Failed to inject console interceptor:', error);
2922
+ console.error('[Console Injection] Error details:', error.message, error.stack);
2923
+ return false; // Failed to inject console interceptor
2924
+ }
2925
+ }
2926
+
2927
+ // Workflow Recording Functions
2928
+ async function startWorkflowRecording(tabId, includeLogsInExport, sessionName = null, screenshotSettings = null) {
2929
+ try {
2930
+ console.log('Starting workflow recording for tab:', tabId);
2931
+
2932
+ // STEP 1: Check license and usage limits BEFORE recording (same pattern as startRecording)
2933
+ console.log('[License] Checking license before workflow recording...');
2934
+ const licenseCheck = await LicenseHelper.checkLicenseBeforeRecording();
2935
+ console.log('[License] License check result:', licenseCheck);
2936
+
2937
+ if (!licenseCheck.allowed) {
2938
+ // Show notification to user (same pattern as screen recording)
2939
+ chrome.notifications.create({
2940
+ type: 'basic',
2941
+ iconUrl: 'icon128.png',
2942
+ title: 'Recording Limit Reached',
2943
+ message: licenseCheck.message || 'Daily limit reached. Upgrade to Pro for unlimited workflow recordings.',
2944
+ buttons: [{ title: 'Upgrade to Pro' }],
2945
+ priority: 2
2946
+ });
2947
+
2948
+ throw new Error(licenseCheck.message || 'Workflow recording not allowed');
2949
+ }
2950
+
2951
+ // Store userId for later usage tracking
2952
+ const userId = licenseCheck.userId;
2953
+ workflowUserIds.set(tabId, userId);
2954
+
2955
+ // Check if this tab allows content script injection
2956
+ const tab = await chrome.tabs.get(tabId);
2957
+ if (!tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://') || tab.url.startsWith('moz-extension://')) {
2958
+ console.log('Cannot start workflow recording on restricted URL:', tab.url);
2959
+ throw new Error('Cannot record workflows on restricted pages (chrome:// URLs)');
2960
+ }
2961
+
2962
+ // Store settings
2963
+ workflowRecordingTabs.set(tabId, true);
2964
+ workflowIncludeLogs.set(tabId, includeLogsInExport);
2965
+ workflowScreenshotSettings.set(tabId, screenshotSettings);
2966
+ workflowSessionNames.set(tabId, sessionName);
2967
+
2968
+ // Generate and store workflow ID
2969
+ const workflowId = `workflow_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
2970
+ await chrome.storage.local.set({ currentWorkflowId: workflowId });
2971
+
2972
+ // Clear any existing logs for this tab
2973
+ await chrome.storage.session.set({ [`workflow_${tabId}`]: [] });
2974
+
2975
+ // Inject main world console interceptor (similar to frame recording)
2976
+ await chrome.scripting.executeScript({
2977
+ target: { tabId: tabId },
2978
+ world: 'MAIN',
2979
+ func: () => {
2980
+ // Check if already injected
2981
+ if (window.__chromePilotWorkflowConsoleOverridden) {
2982
+ return;
2983
+ }
2984
+ window.__chromePilotWorkflowConsoleOverridden = true;
2985
+
2986
+ // Override console methods to capture logs
2987
+ const originalLog = console.log;
2988
+ const originalError = console.error;
2989
+ const originalWarn = console.warn;
2990
+ const originalInfo = console.info;
2991
+
2992
+ const sendLog = (level, args) => {
2993
+ try {
2994
+ // Pre-serialize arguments to strings to avoid structured clone errors
2995
+ const serializedArgs = args.map(arg => {
2996
+ try {
2997
+ if (arg === null) return 'null';
2998
+ if (arg === undefined) return 'undefined';
2999
+ if (typeof arg === 'function') return '[Function: ' + (arg.name || 'anonymous') + ']';
3000
+ if (arg instanceof Element) return '[DOM Element: ' + arg.tagName + ']';
3001
+ if (typeof arg === 'object') {
3002
+ // Try to stringify, but limit depth to avoid circular references
3003
+ return JSON.stringify(arg, null, 2);
3004
+ }
3005
+ return String(arg);
3006
+ } catch (e) {
3007
+ return '[Object: could not serialize]';
3008
+ }
3009
+ });
3010
+
3011
+ // Post message to content script
3012
+ window.postMessage({
3013
+ type: 'chrome-pilot-workflow-console-log',
3014
+ log: {
3015
+ level,
3016
+ message: serializedArgs.join(' '),
3017
+ timestamp: Date.now()
3018
+ }
3019
+ }, '*');
3020
+ } catch (e) {
3021
+ // Ignore errors when sending logs
3022
+ }
3023
+ };
3024
+
3025
+ console.log = (...args) => {
3026
+ sendLog('log', args);
3027
+ originalLog.apply(console, args);
3028
+ };
3029
+
3030
+ console.error = (...args) => {
3031
+ sendLog('error', args);
3032
+ originalError.apply(console, args);
3033
+ };
3034
+
3035
+ console.warn = (...args) => {
3036
+ sendLog('warn', args);
3037
+ originalWarn.apply(console, args);
3038
+ };
3039
+
3040
+ console.info = (...args) => {
3041
+ sendLog('info', args);
3042
+ originalInfo.apply(console, args);
3043
+ };
3044
+
3045
+ // Also capture window errors
3046
+ window.addEventListener('error', (event) => {
3047
+ sendLog('exception', [`${event.message} at ${event.filename}:${event.lineno}:${event.colno}`]);
3048
+ });
3049
+
3050
+ window.addEventListener('unhandledrejection', (event) => {
3051
+ sendLog('exception', [`Unhandled Promise Rejection: ${event.reason}`]);
3052
+ });
3053
+ }
3054
+ });
3055
+
3056
+ // Inject content script to relay messages and handle workflow recording
3057
+ await chrome.scripting.executeScript({
3058
+ target: { tabId: tabId },
3059
+ func: (includeLogsInExport) => {
3060
+ // Listen for console log messages from main world
3061
+ window.addEventListener('message', (event) => {
3062
+ if (event.data && event.data.type === 'chrome-pilot-workflow-console-log') {
3063
+ // Forward to background script
3064
+ chrome.runtime.sendMessage({
3065
+ action: 'workflowConsoleLog',
3066
+ log: event.data.log
3067
+ });
3068
+ }
3069
+ });
3070
+ },
3071
+ args: [includeLogsInExport]
3072
+ });
3073
+
3074
+ // Ensure content script is injected
3075
+ try {
3076
+ await chrome.scripting.executeScript({
3077
+ target: { tabId: tabId },
3078
+ files: ['content.js']
3079
+ });
3080
+ await chrome.scripting.insertCSS({
3081
+ target: { tabId: tabId },
3082
+ files: ['content.css']
3083
+ });
3084
+ } catch (e) {
3085
+ // Content script might already be injected
3086
+ console.log('Content script injection attempt:', e.message);
3087
+ }
3088
+
3089
+ // Tell the content script to start recording
3090
+ await chrome.tabs.sendMessage(tabId, {
3091
+ action: 'startWorkflowRecording',
3092
+ screenshotSettings: screenshotSettings
3093
+ });
3094
+
3095
+ console.log('Workflow recording started successfully');
3096
+ } catch (error) {
3097
+ console.error('Error starting workflow recording:', error);
3098
+ throw error;
3099
+ }
3100
+ }
3101
+
3102
+ async function stopWorkflowRecording(tabId) {
3103
+ try {
3104
+ console.log('Stopping workflow recording for tab:', tabId);
3105
+
3106
+ // Check if recording was active
3107
+ if (!workflowRecordingTabs.has(tabId)) {
3108
+ throw new Error('No workflow recording active for this tab');
3109
+ }
3110
+
3111
+ // Get workflow from content script
3112
+ const response = await chrome.tabs.sendMessage(tabId, {
3113
+ action: 'stopWorkflowRecording'
3114
+ });
3115
+
3116
+ if (!response || !response.success) {
3117
+ throw new Error('Failed to get workflow from content script');
3118
+ }
3119
+
3120
+ // Handle both old format (array) and new format (object with actions and functionTraces)
3121
+ let workflowData = response.workflow || [];
3122
+ let workflow = Array.isArray(workflowData) ? workflowData : (workflowData.actions || []);
3123
+ let functionTraces = Array.isArray(workflowData) ? [] : (workflowData.functionTraces || []);
3124
+
3125
+ console.log(`[Background] Received ${workflow.length} actions and ${functionTraces.length} function traces`);
3126
+
3127
+ // If logs should be included, get them from session storage
3128
+ if (workflowIncludeLogs.get(tabId)) {
3129
+ const result = await chrome.storage.session.get(`workflow_${tabId}`);
3130
+ const logs = result[`workflow_${tabId}`] || [];
3131
+
3132
+ // Associate logs with workflow actions
3133
+ workflow = workflow.map((action, index) => {
3134
+ // Find logs that occurred around this action
3135
+ const actionLogs = logs.filter(log => {
3136
+ // Get time window
3137
+ const prevActionTime = index > 0 ? workflow[index - 1].timestamp : action.timestamp - 500;
3138
+ const nextActionTime = index < workflow.length - 1 ? workflow[index + 1].timestamp : action.timestamp + 5000;
3139
+
3140
+ return log.timestamp > prevActionTime && log.timestamp < nextActionTime;
3141
+ });
3142
+
3143
+ if (actionLogs.length > 0) {
3144
+ return { ...action, logs: actionLogs };
3145
+ }
3146
+ return action;
3147
+ });
3148
+ }
3149
+
3150
+ // Get settings before cleanup
3151
+ const sessionName = workflowSessionNames.get(tabId);
3152
+ const screenshotSettings = workflowScreenshotSettings.get(tabId);
3153
+ const includeLogs = workflowIncludeLogs.get(tabId) || false;
3154
+ const userId = workflowUserIds.get(tabId); // Get userId for usage tracking
3155
+
3156
+ // Clean up
3157
+ workflowRecordingTabs.delete(tabId);
3158
+ workflowIncludeLogs.delete(tabId);
3159
+ workflowScreenshotSettings.delete(tabId);
3160
+ workflowSessionNames.delete(tabId);
3161
+ workflowUserIds.delete(tabId); // Clean up userId tracking
3162
+ await chrome.storage.session.remove(`workflow_${tabId}`);
3163
+
3164
+ console.log('Workflow recording stopped, returning workflow:', workflow);
3165
+
3166
+ // Try to save to server
3167
+ try {
3168
+ const serverPorts = CONFIG_PORTS.slice(0, 5); // Use first 5 configured ports for workflow recording
3169
+ let serverResult = null;
3170
+
3171
+ // Get current URL and title
3172
+ const tab = await chrome.tabs.get(tabId);
3173
+ const sessionId = `workflow_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
3174
+
3175
+ for (const port of serverPorts) {
3176
+ try {
3177
+ console.log(`[Workflow] Attempting to save to server on port ${port}...`);
3178
+ const response = await fetch(`http://localhost:${port}/chromedebug/workflow-recording`, {
3179
+ method: 'POST',
3180
+ headers: {
3181
+ 'Content-Type': 'application/json',
3182
+ },
3183
+ body: JSON.stringify({
3184
+ sessionId: sessionId,
3185
+ name: sessionName,
3186
+ url: tab.url,
3187
+ title: tab.title,
3188
+ includeLogs: includeLogs,
3189
+ screenshotSettings: screenshotSettings || {}, // v2.0.8 fix: ensure it's an object, not null
3190
+ actions: workflow,
3191
+ logs: includeLogs ? workflow.flatMap(action => action.logs || []) : [],
3192
+ functionTraces: functionTraces // Include function execution traces
3193
+ })
3194
+ });
3195
+
3196
+ if (response.ok) {
3197
+ serverResult = await response.json();
3198
+ console.log(`[Workflow] Successfully saved to server on port ${port}`, serverResult);
3199
+
3200
+ // STEP 2: Track usage AFTER successful workflow recording completion (same pattern as stopRecording)
3201
+ if (userId) {
3202
+ console.log('[License] Tracking usage after workflow recording completion...');
3203
+ const usageResult = await LicenseHelper.trackUsageAfterRecording(userId);
3204
+ console.log('[License] Usage tracking result:', usageResult);
3205
+ }
3206
+
3207
+ break;
3208
+ } else {
3209
+ console.log(`[Workflow] Failed on port ${port}: ${response.status} ${response.statusText}`);
3210
+ const errorText = await response.text();
3211
+ console.log(`[Workflow] Error response:`, errorText);
3212
+ }
3213
+ } catch (error) {
3214
+ console.log(`Failed to save to server on port ${port}:`, error.message);
3215
+ }
3216
+ }
3217
+
3218
+ if (serverResult) {
3219
+ return {
3220
+ success: true,
3221
+ workflow: {
3222
+ sessionId: sessionId,
3223
+ url: tab.url,
3224
+ title: tab.title,
3225
+ actions: workflow,
3226
+ logs: workflowIncludeLogs.get(tabId) ? workflow.flatMap(action => action.logs || []) : []
3227
+ },
3228
+ savedToServer: true,
3229
+ serverResult: serverResult
3230
+ };
3231
+ } else {
3232
+ return {
3233
+ success: true,
3234
+ workflow: workflow,
3235
+ savedToServer: false
3236
+ };
3237
+ }
3238
+ } catch (error) {
3239
+ console.error('Error saving workflow:', error);
3240
+ return {
3241
+ success: true,
3242
+ workflow: workflow,
3243
+ savedToServer: false,
3244
+ error: error.message
3245
+ };
3246
+ }
3247
+ } catch (error) {
3248
+ console.error('Error stopping workflow recording:', error);
3249
+ throw error;
3250
+ }
3251
+ }
3252
+
3253
+ // Delete recording from server
3254
+ async function deleteRecordingFromServer(recordingId, sendResponse) {
3255
+ try {
3256
+ const ports = CONFIG_PORTS; // Use configured ports
3257
+ let deleted = false;
3258
+
3259
+ // Use comprehensive error handling for recording deletion
3260
+ const deleteRecording = async (data, context) => {
3261
+ for (const port of ports) {
3262
+ try {
3263
+ const response = await fetch(`http://localhost:${port}/chromedebug/recording/${data.recordingId}`, {
3264
+ method: 'DELETE'
3265
+ });
3266
+
3267
+ if (response.ok) {
3268
+ return { success: true, result: { deleted: true } };
3269
+ } else {
3270
+ throw new Error(`HTTP ${response.status}: Failed to delete recording from port ${port}`);
3271
+ }
3272
+ } catch (error) {
3273
+ console.error(`[ErrorHandling] Recording deletion failed on port ${port}:`, error);
3274
+ // Continue to next port instead of silent failure
3275
+ }
3276
+ }
3277
+ throw new Error('All server ports failed for recording deletion');
3278
+ };
3279
+
3280
+ const deleteResult = await handleSaveAttempt(
3281
+ deleteRecording,
3282
+ { recordingId },
3283
+ { operation: 'recording_deletion', recordingId }
3284
+ );
3285
+
3286
+ if (deleteResult.success) {
3287
+ deleted = true;
3288
+ console.log('Recording deleted successfully');
3289
+ sendResponse({ success: true });
3290
+ return;
3291
+ } else {
3292
+ console.error('[ErrorHandling] Failed to delete recording after comprehensive error handling');
3293
+ }
3294
+
3295
+ if (!deleted) {
3296
+ sendResponse({ success: false, error: 'Could not connect to server' });
3297
+ }
3298
+ } catch (error) {
3299
+ console.error('Error deleting recording:', error);
3300
+ sendResponse({ success: false, error: error.message });
3301
+ }
3302
+ }
3303
+
3304
+ // Retry pending frames after lease validation completes
3305
+ // v2.1.2: Handles race condition between lease renewal and frame validation
3306
+ async function retryPendingFrames(sessionId) {
3307
+ const pending = pendingFrameQueue.get(sessionId);
3308
+ if (!pending || pending.length === 0) return;
3309
+
3310
+ console.log(`[FrameQueue] Retrying ${pending.length} pending frame batches for session ${sessionId}`);
3311
+
3312
+ // Validate session now
3313
+ if (sessionManager) {
3314
+ const validationResult = await sessionManager.isSessionValid(sessionId);
3315
+ if (!validationResult.valid) {
3316
+ console.warn(`[FrameQueue] Session still invalid during retry: ${validationResult.reason}`);
3317
+ // Try again after another delay
3318
+ setTimeout(() => retryPendingFrames(sessionId), 200);
3319
+ return;
3320
+ }
3321
+ }
3322
+
3323
+ // Process all pending batches
3324
+ const batches = pendingFrameQueue.get(sessionId) || [];
3325
+ pendingFrameQueue.delete(sessionId); // Clear queue before processing
3326
+
3327
+ for (const frames of batches) {
3328
+ await handleFrameBatch({ sessionId, frames }, true); // skipValidation=true
3329
+ }
3330
+
3331
+ console.log(`[FrameQueue] Successfully processed ${batches.length} pending batches`);
3332
+ }
3333
+
3334
+ // Handle frame batches from frame capture
3335
+ async function handleFrameBatch(batchData, skipValidation = false) {
3336
+ console.log(`Processing frame batch for session ${batchData.sessionId}`);
3337
+
3338
+ try {
3339
+ const sessionId = batchData.sessionId;
3340
+
3341
+ // Validate session if session manager is available (unless skipping for retry)
3342
+ if (sessionManager && !skipValidation) {
3343
+ const validationResult = await sessionManager.isSessionValid(sessionId);
3344
+ if (!validationResult.valid) {
3345
+ // DON'T LOSE FRAMES - Queue for retry instead of rejecting
3346
+ // v2.1.2: Race condition fix - lease renewal may not have propagated yet
3347
+ if (!pendingFrameQueue.has(sessionId)) {
3348
+ pendingFrameQueue.set(sessionId, []);
3349
+ }
3350
+ pendingFrameQueue.get(sessionId).push(batchData.frames);
3351
+ console.warn(`[FrameQueue] Frame batch queued for retry - session validation failed: ${validationResult.reason}`);
3352
+
3353
+ // Retry after 100ms to allow lease renewal to propagate
3354
+ setTimeout(() => retryPendingFrames(sessionId), 100);
3355
+ return;
3356
+ }
3357
+ }
3358
+
3359
+ // Get current frame count from session manager (preferred) or legacy fallback
3360
+ let currentFrameIndex = 0;
3361
+ if (sessionManager) {
3362
+ const state = await sessionManager.getCurrentState();
3363
+ if (state && state.sessionId === sessionId) {
3364
+ currentFrameIndex = state.frameCount || 0;
3365
+ }
3366
+ } else {
3367
+ // Legacy fallback
3368
+ currentFrameIndex = frameCounter.get(sessionId) || 0;
3369
+ }
3370
+
3371
+ // Add index to each frame
3372
+ const indexedFrames = batchData.frames.map(frame => ({
3373
+ ...frame,
3374
+ index: currentFrameIndex++
3375
+ }));
3376
+
3377
+ // Update frame count via session manager (preferred) or legacy
3378
+ if (sessionManager) {
3379
+ const updateSuccess = await sessionManager.updateFrameCount(sessionId, currentFrameIndex);
3380
+ if (!updateSuccess) {
3381
+ console.warn('Failed to update frame count in session manager, using legacy counter');
3382
+ frameCounter.set(sessionId, currentFrameIndex);
3383
+ }
3384
+ } else {
3385
+ // Legacy fallback
3386
+ frameCounter.set(sessionId, currentFrameIndex);
3387
+ }
3388
+
3389
+ console.log(`Assigning frame indices ${frameCounter.get(sessionId) - indexedFrames.length} to ${currentFrameIndex - 1}`);
3390
+
3391
+ // Find available server port using settings
3392
+ const ports = await getServerPorts();
3393
+ let uploadSuccess = false;
3394
+ let lastError = null;
3395
+
3396
+ // Get session name from current session
3397
+ const sessionName = currentSession?.settings?.sessionName || null;
3398
+
3399
+ for (const port of ports) {
3400
+ try {
3401
+ const formData = new FormData();
3402
+ formData.append('sessionId', batchData.sessionId);
3403
+ formData.append('frames', JSON.stringify(indexedFrames));
3404
+ if (sessionName) {
3405
+ formData.append('sessionName', sessionName);
3406
+ }
3407
+
3408
+ const uploadResponse = await fetch(`http://localhost:${port}/chromedebug/frame-batch`, {
3409
+ method: 'POST',
3410
+ body: formData
3411
+ });
3412
+
3413
+ if (uploadResponse.ok) {
3414
+ const result = await uploadResponse.json();
3415
+ console.log(`Frame batch uploaded successfully to port ${port}:`, result);
3416
+ recordingServerPort = port; // Store the successful port
3417
+ uploadSuccess = true;
3418
+
3419
+ // Also save frames to Chrome local storage
3420
+ const storageKey = batchData.sessionId;
3421
+ const existingData = await chrome.storage.local.get([storageKey]);
3422
+ const sessionData = existingData[storageKey] || {
3423
+ sessionId: batchData.sessionId,
3424
+ frames: [],
3425
+ timestamp: Date.now()
3426
+ };
3427
+
3428
+ // Append new frames
3429
+ sessionData.frames = sessionData.frames.concat(batchData.frames);
3430
+ await chrome.storage.local.set({ [storageKey]: sessionData });
3431
+ console.log(`Saved ${batchData.frames.length} frames to local storage, total: ${sessionData.frames.length}`);
3432
+
3433
+ break;
3434
+ } else {
3435
+ const errorText = await uploadResponse.text();
3436
+ lastError = `HTTP ${uploadResponse.status}: ${errorText}`;
3437
+ console.error(`Failed to upload frame batch to port ${port}: ${lastError}`);
3438
+ }
3439
+ } catch (e) {
3440
+ lastError = e.message;
3441
+ console.log(`Failed to upload frame batch to port ${port}:`, e.message);
3442
+ }
3443
+ }
3444
+
3445
+ if (!uploadSuccess) {
3446
+ console.error(`CRITICAL: Failed to upload frame batch to any Chrome Debug server port. Last error: ${lastError}`);
3447
+ console.error(`Session ${batchData.sessionId} frames are only stored in Chrome extension storage and not accessible to Chrome Debug.`);
3448
+
3449
+ // Store error in chrome storage for user visibility
3450
+ await chrome.storage.local.set({
3451
+ [`upload_error_${batchData.sessionId}`]: {
3452
+ error: lastError,
3453
+ timestamp: Date.now(),
3454
+ sessionId: batchData.sessionId,
3455
+ frameCount: indexedFrames.length
3456
+ }
3457
+ });
3458
+ }
3459
+
3460
+ // Notify LogStreamer that frames have been captured and processed
3461
+ if (logStreamer && uploadSuccess) {
3462
+ logStreamer.notifyFrameCapture();
3463
+ }
3464
+ } catch (error) {
3465
+ console.error('Error processing frame batch:', error);
3466
+ }
3467
+ }
3468
+
3469
+ // Get preferred port from settings
3470
+ async function getServerPorts() {
3471
+ const data = await chrome.storage.sync.get(['serverPort']);
3472
+ const defaultPorts = CONFIG_PORTS.slice(0, 8); // Use first 8 configured ports as defaults
3473
+
3474
+ if (data.serverPort) {
3475
+ // User specified a port, try it first
3476
+ return [data.serverPort, ...defaultPorts.filter(p => p !== data.serverPort)];
3477
+ }
3478
+
3479
+ // Try to discover the active server port
3480
+ const discoveredPort = await discoverActiveServerPort();
3481
+ if (discoveredPort) {
3482
+ console.log(`Discovered Chrome Debug server on port ${discoveredPort}`);
3483
+ return [discoveredPort, ...defaultPorts.filter(p => p !== discoveredPort)];
3484
+ }
3485
+
3486
+ // Try last successful port first, then defaults
3487
+ return recordingServerPort ?
3488
+ [recordingServerPort, ...defaultPorts.filter(p => p !== recordingServerPort)] :
3489
+ defaultPorts;
3490
+ }
3491
+
3492
+ // Discover the active Chrome Debug server port
3493
+ async function discoverActiveServerPort() {
3494
+ const portsToCheck = CONFIG_PORTS.slice(0, 10); // Use first 10 configured ports for discovery
3495
+
3496
+ for (const port of portsToCheck) {
3497
+ try {
3498
+ const response = await fetch(`http://localhost:${port}/chromedebug/status`, {
3499
+ method: 'GET',
3500
+ signal: AbortSignal.timeout(1000) // 1 second timeout
3501
+ });
3502
+
3503
+ if (response.ok) {
3504
+ const data = await response.json();
3505
+ if (data.status === 'online') {
3506
+ return port;
3507
+ }
3508
+ }
3509
+ } catch (error) {
3510
+ // Port not available or server not responding
3511
+ continue;
3512
+ }
3513
+ }
3514
+
3515
+ return null;
3516
+ }
3517
+
3518
+ // Handle frame capture session completion
3519
+ async function handleFrameCaptureComplete(sessionData) {
3520
+ console.log(`Frame capture session ${sessionData.sessionId} completed with ${sessionData.totalFrames} frames`);
3521
+
3522
+ try {
3523
+ // Store the tab ID and session ID before clearing
3524
+ const tabIdToNotify = recordingTabId;
3525
+ const sessionIdToUse = sessionData.sessionId || currentRecordingSessionId;
3526
+
3527
+ // Wait a moment to ensure all frame batches are processed and saved
3528
+ await new Promise(resolve => setTimeout(resolve, 1000));
3529
+
3530
+ // CRITICAL FIX: Wait for LogBuffer flush completion before reading logs
3531
+ let bufferedLogs = [];
3532
+ try {
3533
+ const maxWaitTime = 10000; // 10 seconds maximum wait
3534
+ const pollInterval = 100; // Check every 100ms
3535
+ let waitedTime = 0;
3536
+ let flushCompleted = false;
3537
+
3538
+ console.log('[Background] Waiting for LogBuffer flush completion...');
3539
+
3540
+ // Poll for flush completion status
3541
+ while (waitedTime < maxWaitTime && !flushCompleted) {
3542
+ const flushStatus = await chrome.storage.session.get(`flush_completed_${tabIdToNotify}`);
3543
+ flushCompleted = flushStatus[`flush_completed_${tabIdToNotify}`] === true;
3544
+
3545
+ if (!flushCompleted) {
3546
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
3547
+ waitedTime += pollInterval;
3548
+ }
3549
+ }
3550
+
3551
+ if (flushCompleted) {
3552
+ console.log(`[Background] LogBuffer flush completed after ${waitedTime}ms`);
3553
+ } else {
3554
+ console.warn(`[Background] LogBuffer flush verification timed out after ${waitedTime}ms`);
3555
+ }
3556
+
3557
+ // Retrieve buffered logs for this tab
3558
+ const logsResult = await chrome.storage.session.get(String(tabIdToNotify));
3559
+ bufferedLogs = logsResult[tabIdToNotify] || [];
3560
+ console.log(`Retrieved ${bufferedLogs.length} buffered logs for post-processing`);
3561
+ if (bufferedLogs.length > 0) {
3562
+ console.log('First log:', bufferedLogs[0]);
3563
+ }
3564
+
3565
+ // Clean up flush completion marker
3566
+ await chrome.storage.session.remove(`flush_completed_${tabIdToNotify}`);
3567
+
3568
+ } catch (flushWaitError) {
3569
+ console.error('[Background] Error waiting for flush completion:', flushWaitError);
3570
+ // Fallback: Try to read logs anyway
3571
+ const logsResult = await chrome.storage.session.get(String(tabIdToNotify));
3572
+ bufferedLogs = logsResult[tabIdToNotify] || [];
3573
+ console.log(`Fallback: Retrieved ${bufferedLogs.length} buffered logs`);
3574
+ }
3575
+
3576
+ // Clear the buffered logs
3577
+ await chrome.storage.session.remove(String(tabIdToNotify));
3578
+
3579
+ // Send logs to server for association with frames with retry logic
3580
+ if (bufferedLogs.length > 0) {
3581
+
3582
+ // Retry logic to handle race conditions
3583
+ const maxRetries = 3;
3584
+ let retryCount = 0;
3585
+ let success = false;
3586
+ let lastErrorDetails = null;
3587
+
3588
+ console.log(`[LOG-ASSOC-DEBUG] ========== STARTING LOG ASSOCIATION ==========`);
3589
+ console.log(`[LOG-ASSOC-DEBUG] Session ID: ${sessionIdToUse}`);
3590
+ console.log(`[LOG-ASSOC-DEBUG] Number of logs to associate: ${bufferedLogs.length}`);
3591
+ console.log(`[LOG-ASSOC-DEBUG] Tab ID: ${tabIdToNotify}`);
3592
+
3593
+ // PRE-FLIGHT CHECK: Verify frames exist in database before attempting log association
3594
+ try {
3595
+ const ports = await getServerPorts();
3596
+ console.log(`[LOG-ASSOC-DEBUG] PRE-FLIGHT: Checking if frames exist for session ${sessionIdToUse}...`);
3597
+ for (const port of ports) {
3598
+ try {
3599
+ const checkResponse = await fetch(`http://localhost:${port}/chromedebug/frame-session/${sessionIdToUse}`, {
3600
+ method: 'GET',
3601
+ signal: AbortSignal.timeout(2000)
3602
+ });
3603
+ if (checkResponse.ok) {
3604
+ const sessionData = await checkResponse.json();
3605
+ const frameCount = sessionData.frames?.length || 0;
3606
+ console.log(`[LOG-ASSOC-DEBUG] PRE-FLIGHT: Found ${frameCount} frames on port ${port}`);
3607
+ if (frameCount === 0) {
3608
+ console.warn(`[LOG-ASSOC-DEBUG] ⚠️ WARNING: Session exists but has 0 frames! This will cause log association to fail.`);
3609
+ console.warn(`[LOG-ASSOC-DEBUG] Session data:`, sessionData);
3610
+ }
3611
+ break;
3612
+ }
3613
+ } catch (e) {
3614
+ console.log(`[LOG-ASSOC-DEBUG] PRE-FLIGHT: Could not check port ${port}:`, e.message);
3615
+ }
3616
+ }
3617
+ } catch (preflightError) {
3618
+ console.error(`[LOG-ASSOC-DEBUG] PRE-FLIGHT check failed:`, preflightError);
3619
+ }
3620
+
3621
+ while (retryCount < maxRetries && !success) {
3622
+ try {
3623
+ const ports = await getServerPorts();
3624
+ console.log(`[LOG-ASSOC-DEBUG] Attempt ${retryCount + 1}/${maxRetries}: Trying ports:`, ports);
3625
+
3626
+ for (const port of ports) {
3627
+ try {
3628
+ console.log(`[LOG-ASSOC-DEBUG] Attempting port ${port}...`);
3629
+ const requestBody = {
3630
+ sessionId: sessionIdToUse,
3631
+ logs: bufferedLogs
3632
+ };
3633
+ console.log(`[LOG-ASSOC-DEBUG] Request payload: sessionId=${sessionIdToUse}, logs.length=${bufferedLogs.length}`);
3634
+ if (bufferedLogs.length > 0) {
3635
+ console.log(`[LOG-ASSOC-DEBUG] First log sample:`, {
3636
+ timestamp: bufferedLogs[0].timestamp,
3637
+ level: bufferedLogs[0].level,
3638
+ message: bufferedLogs[0].message?.substring(0, 100)
3639
+ });
3640
+ }
3641
+
3642
+ const response = await fetch(`http://localhost:${port}/chromedebug/associate-logs`, {
3643
+ method: 'POST',
3644
+ headers: {
3645
+ 'Content-Type': 'application/json'
3646
+ },
3647
+ body: JSON.stringify(requestBody)
3648
+ });
3649
+
3650
+ console.log(`[LOG-ASSOC-DEBUG] Port ${port} response status: ${response.status} ${response.statusText}`);
3651
+
3652
+ if (response.ok) {
3653
+ const result = await response.json();
3654
+ console.log(`[LOG-ASSOC-DEBUG] ✅ SUCCESS on port ${port}:`, result);
3655
+ console.log(`Logs associated successfully on port ${port}:`, result);
3656
+ success = true;
3657
+
3658
+ // Fetch the updated session data with logs from server
3659
+ try {
3660
+ const sessionResponse = await fetch(`http://localhost:${port}/chromedebug/frame-session/${sessionIdToUse}`);
3661
+ if (sessionResponse.ok) {
3662
+ const updatedSession = await sessionResponse.json();
3663
+ console.log(`[LOG-ASSOC-DEBUG] Fetched updated session: ${updatedSession.frames?.length || 0} frames`);
3664
+ console.log(`Fetched updated session with ${updatedSession.frames ? updatedSession.frames.length : 0} frames`);
3665
+
3666
+ // Update Chrome local storage with the frames that now have logs
3667
+ const storageKey = sessionIdToUse;
3668
+ await chrome.storage.local.set({ [storageKey]: updatedSession });
3669
+ console.log('[LOG-ASSOC-DEBUG] Updated local storage with frames containing logs');
3670
+ console.log('Updated local storage with frames containing logs');
3671
+ } else {
3672
+ console.error(`[LOG-ASSOC-DEBUG] Failed to fetch updated session: ${sessionResponse.status}`);
3673
+ console.error('Failed to fetch updated session data');
3674
+ }
3675
+ } catch (fetchError) {
3676
+ console.error('[LOG-ASSOC-DEBUG] Error fetching updated session:', fetchError);
3677
+ console.error('Error fetching updated session:', fetchError);
3678
+ }
3679
+
3680
+ break;
3681
+ } else {
3682
+ // Log the actual error response
3683
+ const errorBody = await response.text();
3684
+ lastErrorDetails = {
3685
+ port,
3686
+ status: response.status,
3687
+ statusText: response.statusText,
3688
+ body: errorBody,
3689
+ attempt: retryCount + 1
3690
+ };
3691
+ console.error(`[LOG-ASSOC-DEBUG] ❌ Port ${port} failed with ${response.status}:`, errorBody);
3692
+ try {
3693
+ const errorJson = JSON.parse(errorBody);
3694
+ console.error(`[LOG-ASSOC-DEBUG] Error details:`, errorJson);
3695
+ } catch (e) {
3696
+ // Not JSON, already logged as text
3697
+ }
3698
+ }
3699
+ } catch (e) {
3700
+ lastErrorDetails = {
3701
+ port,
3702
+ error: e.message,
3703
+ attempt: retryCount + 1
3704
+ };
3705
+ console.error(`[LOG-ASSOC-DEBUG] ❌ Port ${port} exception:`, e);
3706
+ console.log(`Failed to associate logs on port ${port}:`, e.message);
3707
+ }
3708
+ }
3709
+
3710
+ if (!success) {
3711
+ retryCount++;
3712
+ if (retryCount < maxRetries) {
3713
+ console.log(`[LOG-ASSOC-DEBUG] Retrying in 1 second (attempt ${retryCount + 1}/${maxRetries})...`);
3714
+ console.log(`Retrying log association (attempt ${retryCount + 1}/${maxRetries})...`);
3715
+ await new Promise(resolve => setTimeout(resolve, 1000));
3716
+ }
3717
+ }
3718
+ } catch (error) {
3719
+ console.error('[LOG-ASSOC-DEBUG] Outer error:', error);
3720
+ console.error('Error associating logs:', error);
3721
+ retryCount++;
3722
+ if (retryCount < maxRetries) {
3723
+ await new Promise(resolve => setTimeout(resolve, 1000));
3724
+ }
3725
+ }
3726
+ }
3727
+
3728
+ console.log(`[LOG-ASSOC-DEBUG] ========== LOG ASSOCIATION COMPLETE ==========`);
3729
+ console.log(`[LOG-ASSOC-DEBUG] Success: ${success}`);
3730
+ if (!success && lastErrorDetails) {
3731
+ console.error(`[LOG-ASSOC-DEBUG] Last error details:`, lastErrorDetails);
3732
+ }
3733
+
3734
+ if (!success) {
3735
+ console.error('Failed to associate logs after all retries');
3736
+
3737
+ // Build detailed error message for notification
3738
+ let errorDetails = `Failed to send ${bufferedLogs.length} console logs to server.`;
3739
+ if (lastErrorDetails) {
3740
+ if (lastErrorDetails.status === 400) {
3741
+ errorDetails += ` Server error: ${lastErrorDetails.body}`;
3742
+ } else if (lastErrorDetails.status) {
3743
+ errorDetails += ` HTTP ${lastErrorDetails.status}: ${lastErrorDetails.statusText}`;
3744
+ } else if (lastErrorDetails.error) {
3745
+ errorDetails += ` Error: ${lastErrorDetails.error}`;
3746
+ }
3747
+ }
3748
+
3749
+ // Show user notification about log association failure
3750
+ chrome.notifications.create({
3751
+ type: 'basic',
3752
+ iconUrl: 'icon.png',
3753
+ title: 'Chrome Debug - Log Association Failed',
3754
+ message: `${errorDetails} Recording saved but logs may be missing. Check console for details.`,
3755
+ priority: 1
3756
+ });
3757
+ }
3758
+ } else {
3759
+ // No logs were captured during the session
3760
+ console.warn('[Recording] No console logs were captured during this session');
3761
+ console.warn('[Recording] Possible reasons:');
3762
+ console.warn(' - Page had no console output during recording');
3763
+ console.warn(' - Console interception failed (check restricted pages like chrome://)');
3764
+ console.warn(' - LogBuffer initialization or content script injection failed');
3765
+
3766
+ // Show user notification about no logs captured
3767
+ chrome.notifications.create({
3768
+ type: 'basic',
3769
+ iconUrl: 'icon.png',
3770
+ title: 'Chrome Debug - No Console Logs Captured',
3771
+ message: 'Recording completed but no console logs were captured. This may be expected if the page had no console activity.',
3772
+ priority: 0
3773
+ });
3774
+ }
3775
+
3776
+ // Save screen interactions to database
3777
+ if (screenInteractions.length > 0) {
3778
+ console.log('Saving', screenInteractions.length, 'screen interactions to database');
3779
+
3780
+ // Use comprehensive error handling for screen interactions save
3781
+ const saveScreenInteractions = async (data, context) => {
3782
+ // Diagnostic logging for screen interactions save
3783
+ console.log('[ScreenInteractions] Preparing to save interactions:', {
3784
+ sessionId: context.sessionId,
3785
+ dataType: typeof data,
3786
+ isArray: Array.isArray(data),
3787
+ interactionCount: Array.isArray(data) ? data.length : 'N/A',
3788
+ hasComplexData: Array.isArray(data) && data.some(i => i.element_html && i.element_html.length > 100)
3789
+ });
3790
+
3791
+ let lastError = null;
3792
+ for (const port of CONFIG_PORTS) {
3793
+ try {
3794
+ // Test JSON serialization before sending
3795
+ let serializedBody;
3796
+ try {
3797
+ // data is already { interactions: screenInteractions } from caller
3798
+ serializedBody = JSON.stringify(data);
3799
+ console.log('[ScreenInteractions] JSON serialization successful, length:', serializedBody.length);
3800
+ } catch (serError) {
3801
+ console.error('[ScreenInteractions] JSON serialization failed:', serError);
3802
+ throw new Error(`JSON serialization failed: ${serError.message}`);
3803
+ }
3804
+
3805
+ const response = await fetch(`http://localhost:${port}/chromedebug/screen-interactions/${context.sessionId}`, {
3806
+ method: 'POST',
3807
+ headers: {
3808
+ 'Content-Type': 'application/json'
3809
+ },
3810
+ body: serializedBody
3811
+ });
3812
+
3813
+ if (response.ok) {
3814
+ // Only log if we had to try multiple ports
3815
+ if (port !== CONFIG_PORTS[0]) {
3816
+ console.log(`[ScreenInteractions] Connected to server on port ${port}`);
3817
+ }
3818
+ return { success: true, result: { saved: true } };
3819
+ } else {
3820
+ throw new Error(`HTTP ${response.status}: Failed to save screen interactions to port ${port}`);
3821
+ }
3822
+ } catch (error) {
3823
+ lastError = error;
3824
+ // Silent during discovery - port attempts are expected to fail until we find the right one
3825
+ }
3826
+ }
3827
+
3828
+ // Only log error when ALL ports fail
3829
+ console.error('[ErrorHandling] Screen interactions save failed on all configured ports:', lastError);
3830
+ throw new Error('All server ports failed for screen interactions save');
3831
+ };
3832
+
3833
+ const saveResult = await handleSaveAttempt(
3834
+ saveScreenInteractions,
3835
+ { interactions: screenInteractions },
3836
+ { operation: 'screen_interactions_save', sessionId: sessionData.sessionId }
3837
+ );
3838
+
3839
+ if (saveResult.success) {
3840
+ console.log('Screen interactions saved successfully with comprehensive error handling');
3841
+ } else {
3842
+ console.error('[ErrorHandling] Failed to save screen interactions after comprehensive error handling');
3843
+ }
3844
+ }
3845
+
3846
+ // Stop session via session manager if available
3847
+ if (sessionManager && currentSession?.sessionId === sessionData.sessionId && currentOwnerId) {
3848
+ try {
3849
+ const stopResult = await sessionManager.stopRecording(currentSession.sessionId, currentOwnerId);
3850
+ if (stopResult.success) {
3851
+ console.log(`Session stopped via session manager. Duration: ${stopResult.sessionDuration}ms, Frames: ${stopResult.frameCount}`);
3852
+ } else {
3853
+ console.warn('Failed to stop session via session manager:', stopResult.error?.message);
3854
+ }
3855
+ } catch (error) {
3856
+ console.error('Error stopping session via session manager:', error);
3857
+ }
3858
+
3859
+ // Clear session cache
3860
+ currentSession = null;
3861
+ currentOwnerId = null;
3862
+ }
3863
+
3864
+ // Update legacy recording state (will be removed when migration is complete)
3865
+ isCurrentlyRecording = false;
3866
+ recordingTabId = null;
3867
+ currentRecordingSessionId = null;
3868
+ frameCounter.delete(sessionData.sessionId); // Clean up frame counter
3869
+ screenInteractions = []; // Clear interactions
3870
+
3871
+ // Update storage
3872
+ chrome.storage.local.set({
3873
+ recordingActive: false,
3874
+ recordingStartTime: null,
3875
+ recordingTabId: null
3876
+ });
3877
+
3878
+ // Update badge
3879
+ chrome.action.setBadgeText({ text: '' });
3880
+ chrome.action.setBadgeBackgroundColor({ color: '#4688F1' });
3881
+
3882
+ // Notify user with session info
3883
+ const portInfo = recordingServerPort ? ` - Server Port: ${recordingServerPort}` : '';
3884
+ chrome.notifications.create({
3885
+ type: 'basic',
3886
+ iconUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
3887
+ title: 'Frame Capture Complete',
3888
+ message: `Session ID: ${sessionData.sessionId} (${sessionData.totalFrames} frames)${portInfo}`,
3889
+ priority: 2
3890
+ });
3891
+
3892
+ // Notify popup if open
3893
+ chrome.runtime.sendMessage({
3894
+ action: 'frameSessionComplete',
3895
+ sessionId: sessionData.sessionId,
3896
+ sessionName: currentSession?.settings?.sessionName || null,
3897
+ totalFrames: sessionData.totalFrames,
3898
+ duration: sessionData.duration,
3899
+ serverPort: recordingServerPort
3900
+ }).catch(() => {});
3901
+
3902
+ // Notify content script to hide recording indicator
3903
+ if (tabIdToNotify) {
3904
+ chrome.tabs.sendMessage(tabIdToNotify, {
3905
+ action: 'recordingSessionComplete',
3906
+ sessionId: sessionData.sessionId,
3907
+ totalFrames: sessionData.totalFrames
3908
+ }).catch(() => {});
3909
+ }
3910
+
3911
+ // Close offscreen document
3912
+ await closeOffscreenDocument();
3913
+
3914
+ } catch (error) {
3915
+ console.error('Error handling frame capture completion:', error);
3916
+ }
3917
+ }