@dynamicu/chromedebug-mcp 2.7.1 → 2.7.4
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 +18 -0
- package/README.md +226 -16
- package/chrome-extension/background.js +569 -64
- package/chrome-extension/browser-recording-manager.js +34 -0
- package/chrome-extension/content.js +438 -32
- package/chrome-extension/firebase-config.public-sw.js +1 -1
- package/chrome-extension/firebase-config.public.js +1 -1
- package/chrome-extension/frame-capture.js +31 -10
- package/chrome-extension/image-processor.js +193 -0
- package/chrome-extension/manifest.free.json +1 -1
- package/chrome-extension/options.html +2 -2
- package/chrome-extension/options.js +4 -4
- package/chrome-extension/popup.html +82 -4
- package/chrome-extension/popup.js +1106 -38
- package/chrome-extension/pro/frame-editor.html +259 -6
- package/chrome-extension/pro/frame-editor.js +959 -10
- package/chrome-extension/pro/video-exporter.js +917 -0
- package/chrome-extension/pro/video-player.js +545 -0
- package/dist/chromedebug-extension-free.zip +0 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +1 -1
- package/scripts/webpack.config.free.cjs +6 -0
- package/scripts/webpack.config.pro.cjs +6 -0
- package/src/chrome-controller.js +6 -6
- package/src/database.js +226 -39
- package/src/http-server.js +55 -11
- package/src/validation/schemas.js +20 -5
package/src/database.js
CHANGED
|
@@ -132,9 +132,20 @@ class ChromeDebugDatabase {
|
|
|
132
132
|
// Migrate from old location if needed
|
|
133
133
|
this.migrateIfNeeded();
|
|
134
134
|
|
|
135
|
+
// Check if database exists and is corrupted
|
|
136
|
+
if (fs.existsSync(this.dbPath)) {
|
|
137
|
+
const integrityStatus = this.checkDatabaseIntegrity();
|
|
138
|
+
if (!integrityStatus.ok) {
|
|
139
|
+
logger.warn(`[Database] Database corruption detected: ${integrityStatus.error}`);
|
|
140
|
+
this.handleCorruptedDatabase();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
135
144
|
// Open database connection
|
|
136
145
|
this.db = new Database(this.dbPath);
|
|
137
146
|
this.db.pragma('journal_mode = WAL'); // Enable WAL mode for better concurrent access
|
|
147
|
+
this.db.pragma('synchronous = NORMAL'); // Balance between safety and performance
|
|
148
|
+
this.db.pragma('foreign_keys = ON'); // Enforce foreign key constraints
|
|
138
149
|
|
|
139
150
|
// Create tables
|
|
140
151
|
this.createTables();
|
|
@@ -142,6 +153,64 @@ class ChromeDebugDatabase {
|
|
|
142
153
|
logger.debug(`ChromeDebug MCP database initialized at: ${this.dbPath}`);
|
|
143
154
|
}
|
|
144
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Check database integrity before opening
|
|
158
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
159
|
+
*/
|
|
160
|
+
checkDatabaseIntegrity() {
|
|
161
|
+
try {
|
|
162
|
+
const tempDb = new Database(this.dbPath, { readonly: true });
|
|
163
|
+
const result = tempDb.pragma('integrity_check');
|
|
164
|
+
tempDb.close();
|
|
165
|
+
|
|
166
|
+
if (result && result[0] && result[0].integrity_check === 'ok') {
|
|
167
|
+
return { ok: true };
|
|
168
|
+
} else {
|
|
169
|
+
const errorMsg = result?.[0]?.integrity_check || 'Unknown integrity error';
|
|
170
|
+
return { ok: false, error: errorMsg };
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return { ok: false, error: error.message };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Handle corrupted database by backing it up and creating a fresh one
|
|
179
|
+
*/
|
|
180
|
+
handleCorruptedDatabase() {
|
|
181
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
182
|
+
const backupPath = `${this.dbPath}.corrupted_${timestamp}`;
|
|
183
|
+
|
|
184
|
+
logger.warn(`[Database] Backing up corrupted database to: ${backupPath}`);
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
// Move corrupted database and associated files
|
|
188
|
+
if (fs.existsSync(this.dbPath)) {
|
|
189
|
+
fs.renameSync(this.dbPath, backupPath);
|
|
190
|
+
}
|
|
191
|
+
if (fs.existsSync(`${this.dbPath}-shm`)) {
|
|
192
|
+
fs.renameSync(`${this.dbPath}-shm`, `${backupPath}-shm`);
|
|
193
|
+
}
|
|
194
|
+
if (fs.existsSync(`${this.dbPath}-wal`)) {
|
|
195
|
+
fs.renameSync(`${this.dbPath}-wal`, `${backupPath}-wal`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
logger.info(`[Database] Corrupted database backed up. A fresh database will be created.`);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
logger.error(`[Database] Failed to backup corrupted database: ${error.message}`);
|
|
201
|
+
// Try to delete instead of rename
|
|
202
|
+
try {
|
|
203
|
+
fs.unlinkSync(this.dbPath);
|
|
204
|
+
if (fs.existsSync(`${this.dbPath}-shm`)) fs.unlinkSync(`${this.dbPath}-shm`);
|
|
205
|
+
if (fs.existsSync(`${this.dbPath}-wal`)) fs.unlinkSync(`${this.dbPath}-wal`);
|
|
206
|
+
logger.info(`[Database] Corrupted database removed. A fresh database will be created.`);
|
|
207
|
+
} catch (deleteError) {
|
|
208
|
+
logger.error(`[Database] Failed to remove corrupted database: ${deleteError.message}`);
|
|
209
|
+
throw new Error(`Cannot recover from database corruption: ${deleteError.message}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
145
214
|
createTables() {
|
|
146
215
|
// Recordings table for frame capture sessions
|
|
147
216
|
this.db.exec(`
|
|
@@ -223,6 +292,8 @@ class ChromeDebugDatabase {
|
|
|
223
292
|
xpath TEXT,
|
|
224
293
|
x INTEGER,
|
|
225
294
|
y INTEGER,
|
|
295
|
+
viewport_width INTEGER,
|
|
296
|
+
viewport_height INTEGER,
|
|
226
297
|
value TEXT,
|
|
227
298
|
text TEXT,
|
|
228
299
|
key TEXT,
|
|
@@ -234,6 +305,24 @@ class ChromeDebugDatabase {
|
|
|
234
305
|
)
|
|
235
306
|
`);
|
|
236
307
|
|
|
308
|
+
// Add viewport columns to screen_interactions if they don't exist (migration for existing DBs)
|
|
309
|
+
try {
|
|
310
|
+
this.db.exec(`ALTER TABLE screen_interactions ADD COLUMN viewport_width INTEGER`);
|
|
311
|
+
} catch (e) {
|
|
312
|
+
// Column already exists, ignore
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
this.db.exec(`ALTER TABLE screen_interactions ADD COLUMN viewport_height INTEGER`);
|
|
316
|
+
} catch (e) {
|
|
317
|
+
// Column already exists, ignore
|
|
318
|
+
}
|
|
319
|
+
// Add devicePixelRatio column (critical for Retina/HiDPI display scaling)
|
|
320
|
+
try {
|
|
321
|
+
this.db.exec(`ALTER TABLE screen_interactions ADD COLUMN device_pixel_ratio REAL`);
|
|
322
|
+
} catch (e) {
|
|
323
|
+
// Column already exists, ignore
|
|
324
|
+
}
|
|
325
|
+
|
|
237
326
|
// Workflow recordings table
|
|
238
327
|
this.db.exec(`
|
|
239
328
|
CREATE TABLE IF NOT EXISTS workflow_recordings (
|
|
@@ -556,7 +645,14 @@ class ChromeDebugDatabase {
|
|
|
556
645
|
logger.debug('[Database] Migrating workflow_recordings table to add screenshot_settings column');
|
|
557
646
|
this.db.exec(`ALTER TABLE workflow_recordings ADD COLUMN screenshot_settings TEXT`);
|
|
558
647
|
}
|
|
559
|
-
|
|
648
|
+
|
|
649
|
+
// Migration for recording_mode support (workflow vs screenshot)
|
|
650
|
+
const hasRecordingMode = columns.some(col => col.name === 'recording_mode');
|
|
651
|
+
if (!hasRecordingMode) {
|
|
652
|
+
logger.debug('[Database] Migrating workflow_recordings table to add recording_mode column');
|
|
653
|
+
this.db.exec(`ALTER TABLE workflow_recordings ADD COLUMN recording_mode TEXT DEFAULT 'workflow'`);
|
|
654
|
+
}
|
|
655
|
+
|
|
560
656
|
// Check if workflow_actions table has screenshot_data column
|
|
561
657
|
const actionColumns = this.db.pragma(`table_info(workflow_actions)`);
|
|
562
658
|
const hasScreenshotData = actionColumns.some(col => col.name === 'screenshot_data');
|
|
@@ -595,23 +691,30 @@ class ChromeDebugDatabase {
|
|
|
595
691
|
logger.debug('[Database] Migrating recordings table to add name column');
|
|
596
692
|
this.db.exec(`ALTER TABLE recordings ADD COLUMN name TEXT`);
|
|
597
693
|
}
|
|
694
|
+
|
|
695
|
+
// Migration for video mode support
|
|
696
|
+
const hasIsVideoMode = recordingsColumns.some(col => col.name === 'is_video_mode');
|
|
697
|
+
if (!hasIsVideoMode) {
|
|
698
|
+
logger.debug('[Database] Migrating recordings table to add is_video_mode column');
|
|
699
|
+
this.db.exec(`ALTER TABLE recordings ADD COLUMN is_video_mode INTEGER DEFAULT 0`);
|
|
700
|
+
}
|
|
598
701
|
}
|
|
599
702
|
|
|
600
703
|
// Store a recording session
|
|
601
|
-
storeRecording(sessionId, type = 'frame_capture', recordingType = 'continuous', userNote = null, name = null) {
|
|
704
|
+
storeRecording(sessionId, type = 'frame_capture', recordingType = 'continuous', userNote = null, name = null, isVideoMode = false) {
|
|
602
705
|
this.init();
|
|
603
706
|
|
|
604
707
|
const stmt = this.db.prepare(`
|
|
605
|
-
INSERT OR REPLACE INTO recordings (id, session_id, type, timestamp, updated_at, recording_type, user_note, name)
|
|
606
|
-
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?, ?)
|
|
708
|
+
INSERT OR REPLACE INTO recordings (id, session_id, type, timestamp, updated_at, recording_type, user_note, name, is_video_mode)
|
|
709
|
+
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?, ?, ?)
|
|
607
710
|
`);
|
|
608
711
|
|
|
609
712
|
// Use base ID instead of prefixed ID
|
|
610
713
|
const recordingId = sessionId;
|
|
611
714
|
const timestamp = Date.now();
|
|
612
715
|
|
|
613
|
-
logger.debug(`[Database] Storing recording: ${recordingId} for session: ${sessionId}, type: ${recordingType}, name: ${name || 'none'}`);
|
|
614
|
-
const result = stmt.run(recordingId, sessionId, type, timestamp, recordingType, userNote, name);
|
|
716
|
+
logger.debug(`[Database] Storing recording: ${recordingId} for session: ${sessionId}, type: ${recordingType}, name: ${name || 'none'}, isVideoMode: ${isVideoMode}`);
|
|
717
|
+
const result = stmt.run(recordingId, sessionId, type, timestamp, recordingType, userNote, name, isVideoMode ? 1 : 0);
|
|
615
718
|
logger.debug(`[Database] Recording stored with changes: ${result.changes}, lastInsertRowid: ${result.lastInsertRowid}`);
|
|
616
719
|
|
|
617
720
|
return recordingId;
|
|
@@ -671,18 +774,18 @@ class ChromeDebugDatabase {
|
|
|
671
774
|
*/
|
|
672
775
|
|
|
673
776
|
// Store frame batch
|
|
674
|
-
storeFrameBatch(sessionId, frames, name = null) {
|
|
777
|
+
storeFrameBatch(sessionId, frames, name = null, isVideoMode = false) {
|
|
675
778
|
this.init();
|
|
676
779
|
|
|
677
780
|
// Use base ID instead of prefixed ID
|
|
678
781
|
const recordingId = sessionId;
|
|
679
782
|
|
|
680
|
-
logger.debug(`[storeFrameBatch] DEATH SPIRAL FIX: Processing ${frames.length} frames for session ${sessionId}`);
|
|
783
|
+
logger.debug(`[storeFrameBatch] DEATH SPIRAL FIX: Processing ${frames.length} frames for session ${sessionId}, isVideoMode: ${isVideoMode}`);
|
|
681
784
|
|
|
682
785
|
// Ensure recording exists (but don't overwrite existing)
|
|
683
786
|
const existingRecording = this.getRecording(sessionId);
|
|
684
787
|
if (!existingRecording) {
|
|
685
|
-
this.storeRecording(sessionId, 'frame_capture', 'continuous', null, name);
|
|
788
|
+
this.storeRecording(sessionId, 'frame_capture', 'continuous', null, name, isVideoMode);
|
|
686
789
|
} else {
|
|
687
790
|
logger.debug(`[storeFrameBatch] Using existing recording: ${existingRecording.id}`);
|
|
688
791
|
// Update name if provided and different
|
|
@@ -691,6 +794,12 @@ class ChromeDebugDatabase {
|
|
|
691
794
|
updateStmt.run(name, sessionId);
|
|
692
795
|
logger.debug(`[storeFrameBatch] Updated recording name to: ${name}`);
|
|
693
796
|
}
|
|
797
|
+
// Update isVideoMode if provided and different (ensure video mode is properly tagged)
|
|
798
|
+
if (isVideoMode && existingRecording.is_video_mode !== 1) {
|
|
799
|
+
const updateModeStmt = this.db.prepare(`UPDATE recordings SET is_video_mode = ? WHERE id = ?`);
|
|
800
|
+
updateModeStmt.run(1, sessionId);
|
|
801
|
+
logger.debug(`[storeFrameBatch] Updated recording isVideoMode to: true`);
|
|
802
|
+
}
|
|
694
803
|
}
|
|
695
804
|
|
|
696
805
|
// Check existing frame count BEFORE insertion
|
|
@@ -1282,12 +1391,31 @@ class ChromeDebugDatabase {
|
|
|
1282
1391
|
|
|
1283
1392
|
// Get all interactions for this recording
|
|
1284
1393
|
const interactionsStmt = this.db.prepare(`
|
|
1285
|
-
SELECT * FROM screen_interactions
|
|
1394
|
+
SELECT * FROM screen_interactions
|
|
1286
1395
|
WHERE recording_id = ?
|
|
1287
1396
|
ORDER BY timestamp ASC
|
|
1288
1397
|
`);
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1398
|
+
const rawInteractions = interactionsStmt.all(recording.id);
|
|
1399
|
+
|
|
1400
|
+
// Transform to camelCase for API consistency
|
|
1401
|
+
const interactions = rawInteractions.map(i => ({
|
|
1402
|
+
id: i.id,
|
|
1403
|
+
index: i.interaction_index,
|
|
1404
|
+
type: i.type,
|
|
1405
|
+
selector: i.selector,
|
|
1406
|
+
xpath: i.xpath,
|
|
1407
|
+
x: i.x,
|
|
1408
|
+
y: i.y,
|
|
1409
|
+
viewportWidth: i.viewport_width,
|
|
1410
|
+
viewportHeight: i.viewport_height,
|
|
1411
|
+
devicePixelRatio: i.device_pixel_ratio, // Critical for Retina/HiDPI display scaling
|
|
1412
|
+
value: i.value,
|
|
1413
|
+
text: i.text,
|
|
1414
|
+
key: i.key,
|
|
1415
|
+
timestamp: i.timestamp,
|
|
1416
|
+
frameIndex: i.frame_index
|
|
1417
|
+
}));
|
|
1418
|
+
|
|
1291
1419
|
return {
|
|
1292
1420
|
type: 'frame_capture',
|
|
1293
1421
|
sessionId: recording.session_id,
|
|
@@ -1302,7 +1430,7 @@ class ChromeDebugDatabase {
|
|
|
1302
1430
|
this.init();
|
|
1303
1431
|
|
|
1304
1432
|
const stmt = this.db.prepare(`
|
|
1305
|
-
SELECT session_id, total_frames, timestamp, name
|
|
1433
|
+
SELECT session_id, total_frames, timestamp, name, is_video_mode
|
|
1306
1434
|
FROM recordings
|
|
1307
1435
|
WHERE type = 'frame_capture'
|
|
1308
1436
|
ORDER BY timestamp DESC
|
|
@@ -1313,7 +1441,8 @@ class ChromeDebugDatabase {
|
|
|
1313
1441
|
sessionId: r.session_id,
|
|
1314
1442
|
totalFrames: r.total_frames,
|
|
1315
1443
|
timestamp: r.timestamp,
|
|
1316
|
-
name: r.name
|
|
1444
|
+
name: r.name,
|
|
1445
|
+
isVideoMode: r.is_video_mode === 1
|
|
1317
1446
|
}));
|
|
1318
1447
|
}
|
|
1319
1448
|
|
|
@@ -1364,6 +1493,38 @@ class ChromeDebugDatabase {
|
|
|
1364
1493
|
return result.changes > 0;
|
|
1365
1494
|
}
|
|
1366
1495
|
|
|
1496
|
+
// Delete all screen recordings
|
|
1497
|
+
deleteAllScreenRecordings() {
|
|
1498
|
+
this.init();
|
|
1499
|
+
|
|
1500
|
+
const transaction = this.db.transaction(() => {
|
|
1501
|
+
// Delete all deferred logs
|
|
1502
|
+
const deleteDeferredLogsStmt = this.db.prepare(`DELETE FROM deferred_logs`);
|
|
1503
|
+
deleteDeferredLogsStmt.run();
|
|
1504
|
+
|
|
1505
|
+
// Delete all screen interactions
|
|
1506
|
+
const deleteInteractionsStmt = this.db.prepare(`DELETE FROM screen_interactions`);
|
|
1507
|
+
deleteInteractionsStmt.run();
|
|
1508
|
+
|
|
1509
|
+
// Delete all console logs (will cascade from frames deletion, but explicit for safety)
|
|
1510
|
+
const deleteLogsStmt = this.db.prepare(`DELETE FROM console_logs`);
|
|
1511
|
+
deleteLogsStmt.run();
|
|
1512
|
+
|
|
1513
|
+
// Delete all frames
|
|
1514
|
+
const deleteFramesStmt = this.db.prepare(`DELETE FROM frames`);
|
|
1515
|
+
deleteFramesStmt.run();
|
|
1516
|
+
|
|
1517
|
+
// Delete all recordings (screen recordings only, type = 'frame_capture')
|
|
1518
|
+
const deleteRecordingsStmt = this.db.prepare(`DELETE FROM recordings WHERE type = 'frame_capture'`);
|
|
1519
|
+
const result = deleteRecordingsStmt.run();
|
|
1520
|
+
|
|
1521
|
+
logger.debug(`[Database] Cleared screen recordings: ${result.changes} recordings deleted`);
|
|
1522
|
+
return result.changes;
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
return transaction();
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1367
1528
|
// Delete workflow recording
|
|
1368
1529
|
deleteWorkflowRecording(recordingId) {
|
|
1369
1530
|
this.init();
|
|
@@ -1400,29 +1561,30 @@ class ChromeDebugDatabase {
|
|
|
1400
1561
|
}
|
|
1401
1562
|
|
|
1402
1563
|
// Store workflow recording
|
|
1403
|
-
storeWorkflowRecording(sessionId, url, title, includeLogs = false, name = null, screenshotSettings = null) {
|
|
1564
|
+
storeWorkflowRecording(sessionId, url, title, includeLogs = false, name = null, screenshotSettings = null, recordingMode = 'workflow') {
|
|
1404
1565
|
this.init();
|
|
1405
|
-
|
|
1406
|
-
logger.debug('[Database] Storing workflow recording with name:', name, 'sessionId:', sessionId);
|
|
1407
|
-
|
|
1566
|
+
|
|
1567
|
+
logger.debug('[Database] Storing workflow recording with name:', name, 'sessionId:', sessionId, 'recordingMode:', recordingMode);
|
|
1568
|
+
|
|
1408
1569
|
const stmt = this.db.prepare(`
|
|
1409
|
-
INSERT OR REPLACE INTO workflow_recordings (id, session_id, name, url, title, timestamp, include_logs, screenshot_settings, updated_at)
|
|
1410
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
1570
|
+
INSERT OR REPLACE INTO workflow_recordings (id, session_id, name, url, title, timestamp, include_logs, screenshot_settings, recording_mode, updated_at)
|
|
1571
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
1411
1572
|
`);
|
|
1412
|
-
|
|
1573
|
+
|
|
1413
1574
|
// sessionId already contains workflow_ prefix from extension, use it directly
|
|
1414
1575
|
const recordingId = sessionId;
|
|
1415
1576
|
const timestamp = Date.now();
|
|
1416
1577
|
|
|
1417
1578
|
stmt.run(
|
|
1418
1579
|
recordingId,
|
|
1419
|
-
sessionId,
|
|
1580
|
+
sessionId,
|
|
1420
1581
|
name,
|
|
1421
|
-
url,
|
|
1422
|
-
title,
|
|
1423
|
-
timestamp,
|
|
1582
|
+
url,
|
|
1583
|
+
title,
|
|
1584
|
+
timestamp,
|
|
1424
1585
|
includeLogs ? 1 : 0,
|
|
1425
|
-
screenshotSettings ? JSON.stringify(screenshotSettings) : null
|
|
1586
|
+
screenshotSettings ? JSON.stringify(screenshotSettings) : null,
|
|
1587
|
+
recordingMode
|
|
1426
1588
|
);
|
|
1427
1589
|
return recordingId;
|
|
1428
1590
|
}
|
|
@@ -1726,16 +1888,32 @@ class ChromeDebugDatabase {
|
|
|
1726
1888
|
}
|
|
1727
1889
|
|
|
1728
1890
|
// List all workflow recordings
|
|
1729
|
-
listWorkflowRecordings() {
|
|
1891
|
+
listWorkflowRecordings(recordingModeFilter = null) {
|
|
1730
1892
|
this.init();
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
SELECT id, session_id, name, url, title, total_actions, timestamp, screenshot_settings
|
|
1734
|
-
FROM workflow_recordings
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1893
|
+
|
|
1894
|
+
let query = `
|
|
1895
|
+
SELECT id, session_id, name, url, title, total_actions, timestamp, screenshot_settings, recording_mode
|
|
1896
|
+
FROM workflow_recordings
|
|
1897
|
+
`;
|
|
1898
|
+
|
|
1899
|
+
// Add filter if specified
|
|
1900
|
+
// For 'workflow' filter, also match NULL values (old recordings before recording_mode was added)
|
|
1901
|
+
if (recordingModeFilter && recordingModeFilter !== 'all') {
|
|
1902
|
+
if (recordingModeFilter === 'workflow') {
|
|
1903
|
+
query += ` WHERE (recording_mode = ? OR recording_mode IS NULL)`;
|
|
1904
|
+
} else {
|
|
1905
|
+
query += ` WHERE recording_mode = ?`;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
query += ` ORDER BY timestamp DESC`;
|
|
1910
|
+
|
|
1911
|
+
const stmt = this.db.prepare(query);
|
|
1912
|
+
const results = recordingModeFilter && recordingModeFilter !== 'all'
|
|
1913
|
+
? stmt.all(recordingModeFilter)
|
|
1914
|
+
: stmt.all();
|
|
1915
|
+
|
|
1916
|
+
return results.map(r => ({
|
|
1739
1917
|
id: r.id,
|
|
1740
1918
|
session_id: r.session_id,
|
|
1741
1919
|
name: r.name,
|
|
@@ -1743,7 +1921,8 @@ class ChromeDebugDatabase {
|
|
|
1743
1921
|
title: r.title,
|
|
1744
1922
|
total_actions: r.total_actions,
|
|
1745
1923
|
timestamp: r.timestamp,
|
|
1746
|
-
screenshot_settings: r.screenshot_settings ? JSON.parse(r.screenshot_settings) : null
|
|
1924
|
+
screenshot_settings: r.screenshot_settings ? JSON.parse(r.screenshot_settings) : null,
|
|
1925
|
+
recording_mode: r.recording_mode || 'workflow'
|
|
1747
1926
|
}));
|
|
1748
1927
|
}
|
|
1749
1928
|
|
|
@@ -1951,11 +2130,11 @@ class ChromeDebugDatabase {
|
|
|
1951
2130
|
|
|
1952
2131
|
const stmt = this.db.prepare(`
|
|
1953
2132
|
INSERT OR REPLACE INTO screen_interactions
|
|
1954
|
-
(recording_id, interaction_index, type, selector, xpath, x, y, value, text, key, timestamp, frame_index,
|
|
2133
|
+
(recording_id, interaction_index, type, selector, xpath, x, y, viewport_width, viewport_height, device_pixel_ratio, value, text, key, timestamp, frame_index,
|
|
1955
2134
|
element_html, component_data, event_handlers, element_state, performance_metrics)
|
|
1956
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2135
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1957
2136
|
`);
|
|
1958
|
-
|
|
2137
|
+
|
|
1959
2138
|
const transaction = this.db.transaction(() => {
|
|
1960
2139
|
interactions.forEach((interaction, index) => {
|
|
1961
2140
|
stmt.run(
|
|
@@ -1966,6 +2145,9 @@ class ChromeDebugDatabase {
|
|
|
1966
2145
|
interaction.xpath || null,
|
|
1967
2146
|
interaction.x || null,
|
|
1968
2147
|
interaction.y || null,
|
|
2148
|
+
interaction.viewportWidth || null,
|
|
2149
|
+
interaction.viewportHeight || null,
|
|
2150
|
+
interaction.devicePixelRatio || null, // Critical for Retina/HiDPI display scaling
|
|
1969
2151
|
interaction.value || null,
|
|
1970
2152
|
interaction.text || null,
|
|
1971
2153
|
interaction.key || null,
|
|
@@ -2048,6 +2230,8 @@ class ChromeDebugDatabase {
|
|
|
2048
2230
|
xpath,
|
|
2049
2231
|
x,
|
|
2050
2232
|
y,
|
|
2233
|
+
viewport_width,
|
|
2234
|
+
viewport_height,
|
|
2051
2235
|
value,
|
|
2052
2236
|
text,
|
|
2053
2237
|
key,
|
|
@@ -2080,6 +2264,9 @@ class ChromeDebugDatabase {
|
|
|
2080
2264
|
xpath: interaction.xpath,
|
|
2081
2265
|
x: interaction.x,
|
|
2082
2266
|
y: interaction.y,
|
|
2267
|
+
viewportWidth: interaction.viewport_width,
|
|
2268
|
+
viewportHeight: interaction.viewport_height,
|
|
2269
|
+
devicePixelRatio: interaction.device_pixel_ratio, // Critical for Retina/HiDPI display scaling
|
|
2083
2270
|
value: interaction.value,
|
|
2084
2271
|
text: interaction.text,
|
|
2085
2272
|
key: interaction.key,
|
package/src/http-server.js
CHANGED
|
@@ -291,16 +291,16 @@ async function startHttpServer(chromeController = null, targetPort = null) {
|
|
|
291
291
|
authorize(PERMISSIONS.WORKFLOW_WRITE),
|
|
292
292
|
createValidator(workflowRecordingSchema),
|
|
293
293
|
async (req, res) => {
|
|
294
|
-
const { sessionId, url, title, includeLogs, actions, logs, functionTraces, name, screenshotSettings } = req.body;
|
|
294
|
+
const { sessionId, url, title, includeLogs, actions, logs, functionTraces, name, screenshotSettings, recordingMode } = req.body;
|
|
295
295
|
|
|
296
|
-
logger.info('[HTTP Server] Received workflow recording with name:', name);
|
|
296
|
+
logger.info('[HTTP Server] Received workflow recording with name:', name, 'recordingMode:', recordingMode || 'workflow');
|
|
297
297
|
logger.info('[HTTP Server] Function traces count:', functionTraces ? functionTraces.length : 0);
|
|
298
298
|
if (!sessionId || !actions) {
|
|
299
299
|
return res.status(400).json({ error: 'sessionId and actions are required' });
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
try {
|
|
303
|
-
const result = await activeController.storeWorkflowRecording(sessionId, url, title, includeLogs, actions, logs, name, screenshotSettings, functionTraces);
|
|
303
|
+
const result = await activeController.storeWorkflowRecording(sessionId, url, title, includeLogs, actions, logs, name, screenshotSettings, functionTraces, recordingMode || 'workflow');
|
|
304
304
|
res.json(result);
|
|
305
305
|
} catch (error) {
|
|
306
306
|
logger.error('Error storing workflow recording:', error);
|
|
@@ -331,7 +331,9 @@ async function startHttpServer(chromeController = null, targetPort = null) {
|
|
|
331
331
|
authorize(PERMISSIONS.WORKFLOW_READ),
|
|
332
332
|
async (req, res) => {
|
|
333
333
|
try {
|
|
334
|
-
|
|
334
|
+
// Support filtering by recording_mode: 'all', 'workflow', 'screenshot'
|
|
335
|
+
const recordingModeFilter = req.query.mode || null;
|
|
336
|
+
const result = await activeController.listWorkflowRecordings(recordingModeFilter);
|
|
335
337
|
res.json(result);
|
|
336
338
|
} catch (error) {
|
|
337
339
|
logger.error('Error listing workflow recordings:', error);
|
|
@@ -475,7 +477,7 @@ async function startHttpServer(chromeController = null, targetPort = null) {
|
|
|
475
477
|
logger.debug(' Raw body:', req.body);
|
|
476
478
|
|
|
477
479
|
// Handle both JSON and FormData
|
|
478
|
-
let sessionId, frames, sessionName;
|
|
480
|
+
let sessionId, frames, sessionName, isVideoMode;
|
|
479
481
|
|
|
480
482
|
if (req.headers['content-type']?.includes('application/json')) {
|
|
481
483
|
logger.debug(' Processing as JSON request');
|
|
@@ -494,21 +496,25 @@ async function startHttpServer(chromeController = null, targetPort = null) {
|
|
|
494
496
|
sessionId = value.sessionId;
|
|
495
497
|
frames = value.frames;
|
|
496
498
|
sessionName = value.sessionName || null;
|
|
499
|
+
isVideoMode = value.isVideoMode || false;
|
|
497
500
|
|
|
498
|
-
logger.debug(' JSON parsed successfully:', { sessionId, frameCount: frames?.length, sessionName });
|
|
501
|
+
logger.debug(' JSON parsed successfully:', { sessionId, frameCount: frames?.length, sessionName, isVideoMode });
|
|
499
502
|
} else {
|
|
500
503
|
logger.debug(' Processing as FormData request');
|
|
501
|
-
|
|
504
|
+
|
|
502
505
|
// FormData request - manual validation
|
|
503
506
|
sessionId = req.body.sessionId;
|
|
504
507
|
sessionName = req.body.sessionName || null;
|
|
508
|
+
// Parse isVideoMode from FormData (can be string 'true'/'false' or boolean)
|
|
509
|
+
isVideoMode = req.body.isVideoMode === 'true' || req.body.isVideoMode === true;
|
|
505
510
|
|
|
506
511
|
logger.debug(' SessionId from FormData:', sessionId);
|
|
507
512
|
logger.debug(' SessionName from FormData:', sessionName);
|
|
513
|
+
logger.debug(' IsVideoMode from FormData:', isVideoMode);
|
|
508
514
|
logger.debug(' Frames field type:', typeof req.body.frames);
|
|
509
515
|
logger.debug(' Frames field length:', req.body.frames?.length);
|
|
510
516
|
logger.debug(' Frames field preview:', req.body.frames?.substring(0, 200));
|
|
511
|
-
|
|
517
|
+
|
|
512
518
|
try {
|
|
513
519
|
if (!req.body.frames) {
|
|
514
520
|
logger.debug(' ❌ No frames field in FormData');
|
|
@@ -520,10 +526,10 @@ async function startHttpServer(chromeController = null, targetPort = null) {
|
|
|
520
526
|
logger.debug(' Parsed frames count:', frames?.length);
|
|
521
527
|
logger.debug(' First frame structure:', frames?.[0] ? Object.keys(frames[0]) : 'no frames');
|
|
522
528
|
}
|
|
523
|
-
|
|
529
|
+
|
|
524
530
|
logger.debug(' 🔄 Validating parsed FormData...');
|
|
525
531
|
// Validate the parsed data
|
|
526
|
-
const { error, value } = frameBatchSchema.validate({ sessionId, frames, sessionName });
|
|
532
|
+
const { error, value } = frameBatchSchema.validate({ sessionId, frames, sessionName, isVideoMode });
|
|
527
533
|
if (error) {
|
|
528
534
|
logger.debug(' ❌ FormData validation failed:', error.details);
|
|
529
535
|
return res.status(400).json({
|
|
@@ -537,6 +543,7 @@ async function startHttpServer(chromeController = null, targetPort = null) {
|
|
|
537
543
|
sessionId = value.sessionId;
|
|
538
544
|
frames = value.frames;
|
|
539
545
|
sessionName = value.sessionName;
|
|
546
|
+
isVideoMode = value.isVideoMode || false;
|
|
540
547
|
|
|
541
548
|
logger.debug(' ✅ FormData validation successful');
|
|
542
549
|
} catch (parseError) {
|
|
@@ -566,8 +573,9 @@ async function startHttpServer(chromeController = null, targetPort = null) {
|
|
|
566
573
|
}
|
|
567
574
|
|
|
568
575
|
logger.debug(' Session name:', sessionName || 'none');
|
|
576
|
+
logger.debug(' Is video mode:', isVideoMode);
|
|
569
577
|
|
|
570
|
-
const result = await activeController.storeFrameBatch(sessionId, frames, sessionName);
|
|
578
|
+
const result = await activeController.storeFrameBatch(sessionId, frames, sessionName, isVideoMode);
|
|
571
579
|
logger.debug(' ✅ Frame batch storage result:', result);
|
|
572
580
|
|
|
573
581
|
logger.debug(' Result recording ID:', result?.id);
|
|
@@ -1090,6 +1098,42 @@ async function startHttpServer(chromeController = null, targetPort = null) {
|
|
|
1090
1098
|
}
|
|
1091
1099
|
});
|
|
1092
1100
|
|
|
1101
|
+
// Delete all screen recordings
|
|
1102
|
+
app.delete('/chromedebug/recordings/all',
|
|
1103
|
+
authenticate,
|
|
1104
|
+
authorize(PERMISSIONS.WORKFLOW_DELETE),
|
|
1105
|
+
async (req, res) => {
|
|
1106
|
+
try {
|
|
1107
|
+
const deletedCount = activeController.database.deleteAllScreenRecordings();
|
|
1108
|
+
res.json({
|
|
1109
|
+
success: true,
|
|
1110
|
+
deletedCount,
|
|
1111
|
+
message: `Deleted ${deletedCount} screen recording(s)`
|
|
1112
|
+
});
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
logger.error('Error deleting all screen recordings:', error);
|
|
1115
|
+
res.status(500).json({ error: 'Failed to delete all screen recordings', details: error.message });
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
// Delete all workflow recordings
|
|
1120
|
+
app.delete('/chromedebug/workflow-recordings/all',
|
|
1121
|
+
authenticate,
|
|
1122
|
+
authorize(PERMISSIONS.WORKFLOW_DELETE),
|
|
1123
|
+
async (req, res) => {
|
|
1124
|
+
try {
|
|
1125
|
+
const deletedCount = activeController.database.clearWorkflowRecordings();
|
|
1126
|
+
res.json({
|
|
1127
|
+
success: true,
|
|
1128
|
+
deletedCount,
|
|
1129
|
+
message: `Deleted ${deletedCount} workflow recording(s)`
|
|
1130
|
+
});
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
logger.error('Error deleting all workflow recordings:', error);
|
|
1133
|
+
res.status(500).json({ error: 'Failed to delete all workflow recordings', details: error.message });
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1093
1137
|
// Save screen interactions for a recording
|
|
1094
1138
|
app.post('/chromedebug/screen-interactions/:sessionId',
|
|
1095
1139
|
authenticate,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import Joi from 'joi';
|
|
2
2
|
|
|
3
|
+
// Valid console log levels - must match what Chrome extension captures
|
|
4
|
+
// Chrome extension captures: log, error, warn, info, debug, trace, table, dir, group, groupEnd, time, timeEnd, count
|
|
5
|
+
// See: chrome-extension/console-interception-library.js line 228
|
|
6
|
+
const VALID_LOG_LEVELS = ['log', 'info', 'warn', 'error', 'debug', 'trace', 'table', 'dir', 'group', 'groupEnd', 'time', 'timeEnd', 'count'];
|
|
7
|
+
|
|
3
8
|
// Common validation patterns
|
|
4
9
|
const patterns = {
|
|
5
10
|
sessionId: Joi.string().pattern(/^[a-zA-Z0-9_-]+$/).min(1).max(100),
|
|
@@ -53,7 +58,7 @@ export const workflowRecordingSchema = Joi.object({
|
|
|
53
58
|
).required(),
|
|
54
59
|
logs: Joi.array().items(
|
|
55
60
|
Joi.object({
|
|
56
|
-
level: Joi.string().valid(
|
|
61
|
+
level: Joi.string().valid(...VALID_LOG_LEVELS).required(),
|
|
57
62
|
message: Joi.string().required(),
|
|
58
63
|
timestamp: Joi.number().required(),
|
|
59
64
|
args: Joi.array().optional()
|
|
@@ -76,13 +81,15 @@ export const workflowRecordingSchema = Joi.object({
|
|
|
76
81
|
format: Joi.string().valid('png', 'jpeg').optional().allow(null),
|
|
77
82
|
enabled: Joi.boolean().optional().allow(null),
|
|
78
83
|
maxWidth: Joi.number().integer().optional().allow(null)
|
|
79
|
-
}).optional().allow(null)
|
|
84
|
+
}).optional().allow(null),
|
|
85
|
+
recordingMode: Joi.string().valid('workflow', 'screenshot').optional().default('workflow')
|
|
80
86
|
});
|
|
81
87
|
|
|
82
88
|
// Frame batch schema
|
|
83
89
|
export const frameBatchSchema = Joi.object({
|
|
84
90
|
sessionId: patterns.sessionId.required(),
|
|
85
91
|
sessionName: Joi.string().max(200).optional().allow(null), // Optional session name for recordings
|
|
92
|
+
isVideoMode: Joi.boolean().optional().default(false), // Whether this is a video mode recording
|
|
86
93
|
frames: Joi.array().items(
|
|
87
94
|
Joi.object({
|
|
88
95
|
timestamp: Joi.number().required(),
|
|
@@ -91,9 +98,11 @@ export const frameBatchSchema = Joi.object({
|
|
|
91
98
|
consoleLog: Joi.string().optional(),
|
|
92
99
|
frameIndex: Joi.number().integer().min(0).optional(),
|
|
93
100
|
index: Joi.number().integer().min(0).optional(), // Chrome extension sends this
|
|
101
|
+
isSynthetic: Joi.boolean().optional(), // Synthetic frames for early log capture
|
|
102
|
+
isManual: Joi.boolean().optional(), // Manual snapshot frames
|
|
94
103
|
logs: Joi.array().items(
|
|
95
104
|
Joi.object({
|
|
96
|
-
level: Joi.string().valid(
|
|
105
|
+
level: Joi.string().valid(...VALID_LOG_LEVELS).required(),
|
|
97
106
|
message: Joi.string().required(),
|
|
98
107
|
timestamp: Joi.number().required(),
|
|
99
108
|
args: Joi.array().optional()
|
|
@@ -105,6 +114,8 @@ export const frameBatchSchema = Joi.object({
|
|
|
105
114
|
timestamp: Joi.number().required(),
|
|
106
115
|
x: Joi.number().when('type', { is: 'click', then: Joi.required(), otherwise: Joi.optional() }),
|
|
107
116
|
y: Joi.number().when('type', { is: 'click', then: Joi.required(), otherwise: Joi.optional() }),
|
|
117
|
+
viewportWidth: Joi.number().optional(), // For coordinate scaling during playback
|
|
118
|
+
viewportHeight: Joi.number().optional(), // For coordinate scaling during playback
|
|
108
119
|
target: Joi.string().optional(),
|
|
109
120
|
targetId: Joi.string().optional(),
|
|
110
121
|
scrollX: Joi.number().when('type', { is: 'scroll', then: Joi.required(), otherwise: Joi.optional() }),
|
|
@@ -121,7 +132,7 @@ export const associateLogsSchema = Joi.object({
|
|
|
121
132
|
sessionId: patterns.sessionId.required(),
|
|
122
133
|
logs: Joi.array().items(
|
|
123
134
|
Joi.object({
|
|
124
|
-
level: Joi.string().valid(
|
|
135
|
+
level: Joi.string().valid(...VALID_LOG_LEVELS).required(),
|
|
125
136
|
message: Joi.string().required(),
|
|
126
137
|
timestamp: Joi.number().required(),
|
|
127
138
|
args: Joi.array().optional()
|
|
@@ -134,7 +145,7 @@ export const streamLogsSchema = Joi.object({
|
|
|
134
145
|
sessionId: patterns.sessionId.required(),
|
|
135
146
|
logs: Joi.array().items(
|
|
136
147
|
Joi.object({
|
|
137
|
-
level: Joi.string().valid(
|
|
148
|
+
level: Joi.string().valid(...VALID_LOG_LEVELS).required(),
|
|
138
149
|
message: Joi.string().required(),
|
|
139
150
|
timestamp: Joi.number().required(),
|
|
140
151
|
sequence: Joi.number().integer().min(0).required(),
|
|
@@ -173,6 +184,10 @@ export const screenInteractionsSchema = Joi.object({
|
|
|
173
184
|
timestamp: Joi.number().required(),
|
|
174
185
|
x: Joi.number().optional(),
|
|
175
186
|
y: Joi.number().optional(),
|
|
187
|
+
pageX: Joi.number().optional(), // Document-relative coordinates
|
|
188
|
+
pageY: Joi.number().optional(), // Document-relative coordinates
|
|
189
|
+
viewportWidth: Joi.number().optional(), // For coordinate scaling during playback
|
|
190
|
+
viewportHeight: Joi.number().optional(), // For coordinate scaling during playback
|
|
176
191
|
key: Joi.string().optional(),
|
|
177
192
|
target: Joi.string().optional(),
|
|
178
193
|
// Basic element fields that were previously blocked
|