@dynamicu/chromedebug-mcp 2.6.6 → 2.7.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.
- package/CLAUDE.md +1 -1
- package/README.md +1 -1
- package/chrome-extension/activation-manager.js +18 -4
- package/chrome-extension/background.js +1044 -552
- package/chrome-extension/browser-recording-manager.js +256 -0
- package/chrome-extension/chrome-debug-logger.js +168 -0
- package/chrome-extension/console-interception-library.js +430 -0
- package/chrome-extension/content.css +16 -16
- package/chrome-extension/content.js +617 -215
- package/chrome-extension/data-buffer.js +206 -17
- package/chrome-extension/extension-config.js +1 -1
- package/chrome-extension/frame-capture.js +52 -15
- package/chrome-extension/license-helper.js +26 -0
- package/chrome-extension/manifest.free.json +3 -6
- package/chrome-extension/options.js +1 -1
- package/chrome-extension/popup.html +315 -181
- package/chrome-extension/popup.js +673 -526
- package/chrome-extension/pro/enhanced-capture.js +406 -0
- package/chrome-extension/pro/frame-editor.html +410 -0
- package/chrome-extension/pro/frame-editor.js +1496 -0
- package/chrome-extension/pro/function-tracker.js +843 -0
- package/chrome-extension/pro/jszip.min.js +13 -0
- package/config/chromedebug-config.json +101 -0
- package/dist/chromedebug-extension-free.zip +0 -0
- package/package.json +3 -1
- package/scripts/package-pro-extension.js +1 -1
- package/scripts/webpack.config.free.cjs +11 -8
- package/scripts/webpack.config.pro.cjs +5 -0
- package/src/chrome-controller.js +7 -7
- package/src/cli.js +2 -2
- package/src/database.js +61 -9
- package/src/http-server.js +3 -2
- package/src/index.js +9 -6
- package/src/mcp/server.js +2 -2
- package/src/services/process-manager.js +10 -6
- package/src/services/process-tracker.js +10 -5
- package/src/services/profile-manager.js +17 -2
- package/src/validation/schemas.js +36 -6
- package/src/index-direct.js +0 -157
- package/src/index-modular.js +0 -219
- package/src/index-monolithic-backup.js +0 -2230
- package/src/legacy/chrome-controller-old.js +0 -1406
- package/src/legacy/index-express.js +0 -625
- package/src/legacy/index-old.js +0 -977
- package/src/legacy/routes.js +0 -260
- package/src/legacy/shared-storage.js +0 -101
|
@@ -8,12 +8,129 @@ importScripts('pako.min.js');
|
|
|
8
8
|
importScripts('data-buffer.js');
|
|
9
9
|
importScripts('upload-manager.js');
|
|
10
10
|
importScripts('chrome-session-manager.js');
|
|
11
|
-
importScripts('
|
|
12
|
-
importScripts('
|
|
11
|
+
importScripts('console-interception-library.js'); // Shared console interception library
|
|
12
|
+
importScripts('browser-recording-manager.js');
|
|
13
13
|
|
|
14
14
|
const CONFIG_PORTS = CHROMEDEBUG_CONFIG.ports;
|
|
15
15
|
const DISCOVERY_TIMEOUT = CHROMEDEBUG_CONFIG.discoveryTimeout;
|
|
16
16
|
|
|
17
|
+
// ============================================================
|
|
18
|
+
// FREE TIER USAGE TRACKING (Local Storage - No Firebase)
|
|
19
|
+
// ============================================================
|
|
20
|
+
// Daily limit: 5 recordings per day
|
|
21
|
+
// Resets at midnight local time
|
|
22
|
+
// Tracks both workflow and screen recordings
|
|
23
|
+
// ============================================================
|
|
24
|
+
|
|
25
|
+
const FREE_TIER_DAILY_LIMIT = 5;
|
|
26
|
+
const USAGE_STORAGE_KEY = 'chromedebug_daily_usage';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if this is the PRO version
|
|
30
|
+
*/
|
|
31
|
+
function isProVersion() {
|
|
32
|
+
const manifest = chrome.runtime.getManifest();
|
|
33
|
+
return manifest.name.includes('PRO');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get today's date string in local timezone (YYYY-MM-DD)
|
|
38
|
+
*/
|
|
39
|
+
function getTodayDateString() {
|
|
40
|
+
const now = new Date();
|
|
41
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get current usage for today
|
|
46
|
+
* @returns {Promise<{count: number, date: string, limit: number}>}
|
|
47
|
+
*/
|
|
48
|
+
async function getUsageToday() {
|
|
49
|
+
// PRO version: unlimited
|
|
50
|
+
if (isProVersion()) {
|
|
51
|
+
return { count: 0, date: getTodayDateString(), limit: Infinity, isPro: true };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const result = await chrome.storage.local.get(USAGE_STORAGE_KEY);
|
|
55
|
+
const usage = result[USAGE_STORAGE_KEY] || { count: 0, date: '' };
|
|
56
|
+
const today = getTodayDateString();
|
|
57
|
+
|
|
58
|
+
// Reset if new day
|
|
59
|
+
if (usage.date !== today) {
|
|
60
|
+
return { count: 0, date: today, limit: FREE_TIER_DAILY_LIMIT };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { count: usage.count, date: today, limit: FREE_TIER_DAILY_LIMIT };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if user can start a new recording
|
|
68
|
+
* @returns {Promise<{allowed: boolean, count: number, limit: number, message?: string}>}
|
|
69
|
+
*/
|
|
70
|
+
async function checkUsageBeforeRecording() {
|
|
71
|
+
const usage = await getUsageToday();
|
|
72
|
+
|
|
73
|
+
if (usage.isPro) {
|
|
74
|
+
return { allowed: true, count: 0, limit: Infinity, isPro: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (usage.count >= usage.limit) {
|
|
78
|
+
return {
|
|
79
|
+
allowed: false,
|
|
80
|
+
count: usage.count,
|
|
81
|
+
limit: usage.limit,
|
|
82
|
+
message: `Daily limit reached (${usage.count}/${usage.limit}). Upgrade to Pro for unlimited recordings.`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { allowed: true, count: usage.count, limit: usage.limit };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Increment usage count after successful recording
|
|
91
|
+
* @returns {Promise<{success: boolean, count: number, limit: number}>}
|
|
92
|
+
*/
|
|
93
|
+
async function incrementUsageAfterRecording() {
|
|
94
|
+
// PRO version: skip tracking
|
|
95
|
+
if (isProVersion()) {
|
|
96
|
+
console.log('[Usage] PRO version - skipping usage tracking');
|
|
97
|
+
return { success: true, count: 0, limit: Infinity, isPro: true };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const today = getTodayDateString();
|
|
101
|
+
const result = await chrome.storage.local.get(USAGE_STORAGE_KEY);
|
|
102
|
+
const usage = result[USAGE_STORAGE_KEY] || { count: 0, date: '' };
|
|
103
|
+
|
|
104
|
+
// Reset if new day, then increment
|
|
105
|
+
const newCount = (usage.date === today) ? usage.count + 1 : 1;
|
|
106
|
+
|
|
107
|
+
await chrome.storage.local.set({
|
|
108
|
+
[USAGE_STORAGE_KEY]: { count: newCount, date: today }
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
console.log(`[Usage] Recording tracked: ${newCount}/${FREE_TIER_DAILY_LIMIT} today`);
|
|
112
|
+
|
|
113
|
+
return { success: true, count: newCount, limit: FREE_TIER_DAILY_LIMIT };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Console interception configuration for screen recording
|
|
117
|
+
const SCREEN_RECORDING_CONSOLE_CONFIG = {
|
|
118
|
+
overrideFlagName: '__chromePilotConsoleOverridden',
|
|
119
|
+
originalConsoleName: '__chromePilotOriginalConsole',
|
|
120
|
+
relayFlagName: '__chromePilotConsoleRelay',
|
|
121
|
+
messageType: 'chrome-debug-console-log',
|
|
122
|
+
backgroundAction: 'consoleLog'
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Console interception configuration for workflow recording
|
|
126
|
+
const WORKFLOW_RECORDING_CONSOLE_CONFIG = {
|
|
127
|
+
overrideFlagName: '__chromePilotWorkflowConsoleOverridden',
|
|
128
|
+
originalConsoleName: '__chromePilotWorkflowOriginalConsole',
|
|
129
|
+
relayFlagName: '__chromePilotWorkflowConsoleRelay',
|
|
130
|
+
messageType: 'chrome-debug-workflow-console-log',
|
|
131
|
+
backgroundAction: 'workflowConsoleLog'
|
|
132
|
+
};
|
|
133
|
+
|
|
17
134
|
// Frame queue for handling validation race conditions
|
|
18
135
|
// v2.1.2: Prevents frame loss when lease renewal hasn't propagated yet
|
|
19
136
|
const pendingFrameQueue = new Map(); // sessionId -> array of frame batches
|
|
@@ -35,12 +152,18 @@ class LogStreamer {
|
|
|
35
152
|
}
|
|
36
153
|
|
|
37
154
|
async init() {
|
|
155
|
+
// Skip initialization in browser-only mode
|
|
156
|
+
if (serverMode === 'browser-only') {
|
|
157
|
+
console.log('[LogStreamer] Skipping initialization - browser-only mode');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
38
161
|
// Test server availability
|
|
39
162
|
await this.checkServerAvailability();
|
|
40
|
-
|
|
163
|
+
|
|
41
164
|
// Start periodic streaming
|
|
42
165
|
this.startPeriodicStreaming();
|
|
43
|
-
|
|
166
|
+
|
|
44
167
|
console.log('[LogStreamer] Initialized with server availability:', this.serverAvailable);
|
|
45
168
|
}
|
|
46
169
|
|
|
@@ -78,10 +201,16 @@ class LogStreamer {
|
|
|
78
201
|
async addLog(log, sessionId) {
|
|
79
202
|
log.sequence = ++this.sequenceNumber;
|
|
80
203
|
log.timestamp = Date.now();
|
|
81
|
-
|
|
204
|
+
|
|
82
205
|
this.currentSessionId = sessionId;
|
|
206
|
+
|
|
207
|
+
// In browser-only mode, skip server streaming (logs stored in LogBuffer)
|
|
208
|
+
if (serverMode === 'browser-only') {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
83
212
|
this.buffer.push(log);
|
|
84
|
-
|
|
213
|
+
|
|
85
214
|
// If buffer is getting full, force immediate stream
|
|
86
215
|
if (this.buffer.length >= this.maxBufferSize) {
|
|
87
216
|
await this.streamLogs();
|
|
@@ -89,6 +218,9 @@ class LogStreamer {
|
|
|
89
218
|
}
|
|
90
219
|
|
|
91
220
|
async streamLogs() {
|
|
221
|
+
// Skip streaming in browser-only mode
|
|
222
|
+
if (serverMode === 'browser-only') return;
|
|
223
|
+
|
|
92
224
|
if (this.buffer.length === 0 || !this.currentSessionId) return;
|
|
93
225
|
|
|
94
226
|
// Check if we have waited at least 100ms since last frame capture
|
|
@@ -465,11 +597,169 @@ class LogTabBuffer {
|
|
|
465
597
|
}
|
|
466
598
|
}
|
|
467
599
|
|
|
600
|
+
/**
|
|
601
|
+
* WorkflowLogBuffer - Race-safe log buffer for workflow recording
|
|
602
|
+
*
|
|
603
|
+
* Mirrors LogTabBuffer but uses workflow-specific storage keys (`workflow_${tabId}`).
|
|
604
|
+
* This prevents race conditions when multiple logs arrive rapidly during workflow recording.
|
|
605
|
+
*
|
|
606
|
+
* Technical Debt Note: This class duplicates LogTabBuffer logic. Future refactoring
|
|
607
|
+
* could make LogTabBuffer accept a configurable storage key prefix to eliminate duplication.
|
|
608
|
+
*/
|
|
609
|
+
class WorkflowLogBuffer {
|
|
610
|
+
constructor(tabId) {
|
|
611
|
+
this.tabId = tabId;
|
|
612
|
+
this.buffer = [];
|
|
613
|
+
this.processing = false;
|
|
614
|
+
this.processingQueue = [];
|
|
615
|
+
this.totalLogsAdded = 0;
|
|
616
|
+
this.totalLogsFlushed = 0;
|
|
617
|
+
this.lastFlushTime = 0;
|
|
618
|
+
this.storageKey = `workflow_${tabId}`;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Add log to buffer with queuing to prevent race conditions
|
|
622
|
+
async addLog(logEntry) {
|
|
623
|
+
return new Promise((resolve, reject) => {
|
|
624
|
+
this.processingQueue.push({
|
|
625
|
+
type: 'add',
|
|
626
|
+
logEntry,
|
|
627
|
+
resolve,
|
|
628
|
+
reject
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// Process queue if not already processing
|
|
632
|
+
if (!this.processing) {
|
|
633
|
+
this.processQueue();
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Flush buffer to Chrome storage
|
|
639
|
+
async flush() {
|
|
640
|
+
return new Promise((resolve, reject) => {
|
|
641
|
+
this.processingQueue.push({
|
|
642
|
+
type: 'flush',
|
|
643
|
+
resolve,
|
|
644
|
+
reject
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// Process queue if not already processing
|
|
648
|
+
if (!this.processing) {
|
|
649
|
+
this.processQueue();
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Sequential processing of queue to prevent race conditions
|
|
655
|
+
async processQueue() {
|
|
656
|
+
if (this.processing) {
|
|
657
|
+
return; // Already processing
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
this.processing = true;
|
|
661
|
+
|
|
662
|
+
try {
|
|
663
|
+
while (this.processingQueue.length > 0) {
|
|
664
|
+
const operation = this.processingQueue.shift();
|
|
665
|
+
|
|
666
|
+
try {
|
|
667
|
+
if (operation.type === 'add') {
|
|
668
|
+
this.buffer.push(operation.logEntry);
|
|
669
|
+
this.totalLogsAdded++;
|
|
670
|
+
|
|
671
|
+
// Auto-flush if buffer gets too large
|
|
672
|
+
if (this.buffer.length >= 100) {
|
|
673
|
+
await this.performFlush();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
operation.resolve();
|
|
677
|
+
} else if (operation.type === 'flush') {
|
|
678
|
+
await this.performFlush();
|
|
679
|
+
operation.resolve();
|
|
680
|
+
}
|
|
681
|
+
} catch (error) {
|
|
682
|
+
operation.reject(error);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
} finally {
|
|
686
|
+
this.processing = false;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Actual flush operation to Chrome storage
|
|
691
|
+
async performFlush() {
|
|
692
|
+
if (this.buffer.length === 0) {
|
|
693
|
+
return; // Nothing to flush
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
try {
|
|
697
|
+
// Read current logs from storage
|
|
698
|
+
const result = await chrome.storage.session.get(this.storageKey);
|
|
699
|
+
const existingLogs = result[this.storageKey] || [];
|
|
700
|
+
|
|
701
|
+
// Append new logs
|
|
702
|
+
const updatedLogs = [...existingLogs, ...this.buffer];
|
|
703
|
+
|
|
704
|
+
// Check storage size limit
|
|
705
|
+
const estimatedSize = JSON.stringify(updatedLogs).length;
|
|
706
|
+
if (estimatedSize > 500000) { // ~500KB limit
|
|
707
|
+
const beforeCount = updatedLogs.length;
|
|
708
|
+
const retentionPercentage = 0.95;
|
|
709
|
+
const afterCount = Math.floor(updatedLogs.length * retentionPercentage);
|
|
710
|
+
console.warn(`[WorkflowLogBuffer] Storage size limit approached for tab ${this.tabId}, truncating from ${beforeCount} to ${afterCount} logs`);
|
|
711
|
+
const recentLogs = updatedLogs.slice(-afterCount);
|
|
712
|
+
await chrome.storage.session.set({ [this.storageKey]: recentLogs });
|
|
713
|
+
} else {
|
|
714
|
+
await chrome.storage.session.set({ [this.storageKey]: updatedLogs });
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
this.totalLogsFlushed += this.buffer.length;
|
|
718
|
+
console.log(`[WorkflowLogBuffer] Flushed ${this.buffer.length} logs for tab ${this.tabId} (total: ${this.totalLogsFlushed})`);
|
|
719
|
+
|
|
720
|
+
// Clear the buffer
|
|
721
|
+
this.buffer = [];
|
|
722
|
+
this.lastFlushTime = Date.now();
|
|
723
|
+
|
|
724
|
+
} catch (error) {
|
|
725
|
+
console.error(`[WorkflowLogBuffer] Failed to flush logs for tab ${this.tabId}:`, error);
|
|
726
|
+
throw error;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Get buffer statistics
|
|
731
|
+
getStats() {
|
|
732
|
+
return {
|
|
733
|
+
bufferedLogs: this.buffer.length,
|
|
734
|
+
totalAdded: this.totalLogsAdded,
|
|
735
|
+
totalFlushed: this.totalLogsFlushed,
|
|
736
|
+
processing: this.processing,
|
|
737
|
+
queueLength: this.processingQueue.length,
|
|
738
|
+
lastFlushTime: this.lastFlushTime
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Clean up buffer when recording stops
|
|
743
|
+
async cleanup() {
|
|
744
|
+
try {
|
|
745
|
+
// Flush any remaining logs
|
|
746
|
+
if (this.buffer.length > 0) {
|
|
747
|
+
await this.performFlush();
|
|
748
|
+
}
|
|
749
|
+
console.log(`[WorkflowLogBuffer] Cleaned up buffer for tab ${this.tabId}`);
|
|
750
|
+
} catch (error) {
|
|
751
|
+
console.error(`[WorkflowLogBuffer] Error during cleanup for tab ${this.tabId}:`, error);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
468
756
|
// Initialize data buffer and upload manager
|
|
469
757
|
let dataBuffer = null;
|
|
470
758
|
let uploadManager = null;
|
|
471
759
|
let logStreamer = null;
|
|
472
760
|
let logBuffer = null;
|
|
761
|
+
let browserRecordingManager = null;
|
|
762
|
+
let serverMode = 'unknown'; // 'server', 'browser-only', or 'unknown'
|
|
473
763
|
|
|
474
764
|
// Error handling state and configuration
|
|
475
765
|
// IMPORTANT: These must be declared before initializeServices()
|
|
@@ -566,6 +856,15 @@ async function initializeServices() {
|
|
|
566
856
|
logBuffer = null;
|
|
567
857
|
}
|
|
568
858
|
|
|
859
|
+
// Initialize browser recording manager for browser-only mode
|
|
860
|
+
try {
|
|
861
|
+
browserRecordingManager = new BrowserRecordingManager(dataBuffer);
|
|
862
|
+
console.log('[Background] Browser recording manager initialized');
|
|
863
|
+
} catch (recordingErr) {
|
|
864
|
+
console.error('[Background] Failed to initialize browser recording manager:', recordingErr);
|
|
865
|
+
browserRecordingManager = null;
|
|
866
|
+
}
|
|
867
|
+
|
|
569
868
|
// Initialize session manager with recovery
|
|
570
869
|
try {
|
|
571
870
|
if (!sessionManager) {
|
|
@@ -628,10 +927,14 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
628
927
|
'restoreFromPoint',
|
|
629
928
|
'getWebSocketPort',
|
|
630
929
|
'startRecording',
|
|
930
|
+
'stopRecording',
|
|
931
|
+
'openFrameEditor',
|
|
631
932
|
'checkConnection',
|
|
632
933
|
'workflowConsoleLog', // v2.0.8: handle workflow console logs
|
|
633
934
|
'workflowAction', // v2.0.8: handle workflow actions
|
|
634
|
-
'renewLease'
|
|
935
|
+
'renewLease', // v2.1.2: session manager lease renewal for frame capture
|
|
936
|
+
'frameSessionComplete', // Frame recording completion notification
|
|
937
|
+
'recordingStopped' // Recording stopped notification for popup refresh
|
|
635
938
|
];
|
|
636
939
|
|
|
637
940
|
if (handledByOtherListener.includes(messageType)) {
|
|
@@ -677,7 +980,27 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
677
980
|
sendResponse({ status: 'error', message: error.message });
|
|
678
981
|
}
|
|
679
982
|
break;
|
|
680
|
-
|
|
983
|
+
|
|
984
|
+
case 'getUsage':
|
|
985
|
+
// Get current usage count for popup display
|
|
986
|
+
getUsageToday().then(usage => {
|
|
987
|
+
sendResponse({ success: true, ...usage });
|
|
988
|
+
}).catch(error => {
|
|
989
|
+
console.error('[Usage] Failed to get usage:', error);
|
|
990
|
+
sendResponse({ success: false, error: error.message });
|
|
991
|
+
});
|
|
992
|
+
return true; // Async response
|
|
993
|
+
|
|
994
|
+
case 'checkUsageLimit':
|
|
995
|
+
// Check if user can start a new recording
|
|
996
|
+
checkUsageBeforeRecording().then(result => {
|
|
997
|
+
sendResponse(result);
|
|
998
|
+
}).catch(error => {
|
|
999
|
+
console.error('[Usage] Failed to check usage limit:', error);
|
|
1000
|
+
sendResponse({ allowed: true, error: error.message }); // Fail-open
|
|
1001
|
+
});
|
|
1002
|
+
return true; // Async response
|
|
1003
|
+
|
|
681
1004
|
case 'UPLOAD_BATCH':
|
|
682
1005
|
// Add events to the background's DataBuffer for upload
|
|
683
1006
|
if (message.events && message.recordingId) {
|
|
@@ -909,105 +1232,127 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
909
1232
|
}
|
|
910
1233
|
break;
|
|
911
1234
|
|
|
912
|
-
|
|
913
|
-
//
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
})
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
if (!response.ok) {
|
|
931
|
-
console.log('[License Auto-Activation] No activation file found - using normal license flow');
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
const activationData = await response.json();
|
|
936
|
-
console.log('[License Auto-Activation] Found activation file:', {
|
|
937
|
-
tier: activationData.tier,
|
|
938
|
-
activated_at: activationData.activated_at
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
// Check if license is already cached and valid
|
|
942
|
-
const cached = await chrome.storage.local.get('chromedebug_license_cache');
|
|
943
|
-
if (cached.chromedebug_license_cache?.valid && cached.chromedebug_license_cache?.tier === 'pro') {
|
|
944
|
-
console.log('[License Auto-Activation] License already activated and cached');
|
|
945
|
-
return;
|
|
946
|
-
}
|
|
1235
|
+
case 'getBrowserRecordings':
|
|
1236
|
+
// Get all browser-only recordings from IndexedDB
|
|
1237
|
+
console.log('[Background] getBrowserRecordings - Manager exists:', !!browserRecordingManager);
|
|
1238
|
+
if (browserRecordingManager) {
|
|
1239
|
+
browserRecordingManager.listRecordings().then(recordings => {
|
|
1240
|
+
console.log('[Background] Recordings retrieved from manager:', recordings);
|
|
1241
|
+
sendResponse(recordings);
|
|
1242
|
+
}).catch(error => {
|
|
1243
|
+
console.error('[Background] Failed to get browser recordings:', error);
|
|
1244
|
+
sendResponse([]);
|
|
1245
|
+
});
|
|
1246
|
+
return true; // Async response
|
|
1247
|
+
} else {
|
|
1248
|
+
console.warn('[Background] browserRecordingManager not initialized');
|
|
1249
|
+
sendResponse([]);
|
|
1250
|
+
}
|
|
1251
|
+
break;
|
|
947
1252
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1253
|
+
case 'exportBrowserRecording':
|
|
1254
|
+
// Export a browser-only recording
|
|
1255
|
+
if (browserRecordingManager && message.sessionId) {
|
|
1256
|
+
browserRecordingManager.exportRecording(message.sessionId).then(result => {
|
|
1257
|
+
sendResponse({ success: true, data: result });
|
|
1258
|
+
}).catch(error => {
|
|
1259
|
+
console.error('[Background] Failed to export recording:', error);
|
|
1260
|
+
sendResponse({ success: false, error: error.message });
|
|
1261
|
+
});
|
|
1262
|
+
return true; // Async response
|
|
1263
|
+
} else {
|
|
1264
|
+
sendResponse({ success: false, error: 'Manager not available or missing sessionId' });
|
|
1265
|
+
}
|
|
1266
|
+
break;
|
|
951
1267
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1268
|
+
case 'deleteBrowserRecording':
|
|
1269
|
+
// Delete a browser-only recording
|
|
1270
|
+
if (browserRecordingManager && message.sessionId) {
|
|
1271
|
+
browserRecordingManager.deleteRecording(message.sessionId).then(() => {
|
|
1272
|
+
sendResponse({ success: true });
|
|
1273
|
+
}).catch(error => {
|
|
1274
|
+
console.error('[Background] Failed to delete recording:', error);
|
|
1275
|
+
sendResponse({ success: false, error: error.message });
|
|
1276
|
+
});
|
|
1277
|
+
return true; // Async response
|
|
1278
|
+
} else {
|
|
1279
|
+
sendResponse({ success: false, error: 'Manager not available or missing sessionId' });
|
|
1280
|
+
}
|
|
1281
|
+
break;
|
|
959
1282
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1283
|
+
case 'getBrowserRecordingFrames':
|
|
1284
|
+
// Get frames for a browser-only recording
|
|
1285
|
+
if (dataBuffer && message.sessionId) {
|
|
1286
|
+
dataBuffer.getBrowserFrames(message.sessionId).then(frames => {
|
|
1287
|
+
console.log(`[DEBUG] getBrowserRecordingFrames: Retrieved ${frames.length} frames from IndexedDB`);
|
|
1288
|
+
|
|
1289
|
+
// Debug first frame to see structure
|
|
1290
|
+
if (frames.length > 0) {
|
|
1291
|
+
const firstFrame = frames[0];
|
|
1292
|
+
const debugInfo = {
|
|
1293
|
+
hasScreenshot: !!firstFrame.screenshot,
|
|
1294
|
+
screenshotPrefix: firstFrame.screenshot ? firstFrame.screenshot.substring(0, 30) : 'N/A',
|
|
1295
|
+
hasLogs: !!firstFrame.logs,
|
|
1296
|
+
logsCount: firstFrame.logs ? firstFrame.logs.length : 0,
|
|
1297
|
+
timestamp: firstFrame.timestamp,
|
|
1298
|
+
frameIndex: firstFrame.frameIndex,
|
|
1299
|
+
allKeys: Object.keys(firstFrame)
|
|
1300
|
+
};
|
|
1301
|
+
console.log('[DEBUG] First frame structure:', JSON.stringify(debugInfo, null, 2));
|
|
1302
|
+
}
|
|
969
1303
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1304
|
+
// Transform frames to match frame editor's expected structure
|
|
1305
|
+
// Frame editor expects 'imageData' property, but we store as 'screenshot'
|
|
1306
|
+
const transformedFrames = frames.map(frame => ({
|
|
1307
|
+
...frame,
|
|
1308
|
+
imageData: frame.screenshot // Rename screenshot to imageData
|
|
1309
|
+
}));
|
|
973
1310
|
|
|
974
|
-
|
|
975
|
-
|
|
1311
|
+
const transformDebug = transformedFrames.length > 0 ? {
|
|
1312
|
+
hasImageData: !!transformedFrames[0].imageData,
|
|
1313
|
+
imageDataPrefix: transformedFrames[0].imageData ? transformedFrames[0].imageData.substring(0, 30) : 'N/A',
|
|
1314
|
+
hasLogs: !!transformedFrames[0].logs,
|
|
1315
|
+
logsCount: transformedFrames[0].logs ? transformedFrames[0].logs.length : 0,
|
|
1316
|
+
allKeys: Object.keys(transformedFrames[0])
|
|
1317
|
+
} : 'No frames';
|
|
1318
|
+
console.log('[DEBUG] Transformed first frame:', JSON.stringify(transformDebug, null, 2));
|
|
976
1319
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
}
|
|
1320
|
+
sendResponse({ success: true, frames: transformedFrames });
|
|
1321
|
+
}).catch(error => {
|
|
1322
|
+
console.error('[Background] Failed to get browser recording frames:', error);
|
|
1323
|
+
sendResponse({ success: false, error: error.message });
|
|
1324
|
+
});
|
|
1325
|
+
return true; // Async response
|
|
1326
|
+
} else {
|
|
1327
|
+
sendResponse({ success: false, error: 'DataBuffer not available or missing sessionId' });
|
|
1328
|
+
}
|
|
1329
|
+
break;
|
|
986
1330
|
|
|
987
|
-
|
|
988
|
-
|
|
1331
|
+
case 'getQuotaInfo':
|
|
1332
|
+
// Get storage quota information
|
|
1333
|
+
getQuotaInfo().then(info => {
|
|
1334
|
+
sendResponse(info);
|
|
1335
|
+
}).catch(error => {
|
|
1336
|
+
console.error('[Background] Failed to get quota info:', error);
|
|
1337
|
+
sendResponse({ supported: false });
|
|
989
1338
|
});
|
|
1339
|
+
return true; // Async response
|
|
990
1340
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
console.
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
} catch (error) {
|
|
998
|
-
// Don't block extension startup on activation errors
|
|
999
|
-
console.error('[License Auto-Activation] Failed to auto-activate license:', error);
|
|
1000
|
-
console.log('[License Auto-Activation] Extension will continue with normal license flow');
|
|
1341
|
+
default:
|
|
1342
|
+
// Don't warn or respond for messages that should be handled by other listener (v2.0.6)
|
|
1343
|
+
// This prevents intercepting workflow recording messages
|
|
1344
|
+
console.log('[Background v2.0.6] Message not handled by this listener:', messageType);
|
|
1345
|
+
// Don't send response - let other listener handle it
|
|
1346
|
+
return;
|
|
1001
1347
|
}
|
|
1002
|
-
}
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
// License checking removed - using separate Chrome Web Store listings for FREE and PRO
|
|
1003
1351
|
|
|
1004
1352
|
// Session recovery on startup - handle any stuck recordings from previous session
|
|
1005
1353
|
chrome.runtime.onStartup.addListener(async () => {
|
|
1006
1354
|
console.log('[Background] Extension startup detected - checking for stuck sessions');
|
|
1007
1355
|
|
|
1008
|
-
// Check for PRO license auto-activation first
|
|
1009
|
-
await checkAndActivateLicense();
|
|
1010
|
-
|
|
1011
1356
|
if (sessionManager) {
|
|
1012
1357
|
try {
|
|
1013
1358
|
// Use existing recovery method from session manager
|
|
@@ -1021,12 +1366,9 @@ chrome.runtime.onStartup.addListener(async () => {
|
|
|
1021
1366
|
}
|
|
1022
1367
|
});
|
|
1023
1368
|
|
|
1024
|
-
// Extension install/update
|
|
1369
|
+
// Extension install/update
|
|
1025
1370
|
chrome.runtime.onInstalled.addListener(async (details) => {
|
|
1026
1371
|
console.log('[Background] Extension installed/updated:', details.reason);
|
|
1027
|
-
|
|
1028
|
-
// Check for PRO license auto-activation on install/update
|
|
1029
|
-
await checkAndActivateLicense();
|
|
1030
1372
|
});
|
|
1031
1373
|
|
|
1032
1374
|
// Listen for tab updates to handle restore points
|
|
@@ -1124,6 +1466,43 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|
|
1124
1466
|
}
|
|
1125
1467
|
});
|
|
1126
1468
|
|
|
1469
|
+
// Defensive cleanup when tabs are closed
|
|
1470
|
+
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
|
|
1471
|
+
// Clean up any workflow recording state for closed tabs
|
|
1472
|
+
if (workflowRecordingTabs.has(tabId)) {
|
|
1473
|
+
console.log(`[Workflow] Tab ${tabId} closed during recording, cleaning up`);
|
|
1474
|
+
workflowRecordingTabs.delete(tabId);
|
|
1475
|
+
workflowIncludeLogs.delete(tabId);
|
|
1476
|
+
workflowScreenshotSettings.delete(tabId);
|
|
1477
|
+
workflowSessionNames.delete(tabId);
|
|
1478
|
+
|
|
1479
|
+
// Clean up WorkflowLogBuffer
|
|
1480
|
+
const buffer = workflowLogBuffers.get(tabId);
|
|
1481
|
+
if (buffer) {
|
|
1482
|
+
buffer.cleanup().then(() => {
|
|
1483
|
+
workflowLogBuffers.delete(tabId);
|
|
1484
|
+
console.log(`[Workflow] Cleaned up WorkflowLogBuffer for closed tab ${tabId}`);
|
|
1485
|
+
}).catch(err => {
|
|
1486
|
+
console.error(`[Workflow] Error cleaning up buffer for closed tab ${tabId}:`, err);
|
|
1487
|
+
workflowLogBuffers.delete(tabId);
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// Remove session storage
|
|
1492
|
+
chrome.storage.session.remove(`workflow_${tabId}`).catch(err => {
|
|
1493
|
+
console.error(`[Workflow] Error removing session storage for closed tab ${tabId}:`, err);
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
// Notify popup that recording has stopped (tab was closed)
|
|
1497
|
+
chrome.storage.local.set({
|
|
1498
|
+
workflowRecording: false,
|
|
1499
|
+
workflowStartTime: null
|
|
1500
|
+
}).catch(err => {
|
|
1501
|
+
console.error(`[Workflow] Error updating storage for closed tab ${tabId}:`, err);
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1127
1506
|
// Session Manager for robust recording state management
|
|
1128
1507
|
let sessionManager = null;
|
|
1129
1508
|
let currentSession = null; // Cache current session state
|
|
@@ -1266,7 +1645,7 @@ let workflowRecordingTabs = new Map(); // Map of tabId to recording state
|
|
|
1266
1645
|
let workflowIncludeLogs = new Map(); // Map of tabId to includeLogsInExport setting
|
|
1267
1646
|
let workflowScreenshotSettings = new Map(); // Map of tabId to screenshot settings
|
|
1268
1647
|
let workflowSessionNames = new Map(); // Map of tabId to session name
|
|
1269
|
-
let
|
|
1648
|
+
let workflowLogBuffers = new Map(); // Map of tabId to WorkflowLogBuffer instance
|
|
1270
1649
|
|
|
1271
1650
|
//=============================================================================
|
|
1272
1651
|
// COMPREHENSIVE ERROR HANDLING SYSTEM
|
|
@@ -1742,7 +2121,7 @@ async function downloadBackupData() {
|
|
|
1742
2121
|
|
|
1743
2122
|
chrome.downloads.download({
|
|
1744
2123
|
url: url,
|
|
1745
|
-
filename: `chrome-
|
|
2124
|
+
filename: `chrome-debug-backup-${Date.now()}.json`
|
|
1746
2125
|
});
|
|
1747
2126
|
|
|
1748
2127
|
console.log('[ErrorHandling] Backup data download initiated');
|
|
@@ -1882,8 +2261,13 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
|
1882
2261
|
}
|
|
1883
2262
|
|
|
1884
2263
|
if (request.action === 'stopRecording') {
|
|
1885
|
-
stopRecording().then(() => {
|
|
1886
|
-
|
|
2264
|
+
stopRecording().then(async (result) => {
|
|
2265
|
+
// Track usage for FREE tier after successful screen recording
|
|
2266
|
+
if (result.success) {
|
|
2267
|
+
const usageResult = await incrementUsageAfterRecording();
|
|
2268
|
+
result.usage = usageResult; // Include updated usage in response
|
|
2269
|
+
}
|
|
2270
|
+
sendResponse(result); // Pass through { success: true, sessionId: ..., usage: ... }
|
|
1887
2271
|
}).catch((error) => {
|
|
1888
2272
|
console.error('Error stopping recording:', error);
|
|
1889
2273
|
sendResponse({ success: false, error: error.message });
|
|
@@ -1891,6 +2275,34 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
|
1891
2275
|
return true;
|
|
1892
2276
|
}
|
|
1893
2277
|
|
|
2278
|
+
if (request.action === 'openFrameEditor') {
|
|
2279
|
+
// Content scripts can't create tabs, so handle it here
|
|
2280
|
+
const sessionId = request.sessionId;
|
|
2281
|
+
const type = request.type; // 'workflow' or undefined for screen recordings
|
|
2282
|
+
const editorUrl = type === 'workflow'
|
|
2283
|
+
? chrome.runtime.getURL(`pro/frame-editor.html?sessionId=${sessionId}&type=workflow`)
|
|
2284
|
+
: chrome.runtime.getURL(`pro/frame-editor.html?sessionId=${sessionId}`);
|
|
2285
|
+
|
|
2286
|
+
chrome.tabs.create({ url: editorUrl }).then(() => {
|
|
2287
|
+
sendResponse({ success: true });
|
|
2288
|
+
}).catch((error) => {
|
|
2289
|
+
console.error('Error opening frame editor:', error);
|
|
2290
|
+
sendResponse({ success: false, error: error.message });
|
|
2291
|
+
});
|
|
2292
|
+
return true;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
// Get quota information for browser-only mode
|
|
2296
|
+
if (request.action === 'getQuotaInfo') {
|
|
2297
|
+
getQuotaInfo().then((quotaInfo) => {
|
|
2298
|
+
sendResponse({ quotaInfo });
|
|
2299
|
+
}).catch((error) => {
|
|
2300
|
+
console.error('Error getting quota info:', error);
|
|
2301
|
+
sendResponse({ quotaInfo: null, error: error.message });
|
|
2302
|
+
});
|
|
2303
|
+
return true;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
1894
2306
|
// Session manager lease renewal handler
|
|
1895
2307
|
if (request.action === 'renewLease') {
|
|
1896
2308
|
console.log('[Background] RENEW LEASE - Session:', request.sessionId, 'Manager exists:', !!sessionManager);
|
|
@@ -2073,20 +2485,6 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
|
2073
2485
|
return true;
|
|
2074
2486
|
}
|
|
2075
2487
|
|
|
2076
|
-
// License check handler for workflow recording
|
|
2077
|
-
if (request.action === 'checkLicenseForWorkflow') {
|
|
2078
|
-
console.log('[License] popup.js requesting license check for workflow recording');
|
|
2079
|
-
LicenseHelper.checkLicenseBeforeRecording().then((licenseCheck) => {
|
|
2080
|
-
console.log('[License] License check result for popup:', licenseCheck);
|
|
2081
|
-
sendResponse(licenseCheck);
|
|
2082
|
-
}).catch((error) => {
|
|
2083
|
-
console.error('[License] Error checking license:', error);
|
|
2084
|
-
// Fail-open: allow recording on error
|
|
2085
|
-
sendResponse({ allowed: true, error: error.message });
|
|
2086
|
-
});
|
|
2087
|
-
return true;
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
2488
|
// Workflow recording handlers
|
|
2091
2489
|
if (request.action === 'startWorkflowRecording') {
|
|
2092
2490
|
console.log('Starting workflow recording for tab:', request.tabId);
|
|
@@ -2105,9 +2503,17 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
|
2105
2503
|
}
|
|
2106
2504
|
|
|
2107
2505
|
if (request.action === 'stopWorkflowRecording') {
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2506
|
+
const tabId = request.tabId || sender.tab?.id;
|
|
2507
|
+
console.log('Stopping workflow recording for tab:', tabId);
|
|
2508
|
+
// stopWorkflowRecording already returns { success: true, workflow: {...}, savedToServer: bool }
|
|
2509
|
+
// so we pass it through directly - don't double-wrap!
|
|
2510
|
+
stopWorkflowRecording(tabId).then(async (result) => {
|
|
2511
|
+
// Track usage for FREE tier after successful recording
|
|
2512
|
+
if (result.success) {
|
|
2513
|
+
const usageResult = await incrementUsageAfterRecording();
|
|
2514
|
+
result.usage = usageResult; // Include updated usage in response
|
|
2515
|
+
}
|
|
2516
|
+
sendResponse(result);
|
|
2111
2517
|
}).catch((error) => {
|
|
2112
2518
|
console.error('Error stopping workflow recording:', error);
|
|
2113
2519
|
sendResponse({ success: false, error: error.message });
|
|
@@ -2212,18 +2618,19 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
|
2212
2618
|
}
|
|
2213
2619
|
}
|
|
2214
2620
|
|
|
2215
|
-
// Buffer workflow console logs
|
|
2621
|
+
// Buffer workflow console logs using race-safe WorkflowLogBuffer
|
|
2216
2622
|
if (request.action === 'workflowConsoleLog' && sender.tab) {
|
|
2217
2623
|
const tabId = sender.tab.id;
|
|
2218
2624
|
if (workflowRecordingTabs.has(tabId) && workflowIncludeLogs.get(tabId)) {
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2625
|
+
const buffer = workflowLogBuffers.get(tabId);
|
|
2626
|
+
if (buffer) {
|
|
2627
|
+
// Use race-safe buffer to prevent read-modify-write race conditions
|
|
2628
|
+
buffer.addLog(request.log).catch(err => {
|
|
2629
|
+
console.error('[WorkflowLogBuffer] Error adding log:', err);
|
|
2630
|
+
});
|
|
2631
|
+
} else {
|
|
2632
|
+
console.warn(`[Workflow] No buffer found for tab ${tabId}, log dropped`);
|
|
2633
|
+
}
|
|
2227
2634
|
}
|
|
2228
2635
|
}
|
|
2229
2636
|
|
|
@@ -2322,13 +2729,24 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
|
2322
2729
|
|
|
2323
2730
|
// Handle workflow screenshot capture
|
|
2324
2731
|
if (request.action === 'captureWorkflowScreenshot' && sender.tab) {
|
|
2732
|
+
console.log('[SCREENSHOT-DEBUG] background.js - Received captureWorkflowScreenshot message');
|
|
2325
2733
|
const tabId = sender.tab.id;
|
|
2326
2734
|
const settings = request.settings || {};
|
|
2327
|
-
|
|
2735
|
+
console.log('[SCREENSHOT-DEBUG] background.js - tabId:', tabId);
|
|
2736
|
+
console.log('[SCREENSHOT-DEBUG] background.js - settings:', JSON.stringify(settings));
|
|
2737
|
+
|
|
2328
2738
|
captureTabScreenshot(tabId, settings).then((screenshotData) => {
|
|
2739
|
+
console.log('[SCREENSHOT-DEBUG] background.js - captureTabScreenshot succeeded');
|
|
2740
|
+
console.log('[SCREENSHOT-DEBUG] background.js - screenshotData type:', typeof screenshotData);
|
|
2741
|
+
console.log('[SCREENSHOT-DEBUG] background.js - screenshotData length:', screenshotData?.length);
|
|
2742
|
+
console.log('[SCREENSHOT-DEBUG] background.js - screenshotData preview:', screenshotData?.substring(0, 100));
|
|
2743
|
+
console.log('[SCREENSHOT-DEBUG] background.js - Sending response with success: true');
|
|
2329
2744
|
sendResponse({ success: true, screenshotData });
|
|
2330
2745
|
}).catch((error) => {
|
|
2331
|
-
console.error('
|
|
2746
|
+
console.error('[SCREENSHOT-DEBUG] background.js - captureTabScreenshot FAILED');
|
|
2747
|
+
console.error('[SCREENSHOT-DEBUG] background.js - Error:', error);
|
|
2748
|
+
console.error('[SCREENSHOT-DEBUG] background.js - Error message:', error.message);
|
|
2749
|
+
console.error('[SCREENSHOT-DEBUG] background.js - Error stack:', error.stack);
|
|
2332
2750
|
sendResponse({ success: false, error: error.message });
|
|
2333
2751
|
});
|
|
2334
2752
|
return true;
|
|
@@ -2456,14 +2874,34 @@ async function takeStandaloneSnapshot(tabId, note = '') {
|
|
|
2456
2874
|
|
|
2457
2875
|
// Capture screenshot for workflow recording
|
|
2458
2876
|
async function captureTabScreenshot(tabId, settings) {
|
|
2877
|
+
console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - ENTRY');
|
|
2878
|
+
console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - tabId:', tabId);
|
|
2879
|
+
console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - settings:', JSON.stringify(settings));
|
|
2880
|
+
|
|
2459
2881
|
try {
|
|
2460
|
-
|
|
2882
|
+
// Get the tab to find its window
|
|
2883
|
+
console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - Getting tab info...');
|
|
2884
|
+
const tab = await chrome.tabs.get(tabId);
|
|
2885
|
+
const windowId = tab.windowId;
|
|
2886
|
+
console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - windowId:', windowId);
|
|
2887
|
+
console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - tab.url:', tab.url);
|
|
2888
|
+
|
|
2889
|
+
const captureOptions = {
|
|
2461
2890
|
format: settings.format || 'jpeg',
|
|
2462
2891
|
quality: settings.quality || 30
|
|
2463
|
-
}
|
|
2464
|
-
|
|
2892
|
+
};
|
|
2893
|
+
console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - captureOptions:', JSON.stringify(captureOptions));
|
|
2894
|
+
console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - Calling chrome.tabs.captureVisibleTab...');
|
|
2895
|
+
|
|
2896
|
+
const dataUrl = await chrome.tabs.captureVisibleTab(windowId, captureOptions);
|
|
2897
|
+
|
|
2898
|
+
console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - captureVisibleTab SUCCESS');
|
|
2899
|
+
console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - dataUrl length:', dataUrl?.length);
|
|
2900
|
+
console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - dataUrl prefix:', dataUrl?.substring(0, 100));
|
|
2901
|
+
|
|
2465
2902
|
// If resolution is specified, resize the image
|
|
2466
2903
|
if (settings.maxWidth || settings.maxHeight) {
|
|
2904
|
+
console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - Resizing required, maxWidth:', settings.maxWidth, 'maxHeight:', settings.maxHeight);
|
|
2467
2905
|
// Create an image element to get dimensions
|
|
2468
2906
|
const img = new Image();
|
|
2469
2907
|
const canvas = new OffscreenCanvas(1, 1);
|
|
@@ -2503,36 +2941,73 @@ async function captureTabScreenshot(tabId, settings) {
|
|
|
2503
2941
|
});
|
|
2504
2942
|
}
|
|
2505
2943
|
|
|
2944
|
+
console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - No resize needed, returning dataUrl');
|
|
2945
|
+
console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - Final dataUrl length:', dataUrl?.length);
|
|
2506
2946
|
return dataUrl;
|
|
2507
2947
|
} catch (error) {
|
|
2508
|
-
console.error('
|
|
2948
|
+
console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - EXCEPTION CAUGHT');
|
|
2949
|
+
console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - Error:', error);
|
|
2950
|
+
console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - Error message:', error.message);
|
|
2951
|
+
console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - Error stack:', error.stack);
|
|
2509
2952
|
throw error;
|
|
2510
2953
|
}
|
|
2511
2954
|
}
|
|
2512
2955
|
|
|
2513
2956
|
// Start recording
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
console.log('[License] Checking license before recording...');
|
|
2518
|
-
const licenseCheck = await LicenseHelper.checkLicenseBeforeRecording();
|
|
2519
|
-
console.log('[License] License check result:', licenseCheck);
|
|
2957
|
+
// Detect if server is available (server mode vs browser-only mode)
|
|
2958
|
+
async function detectServerMode() {
|
|
2959
|
+
console.log('[ServerDetection] Checking server availability...');
|
|
2520
2960
|
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2961
|
+
for (const port of CONFIG_PORTS) {
|
|
2962
|
+
try {
|
|
2963
|
+
const response = await Promise.race([
|
|
2964
|
+
fetch(`http://localhost:${port}/health`, {
|
|
2965
|
+
method: 'GET'
|
|
2966
|
+
}),
|
|
2967
|
+
new Promise((_, reject) =>
|
|
2968
|
+
setTimeout(() => reject(new Error('Timeout')), 1000)
|
|
2969
|
+
)
|
|
2970
|
+
]);
|
|
2530
2971
|
|
|
2531
|
-
|
|
2972
|
+
if (response.ok) {
|
|
2973
|
+
console.log(`[ServerDetection] Server detected on port ${port}`);
|
|
2974
|
+
serverMode = 'server';
|
|
2975
|
+
return 'server';
|
|
2976
|
+
}
|
|
2977
|
+
} catch (error) {
|
|
2978
|
+
// Continue checking other ports
|
|
2532
2979
|
}
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
console.log('[ServerDetection] No server detected - using browser-only mode');
|
|
2983
|
+
serverMode = 'browser-only';
|
|
2984
|
+
return 'browser-only';
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
// Simple quota info utility (replaces over-engineered QuotaMonitor class)
|
|
2988
|
+
async function getQuotaInfo() {
|
|
2989
|
+
if (!navigator.storage?.estimate) {
|
|
2990
|
+
return { supported: false };
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
const { usage = 0, quota = 0 } = await navigator.storage.estimate();
|
|
2994
|
+
const percentage = quota > 0 ? usage / quota : 0;
|
|
2995
|
+
const availableMB = Math.round((quota - usage) / (1024 * 1024));
|
|
2996
|
+
|
|
2997
|
+
return {
|
|
2998
|
+
supported: true,
|
|
2999
|
+
percentage,
|
|
3000
|
+
availableMB,
|
|
3001
|
+
usageMB: Math.round(usage / (1024 * 1024)),
|
|
3002
|
+
quotaMB: Math.round(quota / (1024 * 1024))
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
2533
3005
|
|
|
2534
|
-
|
|
2535
|
-
|
|
3006
|
+
async function startRecording(tabId, settings = {}) {
|
|
3007
|
+
try {
|
|
3008
|
+
// Detect server mode before starting
|
|
3009
|
+
await detectServerMode();
|
|
3010
|
+
console.log(`[Recording] Mode: ${serverMode}`);
|
|
2536
3011
|
|
|
2537
3012
|
// Check if session manager is available
|
|
2538
3013
|
if (!sessionManager) {
|
|
@@ -2602,6 +3077,18 @@ async function startRecording(tabId, settings = {}) {
|
|
|
2602
3077
|
const sessionId = currentSession.sessionId;
|
|
2603
3078
|
const scheduledStartTime = Date.now() + 3000; // Schedule start 3 seconds in future for countdown synchronization
|
|
2604
3079
|
|
|
3080
|
+
// STEP 4: Initialize browser-only recording if in browser-only mode
|
|
3081
|
+
if (serverMode === 'browser-only' && browserRecordingManager) {
|
|
3082
|
+
console.log('[Recording] Initializing browser-only recording in IndexedDB');
|
|
3083
|
+
const tab = await chrome.tabs.get(tabId);
|
|
3084
|
+
await browserRecordingManager.startRecording(sessionId, {
|
|
3085
|
+
tabId: tabId,
|
|
3086
|
+
url: tab.url,
|
|
3087
|
+
title: tab.title || settings.sessionName || 'Browser Recording',
|
|
3088
|
+
startTime: scheduledStartTime
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
|
|
2605
3092
|
// Update legacy state for backward compatibility (will be removed)
|
|
2606
3093
|
recordingTabId = tabId;
|
|
2607
3094
|
isCurrentlyRecording = true;
|
|
@@ -2679,6 +3166,17 @@ async function startRecording(tabId, settings = {}) {
|
|
|
2679
3166
|
}
|
|
2680
3167
|
});
|
|
2681
3168
|
|
|
3169
|
+
// STEP 4: Initialize browser recording if in browser-only mode
|
|
3170
|
+
if (serverMode === 'browser-only' && browserRecordingManager) {
|
|
3171
|
+
const tab = await chrome.tabs.get(tabId);
|
|
3172
|
+
await browserRecordingManager.startRecording(sessionId, {
|
|
3173
|
+
tabId,
|
|
3174
|
+
url: tab.url,
|
|
3175
|
+
title: tab.title
|
|
3176
|
+
});
|
|
3177
|
+
console.log('[Recording] Browser recording initialized for session:', sessionId);
|
|
3178
|
+
}
|
|
3179
|
+
|
|
2682
3180
|
// Send stream ID, session info, and settings to offscreen document for frame capture
|
|
2683
3181
|
await chrome.runtime.sendMessage({
|
|
2684
3182
|
type: 'start-frame-capture',
|
|
@@ -2688,14 +3186,15 @@ async function startRecording(tabId, settings = {}) {
|
|
|
2688
3186
|
sessionId: sessionId,
|
|
2689
3187
|
ownerId: currentOwnerId,
|
|
2690
3188
|
scheduledStartTime: scheduledStartTime,
|
|
3189
|
+
mode: serverMode, // Pass mode to offscreen document
|
|
2691
3190
|
settings: {
|
|
2692
3191
|
frameRate: settings.frameRate || 1,
|
|
2693
|
-
imageQuality: settings.imageQuality || 30,
|
|
3192
|
+
imageQuality: settings.imageQuality || (serverMode === 'browser-only' ? 15 : 30),
|
|
2694
3193
|
frameFlash: settings.frameFlash !== false
|
|
2695
3194
|
}
|
|
2696
3195
|
});
|
|
2697
|
-
|
|
2698
|
-
console.log('Recording started');
|
|
3196
|
+
|
|
3197
|
+
console.log('Recording started in', serverMode, 'mode');
|
|
2699
3198
|
} catch (error) {
|
|
2700
3199
|
console.error('Error in startRecording:', error);
|
|
2701
3200
|
throw error;
|
|
@@ -2708,16 +3207,23 @@ async function stopRecording() {
|
|
|
2708
3207
|
// Check if session manager is available
|
|
2709
3208
|
if (!sessionManager) {
|
|
2710
3209
|
console.warn('Session manager not available, stopping recording anyway');
|
|
2711
|
-
return;
|
|
3210
|
+
return { success: false, error: 'Session manager not available' };
|
|
2712
3211
|
}
|
|
2713
3212
|
|
|
2714
3213
|
// Check if currently recording via session manager
|
|
2715
3214
|
const currentlyRecording = await isCurrentlyRecordingAsync();
|
|
2716
3215
|
if (!currentlyRecording) {
|
|
2717
3216
|
console.log('No active recording session to stop');
|
|
2718
|
-
return;
|
|
3217
|
+
return { success: false, error: 'No active recording' };
|
|
2719
3218
|
}
|
|
2720
3219
|
|
|
3220
|
+
// Store sessionId before clearing it
|
|
3221
|
+
const stoppedSessionId = currentSession?.sessionId;
|
|
3222
|
+
|
|
3223
|
+
// Capture frame count and duration for the response
|
|
3224
|
+
let recordedFrameCount = 0;
|
|
3225
|
+
let recordedDuration = 0;
|
|
3226
|
+
|
|
2721
3227
|
// Stop recording with session manager
|
|
2722
3228
|
if (currentSession?.sessionId && currentOwnerId) {
|
|
2723
3229
|
const stopResult = await sessionManager.stopRecording(currentSession.sessionId, currentOwnerId);
|
|
@@ -2725,12 +3231,15 @@ async function stopRecording() {
|
|
|
2725
3231
|
console.error('Failed to stop session:', stopResult.error?.message);
|
|
2726
3232
|
// Continue with cleanup anyway
|
|
2727
3233
|
} else {
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
3234
|
+
recordedFrameCount = stopResult.frameCount || 0;
|
|
3235
|
+
recordedDuration = stopResult.sessionDuration || 0;
|
|
3236
|
+
console.log(`Recording stopped. Duration: ${recordedDuration}ms, Frames: ${recordedFrameCount}`);
|
|
3237
|
+
|
|
3238
|
+
// Finalize browser-only recording if in browser-only mode
|
|
3239
|
+
if (serverMode === 'browser-only' && browserRecordingManager) {
|
|
3240
|
+
console.log('[Browser-Only] Finalizing browser recording in IndexedDB');
|
|
3241
|
+
await browserRecordingManager.stopRecording(currentSession.sessionId);
|
|
3242
|
+
}
|
|
2734
3243
|
}
|
|
2735
3244
|
}
|
|
2736
3245
|
|
|
@@ -2760,6 +3269,29 @@ async function stopRecording() {
|
|
|
2760
3269
|
target: 'offscreen'
|
|
2761
3270
|
});
|
|
2762
3271
|
|
|
3272
|
+
// CRITICAL: Store recordingTabId BEFORE clearing it (for cleanup)
|
|
3273
|
+
const previousRecordingTabId = recordingTabId;
|
|
3274
|
+
console.log('[Background] previousRecordingTabId:', previousRecordingTabId, 'stoppedSessionId:', stoppedSessionId);
|
|
3275
|
+
|
|
3276
|
+
// CRITICAL FIX: Send stop-screen-capture-tracking message to recording tab WITH sessionId
|
|
3277
|
+
// so it can show the completion UI
|
|
3278
|
+
if (previousRecordingTabId && stoppedSessionId) {
|
|
3279
|
+
try {
|
|
3280
|
+
console.log('[Background] Sending recording-complete-show-ui message to tab:', previousRecordingTabId, 'with sessionId:', stoppedSessionId);
|
|
3281
|
+
await chrome.tabs.sendMessage(previousRecordingTabId, {
|
|
3282
|
+
type: 'recording-complete-show-ui',
|
|
3283
|
+
sessionId: stoppedSessionId
|
|
3284
|
+
}).catch((err) => {
|
|
3285
|
+
console.log('[Background] Could not send completion UI message to recording tab:', err);
|
|
3286
|
+
});
|
|
3287
|
+
console.log('[Background] Successfully sent recording-complete-show-ui message');
|
|
3288
|
+
} catch (error) {
|
|
3289
|
+
console.error('[Background] Failed to send completion UI message:', error);
|
|
3290
|
+
}
|
|
3291
|
+
} else {
|
|
3292
|
+
console.log('[Background] NOT sending recording-complete-show-ui - missing previousRecordingTabId or stoppedSessionId');
|
|
3293
|
+
}
|
|
3294
|
+
|
|
2763
3295
|
// CRITICAL FIX: Send stop-screen-capture-tracking message to ALL content scripts
|
|
2764
3296
|
try {
|
|
2765
3297
|
const tabs = await chrome.tabs.query({});
|
|
@@ -2780,12 +3312,22 @@ async function stopRecording() {
|
|
|
2780
3312
|
console.error('[Background] Failed to send cleanup messages to content scripts:', error);
|
|
2781
3313
|
}
|
|
2782
3314
|
|
|
3315
|
+
// CRITICAL FIX: Restore original console methods and remove event listeners
|
|
3316
|
+
if (previousRecordingTabId) {
|
|
3317
|
+
try {
|
|
3318
|
+
console.log('[Background] Stopping console log capture for tab:', previousRecordingTabId);
|
|
3319
|
+
await stopCapturingLogs(previousRecordingTabId);
|
|
3320
|
+
} catch (cleanupError) {
|
|
3321
|
+
console.error('[Background] Failed to cleanup console interceptor:', cleanupError);
|
|
3322
|
+
// Continue with stop process even if cleanup fails
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
|
|
2783
3326
|
// Clear session cache
|
|
2784
3327
|
currentSession = null;
|
|
2785
3328
|
currentOwnerId = null;
|
|
2786
3329
|
|
|
2787
3330
|
// Update legacy state for backward compatibility (will be removed)
|
|
2788
|
-
const previousRecordingTabId = recordingTabId; // Store for logBuffer cleanup
|
|
2789
3331
|
isCurrentlyRecording = false;
|
|
2790
3332
|
recordingTabId = null;
|
|
2791
3333
|
currentRecordingSessionId = null;
|
|
@@ -2795,11 +3337,56 @@ async function stopRecording() {
|
|
|
2795
3337
|
logBuffer.clearTab(previousRecordingTabId);
|
|
2796
3338
|
}
|
|
2797
3339
|
|
|
3340
|
+
// CRITICAL: Clear pending frame queue to prevent infinite retry loop
|
|
3341
|
+
if (stoppedSessionId && pendingFrameQueue.has(stoppedSessionId)) {
|
|
3342
|
+
console.log(`[FrameQueue] Clearing ${pendingFrameQueue.get(stoppedSessionId).length} pending frames for stopped session ${stoppedSessionId}`);
|
|
3343
|
+
pendingFrameQueue.delete(stoppedSessionId);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
2798
3346
|
console.log('Stop message sent to offscreen document');
|
|
3347
|
+
|
|
3348
|
+
// Notify popup to refresh recordings list
|
|
3349
|
+
chrome.runtime.sendMessage({
|
|
3350
|
+
action: 'recordingStopped',
|
|
3351
|
+
sessionId: stoppedSessionId
|
|
3352
|
+
}).catch(() => {
|
|
3353
|
+
// Popup might not be open, that's okay
|
|
3354
|
+
console.log('[Background] Popup not available to notify of recording stop');
|
|
3355
|
+
});
|
|
3356
|
+
|
|
3357
|
+
// Format duration as human-readable string (e.g., "1m 30s" or "45s")
|
|
3358
|
+
const formatDurationForDisplay = (ms) => {
|
|
3359
|
+
const seconds = Math.floor(ms / 1000);
|
|
3360
|
+
if (seconds < 60) {
|
|
3361
|
+
return `${seconds}s`;
|
|
3362
|
+
}
|
|
3363
|
+
const minutes = Math.floor(seconds / 60);
|
|
3364
|
+
const remainingSeconds = seconds % 60;
|
|
3365
|
+
return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
|
3366
|
+
};
|
|
3367
|
+
|
|
3368
|
+
// Return success with sessionId, frameCount and duration for popup display
|
|
3369
|
+
return {
|
|
3370
|
+
success: true,
|
|
3371
|
+
sessionId: stoppedSessionId,
|
|
3372
|
+
frameCount: recordedFrameCount,
|
|
3373
|
+
duration: formatDurationForDisplay(recordedDuration)
|
|
3374
|
+
};
|
|
2799
3375
|
} catch (error) {
|
|
2800
3376
|
console.error('Error in stopRecording:', error);
|
|
2801
3377
|
// Clear session cache even on error
|
|
2802
3378
|
const errorRecordingTabId = recordingTabId; // Store for cleanup
|
|
3379
|
+
|
|
3380
|
+
// CRITICAL FIX: Clean up console interceptor even on error
|
|
3381
|
+
if (errorRecordingTabId) {
|
|
3382
|
+
try {
|
|
3383
|
+
console.log('[Background] Emergency cleanup of console interceptor for tab:', errorRecordingTabId);
|
|
3384
|
+
await stopCapturingLogs(errorRecordingTabId);
|
|
3385
|
+
} catch (cleanupError) {
|
|
3386
|
+
console.error('[Background] Failed to cleanup console interceptor during error handling:', cleanupError);
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
|
|
2803
3390
|
currentSession = null;
|
|
2804
3391
|
currentOwnerId = null;
|
|
2805
3392
|
isCurrentlyRecording = false;
|
|
@@ -2818,204 +3405,16 @@ async function stopRecording() {
|
|
|
2818
3405
|
|
|
2819
3406
|
|
|
2820
3407
|
// Capture console logs from the recording tab
|
|
3408
|
+
// REFACTORED: Now uses shared console-interception-library.js
|
|
2821
3409
|
async function startCapturingLogs(tabId) {
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
console.log('Cannot inject console logger into restricted URL:', tab.url);
|
|
2826
|
-
console.warn('WARNING: Console logs cannot be captured on restricted pages (chrome://, chrome-extension://, etc.)');
|
|
2827
|
-
console.warn('To capture console logs, please navigate to a regular web page before recording.');
|
|
2828
|
-
return false;
|
|
2829
|
-
}
|
|
2830
|
-
|
|
2831
|
-
// First inject the main world console interceptor
|
|
2832
|
-
try {
|
|
2833
|
-
const results = await chrome.scripting.executeScript({
|
|
2834
|
-
target: { tabId: tabId },
|
|
2835
|
-
world: 'MAIN',
|
|
2836
|
-
func: () => {
|
|
2837
|
-
// Check if we've already overridden console methods
|
|
2838
|
-
if (window.__chromePilotConsoleOverridden) {
|
|
2839
|
-
console.log('[Chrome Debug] Console already overridden, skipping');
|
|
2840
|
-
return 'already_installed';
|
|
2841
|
-
}
|
|
2842
|
-
window.__chromePilotConsoleOverridden = true;
|
|
2843
|
-
console.log('[Chrome Debug] Installing console interceptor');
|
|
2844
|
-
|
|
2845
|
-
// Override console methods to capture logs - complete coverage
|
|
2846
|
-
const originalLog = console.log;
|
|
2847
|
-
const originalError = console.error;
|
|
2848
|
-
const originalWarn = console.warn;
|
|
2849
|
-
const originalInfo = console.info;
|
|
2850
|
-
const originalDebug = console.debug;
|
|
2851
|
-
const originalTrace = console.trace;
|
|
2852
|
-
const originalTable = console.table;
|
|
2853
|
-
const originalDir = console.dir;
|
|
2854
|
-
const originalGroup = console.group;
|
|
2855
|
-
const originalGroupEnd = console.groupEnd;
|
|
2856
|
-
const originalTime = console.time;
|
|
2857
|
-
const originalTimeEnd = console.timeEnd;
|
|
2858
|
-
const originalCount = console.count;
|
|
2859
|
-
|
|
2860
|
-
const sendLog = (level, args) => {
|
|
2861
|
-
try {
|
|
2862
|
-
// Pre-serialize arguments to strings to avoid structured clone errors
|
|
2863
|
-
const serializedArgs = args.map(arg => {
|
|
2864
|
-
try {
|
|
2865
|
-
if (arg === null) return 'null';
|
|
2866
|
-
if (arg === undefined) return 'undefined';
|
|
2867
|
-
if (typeof arg === 'function') return '[Function: ' + (arg.name || 'anonymous') + ']';
|
|
2868
|
-
if (arg instanceof Element) return '[DOM Element: ' + arg.tagName + ']';
|
|
2869
|
-
if (typeof arg === 'object') {
|
|
2870
|
-
// Try to stringify, but limit depth to avoid circular references
|
|
2871
|
-
let stringified = JSON.stringify(arg, null, 2);
|
|
2872
|
-
|
|
2873
|
-
// Check if this looks like a base64 image and truncate it
|
|
2874
|
-
if (stringified.includes('data:image/') && stringified.length > 1000) {
|
|
2875
|
-
const match = stringified.match(/data:image\/([^;]+);base64,(.{0,100})/);
|
|
2876
|
-
if (match) {
|
|
2877
|
-
return `[Base64 Image: ${match[1]}, ${stringified.length} bytes total, truncated...]`;
|
|
2878
|
-
}
|
|
2879
|
-
}
|
|
2880
|
-
|
|
2881
|
-
// Truncate any extremely large strings
|
|
2882
|
-
const maxLength = 5000;
|
|
2883
|
-
if (stringified.length > maxLength) {
|
|
2884
|
-
return stringified.substring(0, maxLength) + `... [TRUNCATED: ${stringified.length} total bytes]`;
|
|
2885
|
-
}
|
|
2886
|
-
|
|
2887
|
-
return stringified;
|
|
2888
|
-
}
|
|
2889
|
-
|
|
2890
|
-
// Also check for base64 strings directly
|
|
2891
|
-
const strValue = String(arg);
|
|
2892
|
-
if (strValue.includes('data:image/') && strValue.length > 1000) {
|
|
2893
|
-
const match = strValue.match(/data:image\/([^;]+);base64,(.{0,100})/);
|
|
2894
|
-
if (match) {
|
|
2895
|
-
return `[Base64 Image: ${match[1]}, ${strValue.length} bytes total, truncated...]`;
|
|
2896
|
-
}
|
|
2897
|
-
}
|
|
2898
|
-
|
|
2899
|
-
// Truncate any extremely large strings
|
|
2900
|
-
if (strValue.length > 5000) {
|
|
2901
|
-
return strValue.substring(0, 5000) + `... [TRUNCATED: ${strValue.length} total bytes]`;
|
|
2902
|
-
}
|
|
2903
|
-
|
|
2904
|
-
return strValue;
|
|
2905
|
-
} catch (e) {
|
|
2906
|
-
return '[Object: could not serialize]';
|
|
2907
|
-
}
|
|
2908
|
-
});
|
|
2909
|
-
|
|
2910
|
-
// Post message to content script
|
|
2911
|
-
window.postMessage({
|
|
2912
|
-
type: 'chrome-pilot-console-log',
|
|
2913
|
-
log: {
|
|
2914
|
-
level,
|
|
2915
|
-
message: serializedArgs.join(' '),
|
|
2916
|
-
timestamp: Date.now()
|
|
2917
|
-
}
|
|
2918
|
-
}, '*');
|
|
2919
|
-
} catch (e) {
|
|
2920
|
-
// Ignore errors when sending logs
|
|
2921
|
-
}
|
|
2922
|
-
};
|
|
2923
|
-
|
|
2924
|
-
console.log = (...args) => {
|
|
2925
|
-
sendLog('log', args);
|
|
2926
|
-
originalLog.apply(console, args);
|
|
2927
|
-
};
|
|
2928
|
-
|
|
2929
|
-
console.error = (...args) => {
|
|
2930
|
-
sendLog('error', args);
|
|
2931
|
-
originalError.apply(console, args);
|
|
2932
|
-
};
|
|
2933
|
-
|
|
2934
|
-
console.warn = (...args) => {
|
|
2935
|
-
sendLog('warn', args);
|
|
2936
|
-
originalWarn.apply(console, args);
|
|
2937
|
-
};
|
|
2938
|
-
|
|
2939
|
-
console.info = (...args) => {
|
|
2940
|
-
sendLog('info', args);
|
|
2941
|
-
originalInfo.apply(console, args);
|
|
2942
|
-
};
|
|
2943
|
-
|
|
2944
|
-
console.debug = (...args) => {
|
|
2945
|
-
sendLog('debug', args);
|
|
2946
|
-
originalDebug.apply(console, args);
|
|
2947
|
-
};
|
|
2948
|
-
|
|
2949
|
-
console.trace = (...args) => {
|
|
2950
|
-
sendLog('trace', args);
|
|
2951
|
-
originalTrace.apply(console, args);
|
|
2952
|
-
};
|
|
2953
|
-
|
|
2954
|
-
console.table = (...args) => {
|
|
2955
|
-
sendLog('table', args);
|
|
2956
|
-
originalTable.apply(console, args);
|
|
2957
|
-
};
|
|
2958
|
-
|
|
2959
|
-
console.dir = (...args) => {
|
|
2960
|
-
sendLog('dir', args);
|
|
2961
|
-
originalDir.apply(console, args);
|
|
2962
|
-
};
|
|
2963
|
-
|
|
2964
|
-
console.group = (...args) => {
|
|
2965
|
-
sendLog('group', args);
|
|
2966
|
-
originalGroup.apply(console, args);
|
|
2967
|
-
};
|
|
2968
|
-
|
|
2969
|
-
console.groupEnd = (...args) => {
|
|
2970
|
-
sendLog('groupEnd', args);
|
|
2971
|
-
originalGroupEnd.apply(console, args);
|
|
2972
|
-
};
|
|
2973
|
-
|
|
2974
|
-
console.time = (...args) => {
|
|
2975
|
-
sendLog('time', args);
|
|
2976
|
-
originalTime.apply(console, args);
|
|
2977
|
-
};
|
|
2978
|
-
|
|
2979
|
-
console.timeEnd = (...args) => {
|
|
2980
|
-
sendLog('timeEnd', args);
|
|
2981
|
-
originalTimeEnd.apply(console, args);
|
|
2982
|
-
};
|
|
2983
|
-
|
|
2984
|
-
console.count = (...args) => {
|
|
2985
|
-
sendLog('count', args);
|
|
2986
|
-
originalCount.apply(console, args);
|
|
2987
|
-
};
|
|
2988
|
-
|
|
2989
|
-
return 'console_installed';
|
|
2990
|
-
}
|
|
2991
|
-
});
|
|
2992
|
-
|
|
2993
|
-
console.log('[Console Injection] MAIN world script injected successfully:', results);
|
|
2994
|
-
|
|
2995
|
-
// Then inject a content script to relay messages from main world to background
|
|
2996
|
-
await chrome.scripting.executeScript({
|
|
2997
|
-
target: { tabId: tabId },
|
|
2998
|
-
func: () => {
|
|
2999
|
-
// Listen for messages from main world
|
|
3000
|
-
window.addEventListener('message', (event) => {
|
|
3001
|
-
if (event.data && event.data.type === 'chrome-pilot-console-log') {
|
|
3002
|
-
// Forward to background script
|
|
3003
|
-
chrome.runtime.sendMessage({
|
|
3004
|
-
action: 'consoleLog',
|
|
3005
|
-
log: event.data.log
|
|
3006
|
-
});
|
|
3007
|
-
}
|
|
3008
|
-
});
|
|
3009
|
-
}
|
|
3010
|
-
});
|
|
3410
|
+
console.log('[Screen Recording] Starting console interception using shared library');
|
|
3411
|
+
return await self.ConsoleInterceptionLibrary.startConsoleInterception(tabId, SCREEN_RECORDING_CONSOLE_CONFIG);
|
|
3412
|
+
}
|
|
3011
3413
|
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
console.error('[Console Injection] Error details:', error.message, error.stack);
|
|
3017
|
-
return false; // Failed to inject console interceptor
|
|
3018
|
-
}
|
|
3414
|
+
// REFACTORED: Now uses shared console-interception-library.js via proper namespace
|
|
3415
|
+
async function stopCapturingLogs(tabId) {
|
|
3416
|
+
console.log('[Screen Recording] Stopping console interception using shared library');
|
|
3417
|
+
return await self.ConsoleInterceptionLibrary.stopConsoleInterception(tabId, SCREEN_RECORDING_CONSOLE_CONFIG);
|
|
3019
3418
|
}
|
|
3020
3419
|
|
|
3021
3420
|
// Workflow Recording Functions
|
|
@@ -3023,29 +3422,6 @@ async function startWorkflowRecording(tabId, includeLogsInExport, sessionName =
|
|
|
3023
3422
|
try {
|
|
3024
3423
|
console.log('Starting workflow recording for tab:', tabId);
|
|
3025
3424
|
|
|
3026
|
-
// STEP 1: Check license and usage limits BEFORE recording (same pattern as startRecording)
|
|
3027
|
-
console.log('[License] Checking license before workflow recording...');
|
|
3028
|
-
const licenseCheck = await LicenseHelper.checkLicenseBeforeRecording();
|
|
3029
|
-
console.log('[License] License check result:', licenseCheck);
|
|
3030
|
-
|
|
3031
|
-
if (!licenseCheck.allowed) {
|
|
3032
|
-
// Show notification to user (same pattern as screen recording)
|
|
3033
|
-
chrome.notifications.create({
|
|
3034
|
-
type: 'basic',
|
|
3035
|
-
iconUrl: chrome.runtime.getURL('icon128.png'),
|
|
3036
|
-
title: 'Recording Limit Reached',
|
|
3037
|
-
message: licenseCheck.message || 'Daily limit reached. Upgrade to Pro for unlimited workflow recordings.',
|
|
3038
|
-
buttons: [{ title: 'Upgrade to Pro' }],
|
|
3039
|
-
priority: 2
|
|
3040
|
-
});
|
|
3041
|
-
|
|
3042
|
-
throw new Error(licenseCheck.message || 'Workflow recording not allowed');
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
// Store userId for later usage tracking
|
|
3046
|
-
const userId = licenseCheck.userId;
|
|
3047
|
-
workflowUserIds.set(tabId, userId);
|
|
3048
|
-
|
|
3049
3425
|
// Check if this tab allows content script injection
|
|
3050
3426
|
const tab = await chrome.tabs.get(tabId);
|
|
3051
3427
|
if (!tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://') || tab.url.startsWith('moz-extension://')) {
|
|
@@ -3063,107 +3439,18 @@ async function startWorkflowRecording(tabId, includeLogsInExport, sessionName =
|
|
|
3063
3439
|
const workflowId = `workflow_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
3064
3440
|
await chrome.storage.local.set({ currentWorkflowId: workflowId });
|
|
3065
3441
|
|
|
3066
|
-
// Clear any existing logs for this tab
|
|
3442
|
+
// Clear any existing logs for this tab and initialize race-safe buffer
|
|
3067
3443
|
await chrome.storage.session.set({ [`workflow_${tabId}`]: [] });
|
|
3068
|
-
|
|
3069
|
-
//
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
window.__chromePilotWorkflowConsoleOverridden = true;
|
|
3079
|
-
|
|
3080
|
-
// Override console methods to capture logs
|
|
3081
|
-
const originalLog = console.log;
|
|
3082
|
-
const originalError = console.error;
|
|
3083
|
-
const originalWarn = console.warn;
|
|
3084
|
-
const originalInfo = console.info;
|
|
3085
|
-
|
|
3086
|
-
const sendLog = (level, args) => {
|
|
3087
|
-
try {
|
|
3088
|
-
// Pre-serialize arguments to strings to avoid structured clone errors
|
|
3089
|
-
const serializedArgs = args.map(arg => {
|
|
3090
|
-
try {
|
|
3091
|
-
if (arg === null) return 'null';
|
|
3092
|
-
if (arg === undefined) return 'undefined';
|
|
3093
|
-
if (typeof arg === 'function') return '[Function: ' + (arg.name || 'anonymous') + ']';
|
|
3094
|
-
if (arg instanceof Element) return '[DOM Element: ' + arg.tagName + ']';
|
|
3095
|
-
if (typeof arg === 'object') {
|
|
3096
|
-
// Try to stringify, but limit depth to avoid circular references
|
|
3097
|
-
return JSON.stringify(arg, null, 2);
|
|
3098
|
-
}
|
|
3099
|
-
return String(arg);
|
|
3100
|
-
} catch (e) {
|
|
3101
|
-
return '[Object: could not serialize]';
|
|
3102
|
-
}
|
|
3103
|
-
});
|
|
3104
|
-
|
|
3105
|
-
// Post message to content script
|
|
3106
|
-
window.postMessage({
|
|
3107
|
-
type: 'chrome-pilot-workflow-console-log',
|
|
3108
|
-
log: {
|
|
3109
|
-
level,
|
|
3110
|
-
message: serializedArgs.join(' '),
|
|
3111
|
-
timestamp: Date.now()
|
|
3112
|
-
}
|
|
3113
|
-
}, '*');
|
|
3114
|
-
} catch (e) {
|
|
3115
|
-
// Ignore errors when sending logs
|
|
3116
|
-
}
|
|
3117
|
-
};
|
|
3118
|
-
|
|
3119
|
-
console.log = (...args) => {
|
|
3120
|
-
sendLog('log', args);
|
|
3121
|
-
originalLog.apply(console, args);
|
|
3122
|
-
};
|
|
3123
|
-
|
|
3124
|
-
console.error = (...args) => {
|
|
3125
|
-
sendLog('error', args);
|
|
3126
|
-
originalError.apply(console, args);
|
|
3127
|
-
};
|
|
3128
|
-
|
|
3129
|
-
console.warn = (...args) => {
|
|
3130
|
-
sendLog('warn', args);
|
|
3131
|
-
originalWarn.apply(console, args);
|
|
3132
|
-
};
|
|
3133
|
-
|
|
3134
|
-
console.info = (...args) => {
|
|
3135
|
-
sendLog('info', args);
|
|
3136
|
-
originalInfo.apply(console, args);
|
|
3137
|
-
};
|
|
3138
|
-
|
|
3139
|
-
// Also capture window errors
|
|
3140
|
-
window.addEventListener('error', (event) => {
|
|
3141
|
-
sendLog('exception', [`${event.message} at ${event.filename}:${event.lineno}:${event.colno}`]);
|
|
3142
|
-
});
|
|
3143
|
-
|
|
3144
|
-
window.addEventListener('unhandledrejection', (event) => {
|
|
3145
|
-
sendLog('exception', [`Unhandled Promise Rejection: ${event.reason}`]);
|
|
3146
|
-
});
|
|
3147
|
-
}
|
|
3148
|
-
});
|
|
3149
|
-
|
|
3150
|
-
// Inject content script to relay messages and handle workflow recording
|
|
3151
|
-
await chrome.scripting.executeScript({
|
|
3152
|
-
target: { tabId: tabId },
|
|
3153
|
-
func: (includeLogsInExport) => {
|
|
3154
|
-
// Listen for console log messages from main world
|
|
3155
|
-
window.addEventListener('message', (event) => {
|
|
3156
|
-
if (event.data && event.data.type === 'chrome-pilot-workflow-console-log') {
|
|
3157
|
-
// Forward to background script
|
|
3158
|
-
chrome.runtime.sendMessage({
|
|
3159
|
-
action: 'workflowConsoleLog',
|
|
3160
|
-
log: event.data.log
|
|
3161
|
-
});
|
|
3162
|
-
}
|
|
3163
|
-
});
|
|
3164
|
-
},
|
|
3165
|
-
args: [includeLogsInExport]
|
|
3166
|
-
});
|
|
3444
|
+
|
|
3445
|
+
// Initialize WorkflowLogBuffer for race-safe log handling
|
|
3446
|
+
if (includeLogsInExport) {
|
|
3447
|
+
const buffer = new WorkflowLogBuffer(tabId);
|
|
3448
|
+
workflowLogBuffers.set(tabId, buffer);
|
|
3449
|
+
console.log(`[Workflow] Initialized WorkflowLogBuffer for tab ${tabId}`);
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
// REFACTORED: Use shared console interception library via proper namespace (replaces 150+ lines of duplicate code)
|
|
3453
|
+
await self.ConsoleInterceptionLibrary.startConsoleInterception(tabId, WORKFLOW_RECORDING_CONSOLE_CONFIG);
|
|
3167
3454
|
|
|
3168
3455
|
// Ensure content script is injected
|
|
3169
3456
|
try {
|
|
@@ -3181,9 +3468,11 @@ async function startWorkflowRecording(tabId, includeLogsInExport, sessionName =
|
|
|
3181
3468
|
}
|
|
3182
3469
|
|
|
3183
3470
|
// Tell the content script to start recording
|
|
3471
|
+
// Include tabId so content script can use it when stopping recording
|
|
3184
3472
|
await chrome.tabs.sendMessage(tabId, {
|
|
3185
3473
|
action: 'startWorkflowRecording',
|
|
3186
|
-
screenshotSettings: screenshotSettings
|
|
3474
|
+
screenshotSettings: screenshotSettings,
|
|
3475
|
+
tabId: tabId
|
|
3187
3476
|
});
|
|
3188
3477
|
|
|
3189
3478
|
console.log('Workflow recording started successfully');
|
|
@@ -3202,38 +3491,77 @@ async function stopWorkflowRecording(tabId) {
|
|
|
3202
3491
|
throw new Error('No workflow recording active for this tab');
|
|
3203
3492
|
}
|
|
3204
3493
|
|
|
3205
|
-
// Get workflow from content script
|
|
3494
|
+
// Get workflow from content script using a different action to avoid circular dependency
|
|
3495
|
+
// (mini menu also sends 'stopWorkflowRecording' to background, creating confusion)
|
|
3206
3496
|
const response = await chrome.tabs.sendMessage(tabId, {
|
|
3207
|
-
action: '
|
|
3497
|
+
action: 'getWorkflowData'
|
|
3208
3498
|
});
|
|
3209
|
-
|
|
3499
|
+
|
|
3210
3500
|
if (!response || !response.success) {
|
|
3211
3501
|
throw new Error('Failed to get workflow from content script');
|
|
3212
3502
|
}
|
|
3213
|
-
|
|
3503
|
+
|
|
3504
|
+
// CRITICAL FIX: Restore original console methods and remove event listeners
|
|
3505
|
+
try {
|
|
3506
|
+
console.log('[Workflow] Stopping console log capture for tab:', tabId);
|
|
3507
|
+
await stopCapturingWorkflowLogs(tabId);
|
|
3508
|
+
} catch (cleanupError) {
|
|
3509
|
+
console.error('[Workflow] Failed to cleanup console interceptor:', cleanupError);
|
|
3510
|
+
// Continue with stop process even if cleanup fails
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3214
3513
|
// Handle both old format (array) and new format (object with actions and functionTraces)
|
|
3215
3514
|
let workflowData = response.workflow || [];
|
|
3216
3515
|
let workflow = Array.isArray(workflowData) ? workflowData : (workflowData.actions || []);
|
|
3217
3516
|
let functionTraces = Array.isArray(workflowData) ? [] : (workflowData.functionTraces || []);
|
|
3218
|
-
|
|
3517
|
+
|
|
3219
3518
|
console.log(`[Background] Received ${workflow.length} actions and ${functionTraces.length} function traces`);
|
|
3519
|
+
console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Checking actions for screenshot_data');
|
|
3520
|
+
|
|
3521
|
+
const actionsWithScreenshots = workflow.filter(a => a.screenshot_data);
|
|
3522
|
+
console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Actions WITH screenshot_data:', actionsWithScreenshots.length);
|
|
3523
|
+
|
|
3524
|
+
workflow.forEach((action, index) => {
|
|
3525
|
+
console.log(`[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Action ${index}:`, {
|
|
3526
|
+
type: action.type,
|
|
3527
|
+
hasScreenshotData: !!action.screenshot_data,
|
|
3528
|
+
screenshotDataLength: action.screenshot_data?.length,
|
|
3529
|
+
screenshotPreview: action.screenshot_data?.substring(0, 50)
|
|
3530
|
+
});
|
|
3531
|
+
});
|
|
3220
3532
|
|
|
3221
|
-
// If logs should be included, get them from session storage
|
|
3533
|
+
// If logs should be included, flush buffer and get them from session storage
|
|
3222
3534
|
if (workflowIncludeLogs.get(tabId)) {
|
|
3535
|
+
// CRITICAL: Flush any buffered logs before retrieval to ensure all logs are captured
|
|
3536
|
+
const buffer = workflowLogBuffers.get(tabId);
|
|
3537
|
+
if (buffer) {
|
|
3538
|
+
await buffer.flush();
|
|
3539
|
+
console.log(`[Workflow] Flushed buffer before retrieval, stats:`, buffer.getStats());
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3223
3542
|
const result = await chrome.storage.session.get(`workflow_${tabId}`);
|
|
3224
3543
|
const logs = result[`workflow_${tabId}`] || [];
|
|
3225
3544
|
|
|
3226
|
-
// Associate logs with workflow actions
|
|
3545
|
+
// Associate logs with workflow actions using NON-OVERLAPPING windows
|
|
3546
|
+
// Each log appears in exactly ONE action to avoid duplicates
|
|
3227
3547
|
workflow = workflow.map((action, index) => {
|
|
3228
|
-
//
|
|
3548
|
+
// Calculate non-overlapping time window:
|
|
3549
|
+
// - Each action owns logs from ITS timestamp until the NEXT action's timestamp
|
|
3550
|
+
// - First action also includes 500ms before it
|
|
3551
|
+
// - Last action includes 5000ms after it
|
|
3552
|
+
const windowStart = index === 0
|
|
3553
|
+
? action.timestamp - 500 // First action: include 500ms before
|
|
3554
|
+
: action.timestamp; // Other actions: start at this action's timestamp
|
|
3555
|
+
|
|
3556
|
+
const windowEnd = index < workflow.length - 1
|
|
3557
|
+
? workflow[index + 1].timestamp // Until next action's timestamp
|
|
3558
|
+
: action.timestamp + 5000; // Last action: 5000ms after
|
|
3559
|
+
|
|
3560
|
+
// Use >= for start and < for end to ensure non-overlapping windows
|
|
3229
3561
|
const actionLogs = logs.filter(log => {
|
|
3230
|
-
|
|
3231
|
-
const prevActionTime = index > 0 ? workflow[index - 1].timestamp : action.timestamp - 500;
|
|
3232
|
-
const nextActionTime = index < workflow.length - 1 ? workflow[index + 1].timestamp : action.timestamp + 5000;
|
|
3233
|
-
|
|
3234
|
-
return log.timestamp > prevActionTime && log.timestamp < nextActionTime;
|
|
3562
|
+
return log.timestamp >= windowStart && log.timestamp < windowEnd;
|
|
3235
3563
|
});
|
|
3236
|
-
|
|
3564
|
+
|
|
3237
3565
|
if (actionLogs.length > 0) {
|
|
3238
3566
|
return { ...action, logs: actionLogs };
|
|
3239
3567
|
}
|
|
@@ -3245,59 +3573,87 @@ async function stopWorkflowRecording(tabId) {
|
|
|
3245
3573
|
const sessionName = workflowSessionNames.get(tabId);
|
|
3246
3574
|
const screenshotSettings = workflowScreenshotSettings.get(tabId);
|
|
3247
3575
|
const includeLogs = workflowIncludeLogs.get(tabId) || false;
|
|
3248
|
-
const userId = workflowUserIds.get(tabId); // Get userId for usage tracking
|
|
3249
3576
|
|
|
3250
3577
|
// Clean up
|
|
3251
3578
|
workflowRecordingTabs.delete(tabId);
|
|
3252
3579
|
workflowIncludeLogs.delete(tabId);
|
|
3253
3580
|
workflowScreenshotSettings.delete(tabId);
|
|
3254
3581
|
workflowSessionNames.delete(tabId);
|
|
3255
|
-
|
|
3582
|
+
|
|
3583
|
+
// Clean up WorkflowLogBuffer
|
|
3584
|
+
const bufferToCleanup = workflowLogBuffers.get(tabId);
|
|
3585
|
+
if (bufferToCleanup) {
|
|
3586
|
+
await bufferToCleanup.cleanup();
|
|
3587
|
+
workflowLogBuffers.delete(tabId);
|
|
3588
|
+
console.log(`[Workflow] Cleaned up WorkflowLogBuffer for tab ${tabId}`);
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3256
3591
|
await chrome.storage.session.remove(`workflow_${tabId}`);
|
|
3257
|
-
|
|
3592
|
+
|
|
3593
|
+
// Notify popup and other extension components that recording has stopped
|
|
3594
|
+
// This is critical for popup UI sync when stopping from mini-menu
|
|
3595
|
+
await chrome.storage.local.set({
|
|
3596
|
+
workflowRecording: false,
|
|
3597
|
+
workflowStartTime: null
|
|
3598
|
+
});
|
|
3599
|
+
|
|
3258
3600
|
console.log('Workflow recording stopped, returning workflow:', workflow);
|
|
3259
3601
|
|
|
3260
3602
|
// Try to save to server
|
|
3261
3603
|
try {
|
|
3262
3604
|
const serverPorts = CONFIG_PORTS.slice(0, 5); // Use first 5 configured ports for workflow recording
|
|
3263
3605
|
let serverResult = null;
|
|
3264
|
-
|
|
3606
|
+
|
|
3265
3607
|
// Get current URL and title
|
|
3266
3608
|
const tab = await chrome.tabs.get(tabId);
|
|
3267
|
-
|
|
3609
|
+
|
|
3610
|
+
// Retrieve the workflow ID that was generated at start
|
|
3611
|
+
const storedData = await chrome.storage.local.get(['currentWorkflowId']);
|
|
3612
|
+
const sessionId = storedData.currentWorkflowId || `workflow_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
3613
|
+
console.log('[Workflow] Using sessionId for save:', sessionId);
|
|
3268
3614
|
|
|
3269
3615
|
for (const port of serverPorts) {
|
|
3270
3616
|
try {
|
|
3271
3617
|
console.log(`[Workflow] Attempting to save to server on port ${port}...`);
|
|
3618
|
+
|
|
3619
|
+
const payloadData = {
|
|
3620
|
+
sessionId: sessionId,
|
|
3621
|
+
name: sessionName,
|
|
3622
|
+
url: tab.url,
|
|
3623
|
+
title: tab.title,
|
|
3624
|
+
includeLogs: includeLogs,
|
|
3625
|
+
screenshotSettings: screenshotSettings || {}, // v2.0.8 fix: ensure it's an object, not null
|
|
3626
|
+
actions: workflow,
|
|
3627
|
+
logs: includeLogs ? workflow.flatMap(action => action.logs || []) : [],
|
|
3628
|
+
functionTraces: functionTraces // Include function execution traces
|
|
3629
|
+
};
|
|
3630
|
+
|
|
3631
|
+
console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - HTTP POST payload being prepared');
|
|
3632
|
+
console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Payload actions count:', payloadData.actions.length);
|
|
3633
|
+
|
|
3634
|
+
const payloadActionsWithScreenshots = payloadData.actions.filter(a => a.screenshot_data);
|
|
3635
|
+
console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Payload actions WITH screenshot_data:', payloadActionsWithScreenshots.length);
|
|
3636
|
+
|
|
3637
|
+
payloadData.actions.forEach((action, index) => {
|
|
3638
|
+
console.log(`[SCREENSHOT-DEBUG] background.stopWorkflowRecording - Payload Action ${index}:`, {
|
|
3639
|
+
type: action.type,
|
|
3640
|
+
hasScreenshotData: !!action.screenshot_data,
|
|
3641
|
+
screenshotDataLength: action.screenshot_data?.length,
|
|
3642
|
+
screenshotPreview: action.screenshot_data?.substring(0, 50)
|
|
3643
|
+
});
|
|
3644
|
+
});
|
|
3645
|
+
|
|
3272
3646
|
const response = await fetch(`http://localhost:${port}/chromedebug/workflow-recording`, {
|
|
3273
3647
|
method: 'POST',
|
|
3274
3648
|
headers: {
|
|
3275
3649
|
'Content-Type': 'application/json',
|
|
3276
3650
|
},
|
|
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
|
-
})
|
|
3651
|
+
body: JSON.stringify(payloadData)
|
|
3288
3652
|
});
|
|
3289
3653
|
|
|
3290
3654
|
if (response.ok) {
|
|
3291
3655
|
serverResult = await response.json();
|
|
3292
3656
|
console.log(`[Workflow] Successfully saved to server on port ${port}`, serverResult);
|
|
3293
|
-
|
|
3294
|
-
// STEP 2: Track usage AFTER successful workflow recording completion (same pattern as stopRecording)
|
|
3295
|
-
if (userId) {
|
|
3296
|
-
console.log('[License] Tracking usage after workflow recording completion...');
|
|
3297
|
-
const usageResult = await LicenseHelper.trackUsageAfterRecording(userId);
|
|
3298
|
-
console.log('[License] Usage tracking result:', usageResult);
|
|
3299
|
-
}
|
|
3300
|
-
|
|
3301
3657
|
break;
|
|
3302
3658
|
} else {
|
|
3303
3659
|
console.log(`[Workflow] Failed on port ${port}: ${response.status} ${response.statusText}`);
|
|
@@ -3310,10 +3666,13 @@ async function stopWorkflowRecording(tabId) {
|
|
|
3310
3666
|
}
|
|
3311
3667
|
|
|
3312
3668
|
if (serverResult) {
|
|
3669
|
+
// Clean up currentWorkflowId after successful save
|
|
3670
|
+
await chrome.storage.local.remove(['currentWorkflowId']);
|
|
3671
|
+
|
|
3313
3672
|
return {
|
|
3314
3673
|
success: true,
|
|
3315
3674
|
workflow: {
|
|
3316
|
-
sessionId: sessionId,
|
|
3675
|
+
sessionId: serverResult.workflowId || serverResult.sessionId || sessionId,
|
|
3317
3676
|
url: tab.url,
|
|
3318
3677
|
title: tab.title,
|
|
3319
3678
|
actions: workflow,
|
|
@@ -3323,17 +3682,36 @@ async function stopWorkflowRecording(tabId) {
|
|
|
3323
3682
|
serverResult: serverResult
|
|
3324
3683
|
};
|
|
3325
3684
|
} else {
|
|
3685
|
+
// Server not available - clean up but return local sessionId
|
|
3686
|
+
await chrome.storage.local.remove(['currentWorkflowId']);
|
|
3687
|
+
|
|
3326
3688
|
return {
|
|
3327
3689
|
success: true,
|
|
3328
|
-
workflow:
|
|
3690
|
+
workflow: {
|
|
3691
|
+
sessionId: sessionId,
|
|
3692
|
+
url: tab.url,
|
|
3693
|
+
title: tab.title,
|
|
3694
|
+
actions: workflow,
|
|
3695
|
+
logs: []
|
|
3696
|
+
},
|
|
3329
3697
|
savedToServer: false
|
|
3330
3698
|
};
|
|
3331
3699
|
}
|
|
3332
3700
|
} catch (error) {
|
|
3333
3701
|
console.error('Error saving workflow:', error);
|
|
3702
|
+
// Clean up on error too
|
|
3703
|
+
await chrome.storage.local.remove(['currentWorkflowId']);
|
|
3704
|
+
|
|
3705
|
+
// Return structured response even on error
|
|
3334
3706
|
return {
|
|
3335
3707
|
success: true,
|
|
3336
|
-
workflow:
|
|
3708
|
+
workflow: {
|
|
3709
|
+
sessionId: sessionId,
|
|
3710
|
+
url: tab?.url || 'unknown',
|
|
3711
|
+
title: tab?.title || 'Untitled',
|
|
3712
|
+
actions: workflow,
|
|
3713
|
+
logs: []
|
|
3714
|
+
},
|
|
3337
3715
|
savedToServer: false,
|
|
3338
3716
|
error: error.message
|
|
3339
3717
|
};
|
|
@@ -3344,6 +3722,13 @@ async function stopWorkflowRecording(tabId) {
|
|
|
3344
3722
|
}
|
|
3345
3723
|
}
|
|
3346
3724
|
|
|
3725
|
+
// CRITICAL FIX: Stop workflow console log capture and restore original methods
|
|
3726
|
+
// REFACTORED: Now uses shared console-interception-library.js via proper namespace
|
|
3727
|
+
async function stopCapturingWorkflowLogs(tabId) {
|
|
3728
|
+
console.log('[Workflow Recording] Stopping console interception using shared library');
|
|
3729
|
+
return await self.ConsoleInterceptionLibrary.stopConsoleInterception(tabId, WORKFLOW_RECORDING_CONSOLE_CONFIG);
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3347
3732
|
// Delete recording from server
|
|
3348
3733
|
async function deleteRecordingFromServer(recordingId, sendResponse) {
|
|
3349
3734
|
try {
|
|
@@ -3403,13 +3788,28 @@ async function retryPendingFrames(sessionId) {
|
|
|
3403
3788
|
|
|
3404
3789
|
console.log(`[FrameQueue] Retrying ${pending.length} pending frame batches for session ${sessionId}`);
|
|
3405
3790
|
|
|
3791
|
+
// Check if this is still the active recording session
|
|
3792
|
+
if (currentSession?.sessionId !== sessionId) {
|
|
3793
|
+
console.log(`[FrameQueue] Session ${sessionId} is no longer active, clearing pending frames`);
|
|
3794
|
+
pendingFrameQueue.delete(sessionId);
|
|
3795
|
+
return;
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3406
3798
|
// Validate session now
|
|
3407
3799
|
if (sessionManager) {
|
|
3408
3800
|
const validationResult = await sessionManager.isSessionValid(sessionId);
|
|
3409
3801
|
if (!validationResult.valid) {
|
|
3410
3802
|
console.warn(`[FrameQueue] Session still invalid during retry: ${validationResult.reason}`);
|
|
3411
|
-
// Try again after another delay
|
|
3412
|
-
|
|
3803
|
+
// Try again after another delay (max 3 retries)
|
|
3804
|
+
const retryCount = pending[0]?.retryCount || 0;
|
|
3805
|
+
if (retryCount < 3) {
|
|
3806
|
+
// Mark all batches with retry count
|
|
3807
|
+
pending.forEach(batch => batch.retryCount = retryCount + 1);
|
|
3808
|
+
setTimeout(() => retryPendingFrames(sessionId), 200);
|
|
3809
|
+
} else {
|
|
3810
|
+
console.error(`[FrameQueue] Max retries (3) exceeded for session ${sessionId}, clearing queue`);
|
|
3811
|
+
pendingFrameQueue.delete(sessionId);
|
|
3812
|
+
}
|
|
3413
3813
|
return;
|
|
3414
3814
|
}
|
|
3415
3815
|
}
|
|
@@ -3432,8 +3832,9 @@ async function handleFrameBatch(batchData, skipValidation = false) {
|
|
|
3432
3832
|
try {
|
|
3433
3833
|
const sessionId = batchData.sessionId;
|
|
3434
3834
|
|
|
3435
|
-
//
|
|
3436
|
-
if (
|
|
3835
|
+
// Skip session validation entirely in browser-only mode
|
|
3836
|
+
// Validate session if session manager is available (unless skipping for retry or in browser-only mode)
|
|
3837
|
+
if (sessionManager && !skipValidation && serverMode !== 'browser-only') {
|
|
3437
3838
|
const validationResult = await sessionManager.isSessionValid(sessionId);
|
|
3438
3839
|
if (!validationResult.valid) {
|
|
3439
3840
|
// DON'T LOSE FRAMES - Queue for retry instead of rejecting
|
|
@@ -3537,18 +3938,43 @@ async function handleFrameBatch(batchData, skipValidation = false) {
|
|
|
3537
3938
|
}
|
|
3538
3939
|
|
|
3539
3940
|
if (!uploadSuccess) {
|
|
3540
|
-
console.
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3941
|
+
console.log(`No server available - checking if browser-only mode`);
|
|
3942
|
+
|
|
3943
|
+
// If in browser-only mode, store frames in IndexedDB
|
|
3944
|
+
if (serverMode === 'browser-only' && browserRecordingManager) {
|
|
3945
|
+
console.log(`[Browser-Only] Storing ${indexedFrames.length} frames in IndexedDB`);
|
|
3946
|
+
// Use the browser recording session ID, not the frame capture session ID
|
|
3947
|
+
const recordingSessionId = currentRecordingSessionId || batchData.sessionId;
|
|
3948
|
+
for (const frame of indexedFrames) {
|
|
3949
|
+
// Validate that critical timestamp data exists
|
|
3950
|
+
if (!frame.absoluteTimestamp) {
|
|
3951
|
+
console.warn(`[Browser-Only] WARNING: Frame ${frame.index} missing absoluteTimestamp - log association will fail`);
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
await browserRecordingManager.addFrame(recordingSessionId, {
|
|
3955
|
+
frameIndex: frame.index,
|
|
3956
|
+
screenshot: frame.imageData || frame.screenshot, // Use imageData from frame capture
|
|
3957
|
+
logs: frame.logs || [],
|
|
3958
|
+
timestamp: frame.timestamp || Date.now(),
|
|
3959
|
+
absoluteTimestamp: frame.absoluteTimestamp // Don't use Date.now() fallback - preserve actual capture time
|
|
3960
|
+
});
|
|
3550
3961
|
}
|
|
3551
|
-
|
|
3962
|
+
console.log(`[Browser-Only] Frames stored successfully in IndexedDB for session ${recordingSessionId}`);
|
|
3963
|
+
} else {
|
|
3964
|
+
// Server mode but no server available - show error
|
|
3965
|
+
console.error(`CRITICAL: Failed to upload frame batch to any Chrome Debug server port. Last error: ${lastError}`);
|
|
3966
|
+
console.error(`Session ${batchData.sessionId} frames are only stored in Chrome extension storage and not accessible to Chrome Debug.`);
|
|
3967
|
+
|
|
3968
|
+
// Store error in chrome storage for user visibility
|
|
3969
|
+
await chrome.storage.local.set({
|
|
3970
|
+
[`upload_error_${batchData.sessionId}`]: {
|
|
3971
|
+
error: lastError,
|
|
3972
|
+
timestamp: Date.now(),
|
|
3973
|
+
sessionId: batchData.sessionId,
|
|
3974
|
+
frameCount: indexedFrames.length
|
|
3975
|
+
}
|
|
3976
|
+
});
|
|
3977
|
+
}
|
|
3552
3978
|
}
|
|
3553
3979
|
|
|
3554
3980
|
// Notify LogStreamer that frames have been captured and processed
|
|
@@ -3671,7 +4097,9 @@ async function handleFrameCaptureComplete(sessionData) {
|
|
|
3671
4097
|
await chrome.storage.session.remove(String(tabIdToNotify));
|
|
3672
4098
|
|
|
3673
4099
|
// Send logs to server for association with frames with retry logic
|
|
3674
|
-
|
|
4100
|
+
// Skip server communication in browser-only mode (logs already in IndexedDB)
|
|
4101
|
+
if (bufferedLogs.length > 0 && serverMode !== 'browser-only') {
|
|
4102
|
+
console.log(`[Server Mode] Associating ${bufferedLogs.length} logs with server...`);
|
|
3675
4103
|
|
|
3676
4104
|
// Retry logic to handle race conditions
|
|
3677
4105
|
const maxRetries = 3;
|
|
@@ -3849,6 +4277,62 @@ async function handleFrameCaptureComplete(sessionData) {
|
|
|
3849
4277
|
priority: 1
|
|
3850
4278
|
});
|
|
3851
4279
|
}
|
|
4280
|
+
} else if (bufferedLogs.length > 0 && serverMode === 'browser-only') {
|
|
4281
|
+
// Browser-only mode: Associate logs with frames in IndexedDB
|
|
4282
|
+
console.log(`[Browser-Only] Associating ${bufferedLogs.length} logs with frames in IndexedDB...`);
|
|
4283
|
+
|
|
4284
|
+
try {
|
|
4285
|
+
// Get all frames for this session from IndexedDB
|
|
4286
|
+
const frames = await dataBuffer.getBrowserFrames(sessionIdToUse);
|
|
4287
|
+
console.log(`[Browser-Only] Retrieved ${frames.length} frames from IndexedDB for log association`);
|
|
4288
|
+
|
|
4289
|
+
if (frames.length === 0) {
|
|
4290
|
+
console.warn('[Browser-Only] No frames found - logs cannot be associated');
|
|
4291
|
+
} else {
|
|
4292
|
+
// Associate each log with the appropriate frame based on timestamp
|
|
4293
|
+
// Frames have absoluteTimestamp, logs have timestamp
|
|
4294
|
+
|
|
4295
|
+
for (const log of bufferedLogs) {
|
|
4296
|
+
// Find the frame that this log belongs to (closest frame before the log timestamp)
|
|
4297
|
+
let targetFrame = null;
|
|
4298
|
+
for (let i = 0; i < frames.length; i++) {
|
|
4299
|
+
const frame = frames[i];
|
|
4300
|
+
const frameTime = frame.absoluteTimestamp || frame.timestamp;
|
|
4301
|
+
if (frameTime <= log.timestamp) {
|
|
4302
|
+
targetFrame = frame;
|
|
4303
|
+
} else {
|
|
4304
|
+
break; // Frames are in chronological order
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
// If no frame found before this log, associate with first frame
|
|
4309
|
+
if (!targetFrame && frames.length > 0) {
|
|
4310
|
+
targetFrame = frames[0];
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
if (targetFrame) {
|
|
4314
|
+
if (!targetFrame.logs) {
|
|
4315
|
+
targetFrame.logs = [];
|
|
4316
|
+
}
|
|
4317
|
+
targetFrame.logs.push(log);
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4321
|
+
// Update frames in IndexedDB with associated logs
|
|
4322
|
+
let updatedCount = 0;
|
|
4323
|
+
for (const frame of frames) {
|
|
4324
|
+
if (frame.logs && frame.logs.length > 0) {
|
|
4325
|
+
// Update this frame in IndexedDB
|
|
4326
|
+
await dataBuffer.updateBrowserFrame(frame.id, { logs: frame.logs });
|
|
4327
|
+
updatedCount++;
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
|
|
4331
|
+
console.log(`[Browser-Only] ✓ Associated logs with ${updatedCount} frames in IndexedDB`);
|
|
4332
|
+
}
|
|
4333
|
+
} catch (error) {
|
|
4334
|
+
console.error('[Browser-Only] Failed to associate logs with frames:', error);
|
|
4335
|
+
}
|
|
3852
4336
|
} else {
|
|
3853
4337
|
// No logs were captured during the session
|
|
3854
4338
|
console.warn('[Recording] No console logs were captured during this session');
|
|
@@ -3866,10 +4350,11 @@ async function handleFrameCaptureComplete(sessionData) {
|
|
|
3866
4350
|
priority: 0
|
|
3867
4351
|
});
|
|
3868
4352
|
}
|
|
3869
|
-
|
|
4353
|
+
|
|
3870
4354
|
// Save screen interactions to database
|
|
3871
|
-
|
|
3872
|
-
|
|
4355
|
+
// Skip server communication in browser-only mode
|
|
4356
|
+
if (screenInteractions.length > 0 && serverMode !== 'browser-only') {
|
|
4357
|
+
console.log(`[Server Mode] Saving ${screenInteractions.length} screen interactions to database`);
|
|
3873
4358
|
|
|
3874
4359
|
// Use comprehensive error handling for screen interactions save
|
|
3875
4360
|
const saveScreenInteractions = async (data, context) => {
|
|
@@ -3935,8 +4420,11 @@ async function handleFrameCaptureComplete(sessionData) {
|
|
|
3935
4420
|
} else {
|
|
3936
4421
|
console.error('[ErrorHandling] Failed to save screen interactions after comprehensive error handling');
|
|
3937
4422
|
}
|
|
4423
|
+
} else if (screenInteractions.length > 0 && serverMode === 'browser-only') {
|
|
4424
|
+
// Browser-only mode: screen interactions not needed
|
|
4425
|
+
console.log(`[Browser-Only] ✓ Skipping screen interactions save (browser-only mode)`);
|
|
3938
4426
|
}
|
|
3939
|
-
|
|
4427
|
+
|
|
3940
4428
|
// Stop session via session manager if available
|
|
3941
4429
|
if (sessionManager && currentSession?.sessionId === sessionData.sessionId && currentOwnerId) {
|
|
3942
4430
|
try {
|
|
@@ -3983,15 +4471,19 @@ async function handleFrameCaptureComplete(sessionData) {
|
|
|
3983
4471
|
priority: 2
|
|
3984
4472
|
});
|
|
3985
4473
|
|
|
3986
|
-
// Notify popup if open
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
4474
|
+
// Notify popup if open (but NOT for browser-only recordings - they're in IndexedDB)
|
|
4475
|
+
if (serverMode !== 'browser-only') {
|
|
4476
|
+
chrome.runtime.sendMessage({
|
|
4477
|
+
action: 'frameSessionComplete',
|
|
4478
|
+
sessionId: sessionData.sessionId,
|
|
4479
|
+
sessionName: currentSession?.settings?.sessionName || null,
|
|
4480
|
+
totalFrames: sessionData.totalFrames,
|
|
4481
|
+
duration: sessionData.duration,
|
|
4482
|
+
serverPort: recordingServerPort
|
|
4483
|
+
}).catch(() => {});
|
|
4484
|
+
} else {
|
|
4485
|
+
console.log('[Browser-Only] Skipping frameSessionComplete message - recording is in IndexedDB');
|
|
4486
|
+
}
|
|
3995
4487
|
|
|
3996
4488
|
// Notify content script to hide recording indicator
|
|
3997
4489
|
if (tabIdToNotify) {
|