@dynamicu/chromedebug-mcp 2.6.4 → 2.6.7

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.
@@ -10,6 +10,7 @@ importScripts('upload-manager.js');
10
10
  importScripts('chrome-session-manager.js');
11
11
  importScripts('firebase-config.js');
12
12
  importScripts('license-helper.js');
13
+ importScripts('browser-recording-manager.js');
13
14
 
14
15
  const CONFIG_PORTS = CHROMEDEBUG_CONFIG.ports;
15
16
  const DISCOVERY_TIMEOUT = CHROMEDEBUG_CONFIG.discoveryTimeout;
@@ -35,12 +36,18 @@ class LogStreamer {
35
36
  }
36
37
 
37
38
  async init() {
39
+ // Skip initialization in browser-only mode
40
+ if (serverMode === 'browser-only') {
41
+ console.log('[LogStreamer] Skipping initialization - browser-only mode');
42
+ return;
43
+ }
44
+
38
45
  // Test server availability
39
46
  await this.checkServerAvailability();
40
-
47
+
41
48
  // Start periodic streaming
42
49
  this.startPeriodicStreaming();
43
-
50
+
44
51
  console.log('[LogStreamer] Initialized with server availability:', this.serverAvailable);
45
52
  }
46
53
 
@@ -78,10 +85,16 @@ class LogStreamer {
78
85
  async addLog(log, sessionId) {
79
86
  log.sequence = ++this.sequenceNumber;
80
87
  log.timestamp = Date.now();
81
-
88
+
82
89
  this.currentSessionId = sessionId;
90
+
91
+ // In browser-only mode, skip server streaming (logs stored in LogBuffer)
92
+ if (serverMode === 'browser-only') {
93
+ return;
94
+ }
95
+
83
96
  this.buffer.push(log);
84
-
97
+
85
98
  // If buffer is getting full, force immediate stream
86
99
  if (this.buffer.length >= this.maxBufferSize) {
87
100
  await this.streamLogs();
@@ -89,6 +102,9 @@ class LogStreamer {
89
102
  }
90
103
 
91
104
  async streamLogs() {
105
+ // Skip streaming in browser-only mode
106
+ if (serverMode === 'browser-only') return;
107
+
92
108
  if (this.buffer.length === 0 || !this.currentSessionId) return;
93
109
 
94
110
  // Check if we have waited at least 100ms since last frame capture
@@ -470,6 +486,8 @@ let dataBuffer = null;
470
486
  let uploadManager = null;
471
487
  let logStreamer = null;
472
488
  let logBuffer = null;
489
+ let browserRecordingManager = null;
490
+ let serverMode = 'unknown'; // 'server', 'browser-only', or 'unknown'
473
491
 
474
492
  // Error handling state and configuration
475
493
  // IMPORTANT: These must be declared before initializeServices()
@@ -566,6 +584,15 @@ async function initializeServices() {
566
584
  logBuffer = null;
567
585
  }
568
586
 
587
+ // Initialize browser recording manager for browser-only mode
588
+ try {
589
+ browserRecordingManager = new BrowserRecordingManager(dataBuffer);
590
+ console.log('[Background] Browser recording manager initialized');
591
+ } catch (recordingErr) {
592
+ console.error('[Background] Failed to initialize browser recording manager:', recordingErr);
593
+ browserRecordingManager = null;
594
+ }
595
+
569
596
  // Initialize session manager with recovery
570
597
  try {
571
598
  if (!sessionManager) {
@@ -909,6 +936,112 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
909
936
  }
910
937
  break;
911
938
 
939
+ case 'getBrowserRecordings':
940
+ // Get all browser-only recordings from IndexedDB
941
+ console.log('[Background] getBrowserRecordings - Manager exists:', !!browserRecordingManager);
942
+ if (browserRecordingManager) {
943
+ browserRecordingManager.listRecordings().then(recordings => {
944
+ console.log('[Background] Recordings retrieved from manager:', recordings);
945
+ sendResponse(recordings);
946
+ }).catch(error => {
947
+ console.error('[Background] Failed to get browser recordings:', error);
948
+ sendResponse([]);
949
+ });
950
+ return true; // Async response
951
+ } else {
952
+ console.warn('[Background] browserRecordingManager not initialized');
953
+ sendResponse([]);
954
+ }
955
+ break;
956
+
957
+ case 'exportBrowserRecording':
958
+ // Export a browser-only recording
959
+ if (browserRecordingManager && message.sessionId) {
960
+ browserRecordingManager.exportRecording(message.sessionId).then(result => {
961
+ sendResponse({ success: true, data: result });
962
+ }).catch(error => {
963
+ console.error('[Background] Failed to export recording:', error);
964
+ sendResponse({ success: false, error: error.message });
965
+ });
966
+ return true; // Async response
967
+ } else {
968
+ sendResponse({ success: false, error: 'Manager not available or missing sessionId' });
969
+ }
970
+ break;
971
+
972
+ case 'deleteBrowserRecording':
973
+ // Delete a browser-only recording
974
+ if (browserRecordingManager && message.sessionId) {
975
+ browserRecordingManager.deleteRecording(message.sessionId).then(() => {
976
+ sendResponse({ success: true });
977
+ }).catch(error => {
978
+ console.error('[Background] Failed to delete recording:', error);
979
+ sendResponse({ success: false, error: error.message });
980
+ });
981
+ return true; // Async response
982
+ } else {
983
+ sendResponse({ success: false, error: 'Manager not available or missing sessionId' });
984
+ }
985
+ break;
986
+
987
+ case 'getBrowserRecordingFrames':
988
+ // Get frames for a browser-only recording
989
+ if (dataBuffer && message.sessionId) {
990
+ dataBuffer.getBrowserFrames(message.sessionId).then(frames => {
991
+ console.log(`[DEBUG] getBrowserRecordingFrames: Retrieved ${frames.length} frames from IndexedDB`);
992
+
993
+ // Debug first frame to see structure
994
+ if (frames.length > 0) {
995
+ const firstFrame = frames[0];
996
+ const debugInfo = {
997
+ hasScreenshot: !!firstFrame.screenshot,
998
+ screenshotPrefix: firstFrame.screenshot ? firstFrame.screenshot.substring(0, 30) : 'N/A',
999
+ hasLogs: !!firstFrame.logs,
1000
+ logsCount: firstFrame.logs ? firstFrame.logs.length : 0,
1001
+ timestamp: firstFrame.timestamp,
1002
+ frameIndex: firstFrame.frameIndex,
1003
+ allKeys: Object.keys(firstFrame)
1004
+ };
1005
+ console.log('[DEBUG] First frame structure:', JSON.stringify(debugInfo, null, 2));
1006
+ }
1007
+
1008
+ // Transform frames to match frame editor's expected structure
1009
+ // Frame editor expects 'imageData' property, but we store as 'screenshot'
1010
+ const transformedFrames = frames.map(frame => ({
1011
+ ...frame,
1012
+ imageData: frame.screenshot // Rename screenshot to imageData
1013
+ }));
1014
+
1015
+ const transformDebug = transformedFrames.length > 0 ? {
1016
+ hasImageData: !!transformedFrames[0].imageData,
1017
+ imageDataPrefix: transformedFrames[0].imageData ? transformedFrames[0].imageData.substring(0, 30) : 'N/A',
1018
+ hasLogs: !!transformedFrames[0].logs,
1019
+ logsCount: transformedFrames[0].logs ? transformedFrames[0].logs.length : 0,
1020
+ allKeys: Object.keys(transformedFrames[0])
1021
+ } : 'No frames';
1022
+ console.log('[DEBUG] Transformed first frame:', JSON.stringify(transformDebug, null, 2));
1023
+
1024
+ sendResponse({ success: true, frames: transformedFrames });
1025
+ }).catch(error => {
1026
+ console.error('[Background] Failed to get browser recording frames:', error);
1027
+ sendResponse({ success: false, error: error.message });
1028
+ });
1029
+ return true; // Async response
1030
+ } else {
1031
+ sendResponse({ success: false, error: 'DataBuffer not available or missing sessionId' });
1032
+ }
1033
+ break;
1034
+
1035
+ case 'getQuotaInfo':
1036
+ // Get storage quota information
1037
+ getQuotaInfo().then(info => {
1038
+ sendResponse(info);
1039
+ }).catch(error => {
1040
+ console.error('[Background] Failed to get quota info:', error);
1041
+ sendResponse({ supported: false });
1042
+ });
1043
+ return true; // Async response
1044
+
912
1045
  default:
913
1046
  // Don't warn or respond for messages that should be handled by other listener (v2.0.6)
914
1047
  // This prevents intercepting workflow recording messages
@@ -1891,6 +2024,17 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
1891
2024
  return true;
1892
2025
  }
1893
2026
 
2027
+ // Get quota information for browser-only mode
2028
+ if (request.action === 'getQuotaInfo') {
2029
+ getQuotaInfo().then((quotaInfo) => {
2030
+ sendResponse({ quotaInfo });
2031
+ }).catch((error) => {
2032
+ console.error('Error getting quota info:', error);
2033
+ sendResponse({ quotaInfo: null, error: error.message });
2034
+ });
2035
+ return true;
2036
+ }
2037
+
1894
2038
  // Session manager lease renewal handler
1895
2039
  if (request.action === 'renewLease') {
1896
2040
  console.log('[Background] RENEW LEASE - Session:', request.sessionId, 'Manager exists:', !!sessionManager);
@@ -2322,13 +2466,24 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2322
2466
 
2323
2467
  // Handle workflow screenshot capture
2324
2468
  if (request.action === 'captureWorkflowScreenshot' && sender.tab) {
2469
+ console.log('[SCREENSHOT-DEBUG] background.js - Received captureWorkflowScreenshot message');
2325
2470
  const tabId = sender.tab.id;
2326
2471
  const settings = request.settings || {};
2327
-
2472
+ console.log('[SCREENSHOT-DEBUG] background.js - tabId:', tabId);
2473
+ console.log('[SCREENSHOT-DEBUG] background.js - settings:', JSON.stringify(settings));
2474
+
2328
2475
  captureTabScreenshot(tabId, settings).then((screenshotData) => {
2476
+ console.log('[SCREENSHOT-DEBUG] background.js - captureTabScreenshot succeeded');
2477
+ console.log('[SCREENSHOT-DEBUG] background.js - screenshotData type:', typeof screenshotData);
2478
+ console.log('[SCREENSHOT-DEBUG] background.js - screenshotData length:', screenshotData?.length);
2479
+ console.log('[SCREENSHOT-DEBUG] background.js - screenshotData preview:', screenshotData?.substring(0, 100));
2480
+ console.log('[SCREENSHOT-DEBUG] background.js - Sending response with success: true');
2329
2481
  sendResponse({ success: true, screenshotData });
2330
2482
  }).catch((error) => {
2331
- console.error('Error capturing screenshot:', error);
2483
+ console.error('[SCREENSHOT-DEBUG] background.js - captureTabScreenshot FAILED');
2484
+ console.error('[SCREENSHOT-DEBUG] background.js - Error:', error);
2485
+ console.error('[SCREENSHOT-DEBUG] background.js - Error message:', error.message);
2486
+ console.error('[SCREENSHOT-DEBUG] background.js - Error stack:', error.stack);
2332
2487
  sendResponse({ success: false, error: error.message });
2333
2488
  });
2334
2489
  return true;
@@ -2456,14 +2611,34 @@ async function takeStandaloneSnapshot(tabId, note = '') {
2456
2611
 
2457
2612
  // Capture screenshot for workflow recording
2458
2613
  async function captureTabScreenshot(tabId, settings) {
2614
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - ENTRY');
2615
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - tabId:', tabId);
2616
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - settings:', JSON.stringify(settings));
2617
+
2459
2618
  try {
2460
- const dataUrl = await chrome.tabs.captureVisibleTab(null, {
2619
+ // Get the tab to find its window
2620
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - Getting tab info...');
2621
+ const tab = await chrome.tabs.get(tabId);
2622
+ const windowId = tab.windowId;
2623
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - windowId:', windowId);
2624
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - tab.url:', tab.url);
2625
+
2626
+ const captureOptions = {
2461
2627
  format: settings.format || 'jpeg',
2462
2628
  quality: settings.quality || 30
2463
- });
2464
-
2629
+ };
2630
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - captureOptions:', JSON.stringify(captureOptions));
2631
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - Calling chrome.tabs.captureVisibleTab...');
2632
+
2633
+ const dataUrl = await chrome.tabs.captureVisibleTab(windowId, captureOptions);
2634
+
2635
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - captureVisibleTab SUCCESS');
2636
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - dataUrl length:', dataUrl?.length);
2637
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - dataUrl prefix:', dataUrl?.substring(0, 100));
2638
+
2465
2639
  // If resolution is specified, resize the image
2466
2640
  if (settings.maxWidth || settings.maxHeight) {
2641
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - Resizing required, maxWidth:', settings.maxWidth, 'maxHeight:', settings.maxHeight);
2467
2642
  // Create an image element to get dimensions
2468
2643
  const img = new Image();
2469
2644
  const canvas = new OffscreenCanvas(1, 1);
@@ -2503,17 +2678,75 @@ async function captureTabScreenshot(tabId, settings) {
2503
2678
  });
2504
2679
  }
2505
2680
 
2681
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - No resize needed, returning dataUrl');
2682
+ console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - Final dataUrl length:', dataUrl?.length);
2506
2683
  return dataUrl;
2507
2684
  } catch (error) {
2508
- console.error('Error capturing tab screenshot:', error);
2685
+ console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - EXCEPTION CAUGHT');
2686
+ console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - Error:', error);
2687
+ console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - Error message:', error.message);
2688
+ console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - Error stack:', error.stack);
2509
2689
  throw error;
2510
2690
  }
2511
2691
  }
2512
2692
 
2513
2693
  // Start recording
2694
+ // Detect if server is available (server mode vs browser-only mode)
2695
+ async function detectServerMode() {
2696
+ console.log('[ServerDetection] Checking server availability...');
2697
+
2698
+ for (const port of CONFIG_PORTS) {
2699
+ try {
2700
+ const response = await Promise.race([
2701
+ fetch(`http://localhost:${port}/health`, {
2702
+ method: 'GET'
2703
+ }),
2704
+ new Promise((_, reject) =>
2705
+ setTimeout(() => reject(new Error('Timeout')), 1000)
2706
+ )
2707
+ ]);
2708
+
2709
+ if (response.ok) {
2710
+ console.log(`[ServerDetection] Server detected on port ${port}`);
2711
+ serverMode = 'server';
2712
+ return 'server';
2713
+ }
2714
+ } catch (error) {
2715
+ // Continue checking other ports
2716
+ }
2717
+ }
2718
+
2719
+ console.log('[ServerDetection] No server detected - using browser-only mode');
2720
+ serverMode = 'browser-only';
2721
+ return 'browser-only';
2722
+ }
2723
+
2724
+ // Simple quota info utility (replaces over-engineered QuotaMonitor class)
2725
+ async function getQuotaInfo() {
2726
+ if (!navigator.storage?.estimate) {
2727
+ return { supported: false };
2728
+ }
2729
+
2730
+ const { usage = 0, quota = 0 } = await navigator.storage.estimate();
2731
+ const percentage = quota > 0 ? usage / quota : 0;
2732
+ const availableMB = Math.round((quota - usage) / (1024 * 1024));
2733
+
2734
+ return {
2735
+ supported: true,
2736
+ percentage,
2737
+ availableMB,
2738
+ usageMB: Math.round(usage / (1024 * 1024)),
2739
+ quotaMB: Math.round(quota / (1024 * 1024))
2740
+ };
2741
+ }
2742
+
2514
2743
  async function startRecording(tabId, settings = {}) {
2515
2744
  try {
2516
- // STEP 1: Check license and usage limits BEFORE recording
2745
+ // STEP 1: Detect server mode before starting
2746
+ await detectServerMode();
2747
+ console.log(`[Recording] Mode: ${serverMode}`);
2748
+
2749
+ // STEP 2: Check license and usage limits BEFORE recording
2517
2750
  console.log('[License] Checking license before recording...');
2518
2751
  const licenseCheck = await LicenseHelper.checkLicenseBeforeRecording();
2519
2752
  console.log('[License] License check result:', licenseCheck);
@@ -2602,6 +2835,18 @@ async function startRecording(tabId, settings = {}) {
2602
2835
  const sessionId = currentSession.sessionId;
2603
2836
  const scheduledStartTime = Date.now() + 3000; // Schedule start 3 seconds in future for countdown synchronization
2604
2837
 
2838
+ // STEP 4: Initialize browser-only recording if in browser-only mode
2839
+ if (serverMode === 'browser-only' && browserRecordingManager) {
2840
+ console.log('[Recording] Initializing browser-only recording in IndexedDB');
2841
+ const tab = await chrome.tabs.get(tabId);
2842
+ await browserRecordingManager.startRecording(sessionId, {
2843
+ tabId: tabId,
2844
+ url: tab.url,
2845
+ title: tab.title || settings.sessionName || 'Browser Recording',
2846
+ startTime: scheduledStartTime
2847
+ });
2848
+ }
2849
+
2605
2850
  // Update legacy state for backward compatibility (will be removed)
2606
2851
  recordingTabId = tabId;
2607
2852
  isCurrentlyRecording = true;
@@ -2679,6 +2924,17 @@ async function startRecording(tabId, settings = {}) {
2679
2924
  }
2680
2925
  });
2681
2926
 
2927
+ // STEP 4: Initialize browser recording if in browser-only mode
2928
+ if (serverMode === 'browser-only' && browserRecordingManager) {
2929
+ const tab = await chrome.tabs.get(tabId);
2930
+ await browserRecordingManager.startRecording(sessionId, {
2931
+ tabId,
2932
+ url: tab.url,
2933
+ title: tab.title
2934
+ });
2935
+ console.log('[Recording] Browser recording initialized for session:', sessionId);
2936
+ }
2937
+
2682
2938
  // Send stream ID, session info, and settings to offscreen document for frame capture
2683
2939
  await chrome.runtime.sendMessage({
2684
2940
  type: 'start-frame-capture',
@@ -2688,14 +2944,15 @@ async function startRecording(tabId, settings = {}) {
2688
2944
  sessionId: sessionId,
2689
2945
  ownerId: currentOwnerId,
2690
2946
  scheduledStartTime: scheduledStartTime,
2947
+ mode: serverMode, // Pass mode to offscreen document
2691
2948
  settings: {
2692
2949
  frameRate: settings.frameRate || 1,
2693
- imageQuality: settings.imageQuality || 30,
2950
+ imageQuality: settings.imageQuality || (serverMode === 'browser-only' ? 15 : 30),
2694
2951
  frameFlash: settings.frameFlash !== false
2695
2952
  }
2696
2953
  });
2697
-
2698
- console.log('Recording started');
2954
+
2955
+ console.log('Recording started in', serverMode, 'mode');
2699
2956
  } catch (error) {
2700
2957
  console.error('Error in startRecording:', error);
2701
2958
  throw error;
@@ -2727,6 +2984,12 @@ async function stopRecording() {
2727
2984
  } else {
2728
2985
  console.log(`Recording stopped. Duration: ${stopResult.sessionDuration}ms, Frames: ${stopResult.frameCount}`);
2729
2986
 
2987
+ // Finalize browser-only recording if in browser-only mode
2988
+ if (serverMode === 'browser-only' && browserRecordingManager) {
2989
+ console.log('[Browser-Only] Finalizing browser recording in IndexedDB');
2990
+ await browserRecordingManager.stopRecording(currentSession.sessionId);
2991
+ }
2992
+
2730
2993
  // STEP 2: Track usage AFTER successful recording completion
2731
2994
  console.log('[License] Tracking usage after recording completion...');
2732
2995
  const usageResult = await LicenseHelper.trackUsageAfterRecording();
@@ -3215,8 +3478,21 @@ async function stopWorkflowRecording(tabId) {
3215
3478
  let workflowData = response.workflow || [];
3216
3479
  let workflow = Array.isArray(workflowData) ? workflowData : (workflowData.actions || []);
3217
3480
  let functionTraces = Array.isArray(workflowData) ? [] : (workflowData.functionTraces || []);
3218
-
3481
+
3219
3482
  console.log(`[Background] Received ${workflow.length} actions and ${functionTraces.length} function traces`);
3483
+ console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Checking actions for screenshot_data');
3484
+
3485
+ const actionsWithScreenshots = workflow.filter(a => a.screenshot_data);
3486
+ console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Actions WITH screenshot_data:', actionsWithScreenshots.length);
3487
+
3488
+ workflow.forEach((action, index) => {
3489
+ console.log(`[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Action ${index}:`, {
3490
+ type: action.type,
3491
+ hasScreenshotData: !!action.screenshot_data,
3492
+ screenshotDataLength: action.screenshot_data?.length,
3493
+ screenshotPreview: action.screenshot_data?.substring(0, 50)
3494
+ });
3495
+ });
3220
3496
 
3221
3497
  // If logs should be included, get them from session storage
3222
3498
  if (workflowIncludeLogs.get(tabId)) {
@@ -3269,22 +3545,40 @@ async function stopWorkflowRecording(tabId) {
3269
3545
  for (const port of serverPorts) {
3270
3546
  try {
3271
3547
  console.log(`[Workflow] Attempting to save to server on port ${port}...`);
3548
+
3549
+ const payloadData = {
3550
+ sessionId: sessionId,
3551
+ name: sessionName,
3552
+ url: tab.url,
3553
+ title: tab.title,
3554
+ includeLogs: includeLogs,
3555
+ screenshotSettings: screenshotSettings || {}, // v2.0.8 fix: ensure it's an object, not null
3556
+ actions: workflow,
3557
+ logs: includeLogs ? workflow.flatMap(action => action.logs || []) : [],
3558
+ functionTraces: functionTraces // Include function execution traces
3559
+ };
3560
+
3561
+ console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - HTTP POST payload being prepared');
3562
+ console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Payload actions count:', payloadData.actions.length);
3563
+
3564
+ const payloadActionsWithScreenshots = payloadData.actions.filter(a => a.screenshot_data);
3565
+ console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Payload actions WITH screenshot_data:', payloadActionsWithScreenshots.length);
3566
+
3567
+ payloadData.actions.forEach((action, index) => {
3568
+ console.log(`[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Payload Action ${index}:`, {
3569
+ type: action.type,
3570
+ hasScreenshotData: !!action.screenshot_data,
3571
+ screenshotDataLength: action.screenshot_data?.length,
3572
+ screenshotPreview: action.screenshot_data?.substring(0, 50)
3573
+ });
3574
+ });
3575
+
3272
3576
  const response = await fetch(`http://localhost:${port}/chromedebug/workflow-recording`, {
3273
3577
  method: 'POST',
3274
3578
  headers: {
3275
3579
  'Content-Type': 'application/json',
3276
3580
  },
3277
- body: JSON.stringify({
3278
- sessionId: sessionId,
3279
- name: sessionName,
3280
- url: tab.url,
3281
- title: tab.title,
3282
- includeLogs: includeLogs,
3283
- screenshotSettings: screenshotSettings || {}, // v2.0.8 fix: ensure it's an object, not null
3284
- actions: workflow,
3285
- logs: includeLogs ? workflow.flatMap(action => action.logs || []) : [],
3286
- functionTraces: functionTraces // Include function execution traces
3287
- })
3581
+ body: JSON.stringify(payloadData)
3288
3582
  });
3289
3583
 
3290
3584
  if (response.ok) {
@@ -3432,8 +3726,9 @@ async function handleFrameBatch(batchData, skipValidation = false) {
3432
3726
  try {
3433
3727
  const sessionId = batchData.sessionId;
3434
3728
 
3435
- // Validate session if session manager is available (unless skipping for retry)
3436
- if (sessionManager && !skipValidation) {
3729
+ // Skip session validation entirely in browser-only mode
3730
+ // Validate session if session manager is available (unless skipping for retry or in browser-only mode)
3731
+ if (sessionManager && !skipValidation && serverMode !== 'browser-only') {
3437
3732
  const validationResult = await sessionManager.isSessionValid(sessionId);
3438
3733
  if (!validationResult.valid) {
3439
3734
  // DON'T LOSE FRAMES - Queue for retry instead of rejecting
@@ -3537,18 +3832,43 @@ async function handleFrameBatch(batchData, skipValidation = false) {
3537
3832
  }
3538
3833
 
3539
3834
  if (!uploadSuccess) {
3540
- console.error(`CRITICAL: Failed to upload frame batch to any Chrome Debug server port. Last error: ${lastError}`);
3541
- console.error(`Session ${batchData.sessionId} frames are only stored in Chrome extension storage and not accessible to Chrome Debug.`);
3542
-
3543
- // Store error in chrome storage for user visibility
3544
- await chrome.storage.local.set({
3545
- [`upload_error_${batchData.sessionId}`]: {
3546
- error: lastError,
3547
- timestamp: Date.now(),
3548
- sessionId: batchData.sessionId,
3549
- frameCount: indexedFrames.length
3835
+ console.log(`No server available - checking if browser-only mode`);
3836
+
3837
+ // If in browser-only mode, store frames in IndexedDB
3838
+ if (serverMode === 'browser-only' && browserRecordingManager) {
3839
+ console.log(`[Browser-Only] Storing ${indexedFrames.length} frames in IndexedDB`);
3840
+ // Use the browser recording session ID, not the frame capture session ID
3841
+ const recordingSessionId = currentRecordingSessionId || batchData.sessionId;
3842
+ for (const frame of indexedFrames) {
3843
+ // Validate that critical timestamp data exists
3844
+ if (!frame.absoluteTimestamp) {
3845
+ console.warn(`[Browser-Only] WARNING: Frame ${frame.index} missing absoluteTimestamp - log association will fail`);
3846
+ }
3847
+
3848
+ await browserRecordingManager.addFrame(recordingSessionId, {
3849
+ frameIndex: frame.index,
3850
+ screenshot: frame.imageData || frame.screenshot, // Use imageData from frame capture
3851
+ logs: frame.logs || [],
3852
+ timestamp: frame.timestamp || Date.now(),
3853
+ absoluteTimestamp: frame.absoluteTimestamp // Don't use Date.now() fallback - preserve actual capture time
3854
+ });
3550
3855
  }
3551
- });
3856
+ console.log(`[Browser-Only] Frames stored successfully in IndexedDB for session ${recordingSessionId}`);
3857
+ } else {
3858
+ // Server mode but no server available - show error
3859
+ console.error(`CRITICAL: Failed to upload frame batch to any Chrome Debug server port. Last error: ${lastError}`);
3860
+ console.error(`Session ${batchData.sessionId} frames are only stored in Chrome extension storage and not accessible to Chrome Debug.`);
3861
+
3862
+ // Store error in chrome storage for user visibility
3863
+ await chrome.storage.local.set({
3864
+ [`upload_error_${batchData.sessionId}`]: {
3865
+ error: lastError,
3866
+ timestamp: Date.now(),
3867
+ sessionId: batchData.sessionId,
3868
+ frameCount: indexedFrames.length
3869
+ }
3870
+ });
3871
+ }
3552
3872
  }
3553
3873
 
3554
3874
  // Notify LogStreamer that frames have been captured and processed
@@ -3671,7 +3991,9 @@ async function handleFrameCaptureComplete(sessionData) {
3671
3991
  await chrome.storage.session.remove(String(tabIdToNotify));
3672
3992
 
3673
3993
  // Send logs to server for association with frames with retry logic
3674
- if (bufferedLogs.length > 0) {
3994
+ // Skip server communication in browser-only mode (logs already in IndexedDB)
3995
+ if (bufferedLogs.length > 0 && serverMode !== 'browser-only') {
3996
+ console.log(`[Server Mode] Associating ${bufferedLogs.length} logs with server...`);
3675
3997
 
3676
3998
  // Retry logic to handle race conditions
3677
3999
  const maxRetries = 3;
@@ -3849,6 +4171,62 @@ async function handleFrameCaptureComplete(sessionData) {
3849
4171
  priority: 1
3850
4172
  });
3851
4173
  }
4174
+ } else if (bufferedLogs.length > 0 && serverMode === 'browser-only') {
4175
+ // Browser-only mode: Associate logs with frames in IndexedDB
4176
+ console.log(`[Browser-Only] Associating ${bufferedLogs.length} logs with frames in IndexedDB...`);
4177
+
4178
+ try {
4179
+ // Get all frames for this session from IndexedDB
4180
+ const frames = await dataBuffer.getBrowserFrames(sessionIdToUse);
4181
+ console.log(`[Browser-Only] Retrieved ${frames.length} frames from IndexedDB for log association`);
4182
+
4183
+ if (frames.length === 0) {
4184
+ console.warn('[Browser-Only] No frames found - logs cannot be associated');
4185
+ } else {
4186
+ // Associate each log with the appropriate frame based on timestamp
4187
+ // Frames have absoluteTimestamp, logs have timestamp
4188
+
4189
+ for (const log of bufferedLogs) {
4190
+ // Find the frame that this log belongs to (closest frame before the log timestamp)
4191
+ let targetFrame = null;
4192
+ for (let i = 0; i < frames.length; i++) {
4193
+ const frame = frames[i];
4194
+ const frameTime = frame.absoluteTimestamp || frame.timestamp;
4195
+ if (frameTime <= log.timestamp) {
4196
+ targetFrame = frame;
4197
+ } else {
4198
+ break; // Frames are in chronological order
4199
+ }
4200
+ }
4201
+
4202
+ // If no frame found before this log, associate with first frame
4203
+ if (!targetFrame && frames.length > 0) {
4204
+ targetFrame = frames[0];
4205
+ }
4206
+
4207
+ if (targetFrame) {
4208
+ if (!targetFrame.logs) {
4209
+ targetFrame.logs = [];
4210
+ }
4211
+ targetFrame.logs.push(log);
4212
+ }
4213
+ }
4214
+
4215
+ // Update frames in IndexedDB with associated logs
4216
+ let updatedCount = 0;
4217
+ for (const frame of frames) {
4218
+ if (frame.logs && frame.logs.length > 0) {
4219
+ // Update this frame in IndexedDB
4220
+ await dataBuffer.updateBrowserFrame(frame.id, { logs: frame.logs });
4221
+ updatedCount++;
4222
+ }
4223
+ }
4224
+
4225
+ console.log(`[Browser-Only] ✓ Associated logs with ${updatedCount} frames in IndexedDB`);
4226
+ }
4227
+ } catch (error) {
4228
+ console.error('[Browser-Only] Failed to associate logs with frames:', error);
4229
+ }
3852
4230
  } else {
3853
4231
  // No logs were captured during the session
3854
4232
  console.warn('[Recording] No console logs were captured during this session');
@@ -3866,10 +4244,11 @@ async function handleFrameCaptureComplete(sessionData) {
3866
4244
  priority: 0
3867
4245
  });
3868
4246
  }
3869
-
4247
+
3870
4248
  // Save screen interactions to database
3871
- if (screenInteractions.length > 0) {
3872
- console.log('Saving', screenInteractions.length, 'screen interactions to database');
4249
+ // Skip server communication in browser-only mode
4250
+ if (screenInteractions.length > 0 && serverMode !== 'browser-only') {
4251
+ console.log(`[Server Mode] Saving ${screenInteractions.length} screen interactions to database`);
3873
4252
 
3874
4253
  // Use comprehensive error handling for screen interactions save
3875
4254
  const saveScreenInteractions = async (data, context) => {
@@ -3935,8 +4314,11 @@ async function handleFrameCaptureComplete(sessionData) {
3935
4314
  } else {
3936
4315
  console.error('[ErrorHandling] Failed to save screen interactions after comprehensive error handling');
3937
4316
  }
4317
+ } else if (screenInteractions.length > 0 && serverMode === 'browser-only') {
4318
+ // Browser-only mode: screen interactions not needed
4319
+ console.log(`[Browser-Only] ✓ Skipping screen interactions save (browser-only mode)`);
3938
4320
  }
3939
-
4321
+
3940
4322
  // Stop session via session manager if available
3941
4323
  if (sessionManager && currentSession?.sessionId === sessionData.sessionId && currentOwnerId) {
3942
4324
  try {
@@ -3983,15 +4365,19 @@ async function handleFrameCaptureComplete(sessionData) {
3983
4365
  priority: 2
3984
4366
  });
3985
4367
 
3986
- // Notify popup if open
3987
- chrome.runtime.sendMessage({
3988
- action: 'frameSessionComplete',
3989
- sessionId: sessionData.sessionId,
3990
- sessionName: currentSession?.settings?.sessionName || null,
3991
- totalFrames: sessionData.totalFrames,
3992
- duration: sessionData.duration,
3993
- serverPort: recordingServerPort
3994
- }).catch(() => {});
4368
+ // Notify popup if open (but NOT for browser-only recordings - they're in IndexedDB)
4369
+ if (serverMode !== 'browser-only') {
4370
+ chrome.runtime.sendMessage({
4371
+ action: 'frameSessionComplete',
4372
+ sessionId: sessionData.sessionId,
4373
+ sessionName: currentSession?.settings?.sessionName || null,
4374
+ totalFrames: sessionData.totalFrames,
4375
+ duration: sessionData.duration,
4376
+ serverPort: recordingServerPort
4377
+ }).catch(() => {});
4378
+ } else {
4379
+ console.log('[Browser-Only] Skipping frameSessionComplete message - recording is in IndexedDB');
4380
+ }
3995
4381
 
3996
4382
  // Notify content script to hide recording indicator
3997
4383
  if (tabIdToNotify) {