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