@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
package/src/database.js
ADDED
|
@@ -0,0 +1,2178 @@
|
|
|
1
|
+
// SQLite database module for ChromeDebug MCP recordings
|
|
2
|
+
import Database from 'better-sqlite3';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { ProjectManager } from './services/project-manager.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
// Initialize project manager for path resolution
|
|
11
|
+
const projectManager = new ProjectManager();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Gets the appropriate database path (always use global chromedebug.db)
|
|
15
|
+
* @returns {string} Database file path
|
|
16
|
+
*/
|
|
17
|
+
function getDatabasePath() {
|
|
18
|
+
// Always use global database for cross-project accessibility
|
|
19
|
+
const globalDbPath = path.join(__dirname, '../data/chromedebug.db');
|
|
20
|
+
|
|
21
|
+
// Ensure data directory exists
|
|
22
|
+
const dbDir = path.dirname(globalDbPath);
|
|
23
|
+
if (!fs.existsSync(dbDir)) {
|
|
24
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(`[Database] Using global database: ${globalDbPath}`);
|
|
28
|
+
return globalDbPath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Get the database path (project-local or global)
|
|
32
|
+
const DB_PATH = getDatabasePath();
|
|
33
|
+
|
|
34
|
+
class ChromePilotDatabase {
|
|
35
|
+
constructor() {
|
|
36
|
+
this.db = null;
|
|
37
|
+
this.initialized = false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
init() {
|
|
41
|
+
if (this.initialized) return;
|
|
42
|
+
|
|
43
|
+
// Ensure data directory exists
|
|
44
|
+
const dataDir = path.dirname(DB_PATH);
|
|
45
|
+
if (!fs.existsSync(dataDir)) {
|
|
46
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.db = new Database(DB_PATH);
|
|
50
|
+
this.db.pragma('journal_mode = WAL'); // Enable WAL mode for better concurrent access
|
|
51
|
+
|
|
52
|
+
// Create tables
|
|
53
|
+
this.createTables();
|
|
54
|
+
this.initialized = true;
|
|
55
|
+
console.log(`ChromeDebug MCP database initialized at: ${DB_PATH}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
createTables() {
|
|
59
|
+
// Recordings table for frame capture sessions
|
|
60
|
+
this.db.exec(`
|
|
61
|
+
CREATE TABLE IF NOT EXISTS recordings (
|
|
62
|
+
id TEXT PRIMARY KEY,
|
|
63
|
+
session_id TEXT NOT NULL,
|
|
64
|
+
type TEXT NOT NULL DEFAULT 'frame_capture',
|
|
65
|
+
timestamp INTEGER NOT NULL,
|
|
66
|
+
total_frames INTEGER DEFAULT 0,
|
|
67
|
+
name TEXT,
|
|
68
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
69
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
70
|
+
)
|
|
71
|
+
`);
|
|
72
|
+
|
|
73
|
+
// Frames table to store individual frames
|
|
74
|
+
this.db.exec(`
|
|
75
|
+
CREATE TABLE IF NOT EXISTS frames (
|
|
76
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
77
|
+
recording_id TEXT NOT NULL,
|
|
78
|
+
frame_index INTEGER NOT NULL,
|
|
79
|
+
timestamp INTEGER NOT NULL,
|
|
80
|
+
absolute_timestamp INTEGER NOT NULL,
|
|
81
|
+
image_data TEXT NOT NULL,
|
|
82
|
+
interactions TEXT DEFAULT '[]',
|
|
83
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
84
|
+
FOREIGN KEY (recording_id) REFERENCES recordings (id) ON DELETE CASCADE,
|
|
85
|
+
UNIQUE(recording_id, frame_index)
|
|
86
|
+
)
|
|
87
|
+
`);
|
|
88
|
+
|
|
89
|
+
// Add interactions column to existing frames table if it doesn't exist
|
|
90
|
+
try {
|
|
91
|
+
this.db.exec(`ALTER TABLE frames ADD COLUMN interactions TEXT DEFAULT '[]'`);
|
|
92
|
+
console.log('[Database] Added interactions column to frames table');
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// Column already exists, ignore error
|
|
95
|
+
if (!error.message.includes('duplicate column name')) {
|
|
96
|
+
console.warn('[Database] Unexpected error adding interactions column:', error.message);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Console logs table
|
|
101
|
+
this.db.exec(`
|
|
102
|
+
CREATE TABLE IF NOT EXISTS console_logs (
|
|
103
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
104
|
+
frame_id INTEGER NOT NULL,
|
|
105
|
+
level TEXT NOT NULL,
|
|
106
|
+
message TEXT NOT NULL,
|
|
107
|
+
relative_time INTEGER DEFAULT 0,
|
|
108
|
+
sequence_number INTEGER DEFAULT 0,
|
|
109
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
110
|
+
FOREIGN KEY (frame_id) REFERENCES frames (id) ON DELETE CASCADE
|
|
111
|
+
)
|
|
112
|
+
`);
|
|
113
|
+
|
|
114
|
+
// Deferred logs table for logs that arrive before frames are created
|
|
115
|
+
this.db.exec(`
|
|
116
|
+
CREATE TABLE IF NOT EXISTS deferred_logs (
|
|
117
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
118
|
+
session_id TEXT NOT NULL,
|
|
119
|
+
level TEXT NOT NULL,
|
|
120
|
+
message TEXT NOT NULL,
|
|
121
|
+
timestamp INTEGER NOT NULL,
|
|
122
|
+
sequence_number INTEGER NOT NULL,
|
|
123
|
+
args TEXT,
|
|
124
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
125
|
+
)
|
|
126
|
+
`);
|
|
127
|
+
|
|
128
|
+
// Screen interactions table for recording user interactions during screen recordings
|
|
129
|
+
this.db.exec(`
|
|
130
|
+
CREATE TABLE IF NOT EXISTS screen_interactions (
|
|
131
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
132
|
+
recording_id TEXT NOT NULL,
|
|
133
|
+
interaction_index INTEGER NOT NULL,
|
|
134
|
+
type TEXT NOT NULL,
|
|
135
|
+
selector TEXT,
|
|
136
|
+
xpath TEXT,
|
|
137
|
+
x INTEGER,
|
|
138
|
+
y INTEGER,
|
|
139
|
+
value TEXT,
|
|
140
|
+
text TEXT,
|
|
141
|
+
key TEXT,
|
|
142
|
+
timestamp INTEGER NOT NULL,
|
|
143
|
+
frame_index INTEGER,
|
|
144
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
145
|
+
FOREIGN KEY (recording_id) REFERENCES recordings (id) ON DELETE CASCADE,
|
|
146
|
+
UNIQUE(recording_id, interaction_index)
|
|
147
|
+
)
|
|
148
|
+
`);
|
|
149
|
+
|
|
150
|
+
// Workflow recordings table
|
|
151
|
+
this.db.exec(`
|
|
152
|
+
CREATE TABLE IF NOT EXISTS workflow_recordings (
|
|
153
|
+
id TEXT PRIMARY KEY,
|
|
154
|
+
session_id TEXT NOT NULL,
|
|
155
|
+
name TEXT,
|
|
156
|
+
url TEXT NOT NULL,
|
|
157
|
+
title TEXT,
|
|
158
|
+
timestamp INTEGER NOT NULL,
|
|
159
|
+
total_actions INTEGER DEFAULT 0,
|
|
160
|
+
include_logs BOOLEAN DEFAULT FALSE,
|
|
161
|
+
screenshot_settings TEXT,
|
|
162
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
163
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
164
|
+
)
|
|
165
|
+
`);
|
|
166
|
+
|
|
167
|
+
// Workflow actions table
|
|
168
|
+
this.db.exec(`
|
|
169
|
+
CREATE TABLE IF NOT EXISTS workflow_actions (
|
|
170
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
171
|
+
workflow_id TEXT NOT NULL,
|
|
172
|
+
action_index INTEGER NOT NULL,
|
|
173
|
+
type TEXT NOT NULL,
|
|
174
|
+
selector TEXT NOT NULL,
|
|
175
|
+
x INTEGER,
|
|
176
|
+
y INTEGER,
|
|
177
|
+
value TEXT,
|
|
178
|
+
text TEXT,
|
|
179
|
+
placeholder TEXT,
|
|
180
|
+
timestamp INTEGER NOT NULL,
|
|
181
|
+
screenshot_data TEXT,
|
|
182
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
183
|
+
FOREIGN KEY (workflow_id) REFERENCES workflow_recordings (id) ON DELETE CASCADE,
|
|
184
|
+
UNIQUE(workflow_id, action_index)
|
|
185
|
+
)
|
|
186
|
+
`);
|
|
187
|
+
|
|
188
|
+
// Workflow logs table
|
|
189
|
+
this.db.exec(`
|
|
190
|
+
CREATE TABLE IF NOT EXISTS workflow_logs (
|
|
191
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
192
|
+
workflow_id TEXT NOT NULL,
|
|
193
|
+
level TEXT NOT NULL,
|
|
194
|
+
message TEXT NOT NULL,
|
|
195
|
+
timestamp INTEGER NOT NULL,
|
|
196
|
+
relative_time INTEGER DEFAULT 0,
|
|
197
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
198
|
+
FOREIGN KEY (workflow_id) REFERENCES workflow_recordings (id) ON DELETE CASCADE
|
|
199
|
+
)
|
|
200
|
+
`);
|
|
201
|
+
|
|
202
|
+
// Add columns for function trace data if they don't exist
|
|
203
|
+
try {
|
|
204
|
+
// Check if component column exists
|
|
205
|
+
const columns = this.db.pragma('table_info(workflow_actions)');
|
|
206
|
+
const hasComponent = columns.some(col => col.name === 'component');
|
|
207
|
+
const hasArgs = columns.some(col => col.name === 'args');
|
|
208
|
+
const hasStack = columns.some(col => col.name === 'stack');
|
|
209
|
+
const hasElementHtml = columns.some(col => col.name === 'element_html');
|
|
210
|
+
const hasComponentData = columns.some(col => col.name === 'component_data');
|
|
211
|
+
const hasEventHandlers = columns.some(col => col.name === 'event_handlers');
|
|
212
|
+
const hasElementState = columns.some(col => col.name === 'element_state');
|
|
213
|
+
const hasPerformanceMetrics = columns.some(col => col.name === 'performance_metrics');
|
|
214
|
+
|
|
215
|
+
if (!hasComponent) {
|
|
216
|
+
this.db.exec('ALTER TABLE workflow_actions ADD COLUMN component TEXT');
|
|
217
|
+
console.log('[Database] Added component column to workflow_actions');
|
|
218
|
+
}
|
|
219
|
+
if (!hasArgs) {
|
|
220
|
+
this.db.exec('ALTER TABLE workflow_actions ADD COLUMN args TEXT');
|
|
221
|
+
console.log('[Database] Added args column to workflow_actions');
|
|
222
|
+
}
|
|
223
|
+
if (!hasStack) {
|
|
224
|
+
this.db.exec('ALTER TABLE workflow_actions ADD COLUMN stack TEXT');
|
|
225
|
+
console.log('[Database] Added stack column to workflow_actions');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Add enhanced click tracking columns
|
|
229
|
+
if (!hasElementHtml) {
|
|
230
|
+
this.db.exec('ALTER TABLE workflow_actions ADD COLUMN element_html TEXT');
|
|
231
|
+
console.log('[Database] Added element_html column to workflow_actions');
|
|
232
|
+
}
|
|
233
|
+
if (!hasComponentData) {
|
|
234
|
+
this.db.exec('ALTER TABLE workflow_actions ADD COLUMN component_data TEXT');
|
|
235
|
+
console.log('[Database] Added component_data column to workflow_actions');
|
|
236
|
+
}
|
|
237
|
+
if (!hasEventHandlers) {
|
|
238
|
+
this.db.exec('ALTER TABLE workflow_actions ADD COLUMN event_handlers TEXT');
|
|
239
|
+
console.log('[Database] Added event_handlers column to workflow_actions');
|
|
240
|
+
}
|
|
241
|
+
if (!hasElementState) {
|
|
242
|
+
this.db.exec('ALTER TABLE workflow_actions ADD COLUMN element_state TEXT');
|
|
243
|
+
console.log('[Database] Added element_state column to workflow_actions');
|
|
244
|
+
}
|
|
245
|
+
if (!hasPerformanceMetrics) {
|
|
246
|
+
this.db.exec('ALTER TABLE workflow_actions ADD COLUMN performance_metrics TEXT');
|
|
247
|
+
console.log('[Database] Added performance_metrics column to workflow_actions');
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
// Columns might already exist, that's ok
|
|
251
|
+
console.log('[Database] Function trace columns already exist or migration failed:', error.message);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Add enhanced columns to screen_interactions table
|
|
255
|
+
try {
|
|
256
|
+
const screenInteractionsColumns = this.db.pragma('table_info(screen_interactions)');
|
|
257
|
+
const hasScreenElementHtml = screenInteractionsColumns.some(col => col.name === 'element_html');
|
|
258
|
+
const hasScreenComponentData = screenInteractionsColumns.some(col => col.name === 'component_data');
|
|
259
|
+
const hasScreenEventHandlers = screenInteractionsColumns.some(col => col.name === 'event_handlers');
|
|
260
|
+
const hasScreenElementState = screenInteractionsColumns.some(col => col.name === 'element_state');
|
|
261
|
+
const hasScreenPerformanceMetrics = screenInteractionsColumns.some(col => col.name === 'performance_metrics');
|
|
262
|
+
|
|
263
|
+
// Add enhanced click tracking columns to screen_interactions
|
|
264
|
+
if (!hasScreenElementHtml) {
|
|
265
|
+
this.db.exec('ALTER TABLE screen_interactions ADD COLUMN element_html TEXT');
|
|
266
|
+
console.log('[Database] Added element_html column to screen_interactions');
|
|
267
|
+
}
|
|
268
|
+
if (!hasScreenComponentData) {
|
|
269
|
+
this.db.exec('ALTER TABLE screen_interactions ADD COLUMN component_data TEXT');
|
|
270
|
+
console.log('[Database] Added component_data column to screen_interactions');
|
|
271
|
+
}
|
|
272
|
+
if (!hasScreenEventHandlers) {
|
|
273
|
+
this.db.exec('ALTER TABLE screen_interactions ADD COLUMN event_handlers TEXT');
|
|
274
|
+
console.log('[Database] Added event_handlers column to screen_interactions');
|
|
275
|
+
}
|
|
276
|
+
if (!hasScreenElementState) {
|
|
277
|
+
this.db.exec('ALTER TABLE screen_interactions ADD COLUMN element_state TEXT');
|
|
278
|
+
console.log('[Database] Added element_state column to screen_interactions');
|
|
279
|
+
}
|
|
280
|
+
if (!hasScreenPerformanceMetrics) {
|
|
281
|
+
this.db.exec('ALTER TABLE screen_interactions ADD COLUMN performance_metrics TEXT');
|
|
282
|
+
console.log('[Database] Added performance_metrics column to screen_interactions');
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
// Columns might already exist, that's ok
|
|
286
|
+
console.log('[Database] Screen interactions enhanced columns already exist or migration failed:', error.message);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Restore points table
|
|
290
|
+
this.db.exec(`
|
|
291
|
+
CREATE TABLE IF NOT EXISTS restore_points (
|
|
292
|
+
id TEXT PRIMARY KEY,
|
|
293
|
+
workflow_id TEXT NOT NULL,
|
|
294
|
+
action_index INTEGER,
|
|
295
|
+
timestamp INTEGER NOT NULL,
|
|
296
|
+
url TEXT NOT NULL,
|
|
297
|
+
title TEXT,
|
|
298
|
+
dom_snapshot TEXT NOT NULL,
|
|
299
|
+
scroll_x INTEGER DEFAULT 0,
|
|
300
|
+
scroll_y INTEGER DEFAULT 0,
|
|
301
|
+
local_storage TEXT,
|
|
302
|
+
session_storage TEXT,
|
|
303
|
+
cookies TEXT,
|
|
304
|
+
console_logs TEXT,
|
|
305
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
306
|
+
FOREIGN KEY (workflow_id) REFERENCES workflow_recordings (id) ON DELETE CASCADE
|
|
307
|
+
)
|
|
308
|
+
`);
|
|
309
|
+
|
|
310
|
+
// Server instances table to track single-server mode
|
|
311
|
+
this.db.exec(`
|
|
312
|
+
CREATE TABLE IF NOT EXISTS server_instances (
|
|
313
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
314
|
+
pid INTEGER NOT NULL,
|
|
315
|
+
mode TEXT NOT NULL,
|
|
316
|
+
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
317
|
+
UNIQUE(pid)
|
|
318
|
+
)
|
|
319
|
+
`);
|
|
320
|
+
|
|
321
|
+
// Full data recording configuration table
|
|
322
|
+
this.db.exec(`
|
|
323
|
+
CREATE TABLE IF NOT EXISTS full_recording_configs (
|
|
324
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
325
|
+
recording_id TEXT NOT NULL,
|
|
326
|
+
instrumentation_level INTEGER DEFAULT 0,
|
|
327
|
+
capture_console BOOLEAN DEFAULT TRUE,
|
|
328
|
+
capture_dom BOOLEAN DEFAULT FALSE,
|
|
329
|
+
capture_functions BOOLEAN DEFAULT FALSE,
|
|
330
|
+
capture_variables BOOLEAN DEFAULT FALSE,
|
|
331
|
+
capture_network BOOLEAN DEFAULT FALSE,
|
|
332
|
+
performance_threshold_ms INTEGER DEFAULT 10,
|
|
333
|
+
max_data_size_mb INTEGER DEFAULT 100,
|
|
334
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
335
|
+
FOREIGN KEY (recording_id) REFERENCES recordings (id) ON DELETE CASCADE
|
|
336
|
+
)
|
|
337
|
+
`);
|
|
338
|
+
|
|
339
|
+
// Execution traces table for function call tracking
|
|
340
|
+
this.db.exec(`
|
|
341
|
+
CREATE TABLE IF NOT EXISTS execution_traces (
|
|
342
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
343
|
+
recording_id TEXT NOT NULL,
|
|
344
|
+
trace_index INTEGER NOT NULL,
|
|
345
|
+
timestamp INTEGER NOT NULL,
|
|
346
|
+
function_name TEXT,
|
|
347
|
+
function_source TEXT,
|
|
348
|
+
arguments TEXT,
|
|
349
|
+
return_value TEXT,
|
|
350
|
+
error TEXT,
|
|
351
|
+
call_stack TEXT,
|
|
352
|
+
execution_time_ms INTEGER,
|
|
353
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
354
|
+
FOREIGN KEY (recording_id) REFERENCES recordings (id) ON DELETE CASCADE,
|
|
355
|
+
UNIQUE(recording_id, trace_index)
|
|
356
|
+
)
|
|
357
|
+
`);
|
|
358
|
+
|
|
359
|
+
// Variable states table for runtime variable tracking
|
|
360
|
+
this.db.exec(`
|
|
361
|
+
CREATE TABLE IF NOT EXISTS variable_states (
|
|
362
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
363
|
+
recording_id TEXT NOT NULL,
|
|
364
|
+
timestamp INTEGER NOT NULL,
|
|
365
|
+
scope_chain TEXT,
|
|
366
|
+
variables TEXT,
|
|
367
|
+
heap_snapshot BLOB,
|
|
368
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
369
|
+
FOREIGN KEY (recording_id) REFERENCES recordings (id) ON DELETE CASCADE
|
|
370
|
+
)
|
|
371
|
+
`);
|
|
372
|
+
|
|
373
|
+
// DOM mutations table for DOM change tracking
|
|
374
|
+
this.db.exec(`
|
|
375
|
+
CREATE TABLE IF NOT EXISTS dom_mutations (
|
|
376
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
377
|
+
recording_id TEXT NOT NULL,
|
|
378
|
+
timestamp INTEGER NOT NULL,
|
|
379
|
+
mutation_type TEXT,
|
|
380
|
+
target_selector TEXT,
|
|
381
|
+
target_xpath TEXT,
|
|
382
|
+
old_value TEXT,
|
|
383
|
+
new_value TEXT,
|
|
384
|
+
attributes_changed TEXT,
|
|
385
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
386
|
+
FOREIGN KEY (recording_id) REFERENCES recordings (id) ON DELETE CASCADE
|
|
387
|
+
)
|
|
388
|
+
`);
|
|
389
|
+
|
|
390
|
+
// Network requests table for API tracking
|
|
391
|
+
this.db.exec(`
|
|
392
|
+
CREATE TABLE IF NOT EXISTS network_requests (
|
|
393
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
394
|
+
recording_id TEXT NOT NULL,
|
|
395
|
+
timestamp INTEGER NOT NULL,
|
|
396
|
+
request_id TEXT,
|
|
397
|
+
method TEXT,
|
|
398
|
+
url TEXT,
|
|
399
|
+
headers TEXT,
|
|
400
|
+
body TEXT,
|
|
401
|
+
response_status INTEGER,
|
|
402
|
+
response_headers TEXT,
|
|
403
|
+
response_body TEXT,
|
|
404
|
+
duration_ms INTEGER,
|
|
405
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
406
|
+
FOREIGN KEY (recording_id) REFERENCES recordings (id) ON DELETE CASCADE
|
|
407
|
+
)
|
|
408
|
+
`);
|
|
409
|
+
|
|
410
|
+
// Performance metrics table
|
|
411
|
+
this.db.exec(`
|
|
412
|
+
CREATE TABLE IF NOT EXISTS performance_metrics (
|
|
413
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
414
|
+
recording_id TEXT NOT NULL,
|
|
415
|
+
timestamp INTEGER NOT NULL,
|
|
416
|
+
metric_type TEXT,
|
|
417
|
+
metric_name TEXT,
|
|
418
|
+
metric_value REAL,
|
|
419
|
+
metadata TEXT,
|
|
420
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
421
|
+
FOREIGN KEY (recording_id) REFERENCES recordings (id) ON DELETE CASCADE
|
|
422
|
+
)
|
|
423
|
+
`);
|
|
424
|
+
|
|
425
|
+
// Create indexes for better performance
|
|
426
|
+
this.db.exec(`
|
|
427
|
+
CREATE INDEX IF NOT EXISTS idx_recordings_session_id ON recordings (session_id);
|
|
428
|
+
CREATE INDEX IF NOT EXISTS idx_recordings_timestamp ON recordings (timestamp);
|
|
429
|
+
CREATE INDEX IF NOT EXISTS idx_frames_recording_id ON frames (recording_id);
|
|
430
|
+
CREATE INDEX IF NOT EXISTS idx_frames_frame_index ON frames (recording_id, frame_index);
|
|
431
|
+
CREATE INDEX IF NOT EXISTS idx_console_logs_frame_id ON console_logs (frame_id);
|
|
432
|
+
CREATE INDEX IF NOT EXISTS idx_screen_interactions_recording_id ON screen_interactions (recording_id);
|
|
433
|
+
CREATE INDEX IF NOT EXISTS idx_screen_interactions_timestamp ON screen_interactions (timestamp);
|
|
434
|
+
CREATE INDEX IF NOT EXISTS idx_screen_interactions_frame_index ON screen_interactions (recording_id, frame_index);
|
|
435
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_recordings_session_id ON workflow_recordings (session_id);
|
|
436
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_recordings_timestamp ON workflow_recordings (timestamp);
|
|
437
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_actions_workflow_id ON workflow_actions (workflow_id);
|
|
438
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_actions_action_index ON workflow_actions (workflow_id, action_index);
|
|
439
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_logs_workflow_id ON workflow_logs (workflow_id);
|
|
440
|
+
CREATE INDEX IF NOT EXISTS idx_restore_points_workflow_id ON restore_points (workflow_id);
|
|
441
|
+
CREATE INDEX IF NOT EXISTS idx_restore_points_timestamp ON restore_points (timestamp);
|
|
442
|
+
|
|
443
|
+
-- Indexes for new full recording tables
|
|
444
|
+
CREATE INDEX IF NOT EXISTS idx_full_recording_configs_recording_id ON full_recording_configs (recording_id);
|
|
445
|
+
CREATE INDEX IF NOT EXISTS idx_execution_traces_recording_id ON execution_traces (recording_id, timestamp);
|
|
446
|
+
CREATE INDEX IF NOT EXISTS idx_execution_traces_function ON execution_traces (function_name);
|
|
447
|
+
CREATE INDEX IF NOT EXISTS idx_variable_states_recording_id ON variable_states (recording_id, timestamp);
|
|
448
|
+
CREATE INDEX IF NOT EXISTS idx_dom_mutations_recording_id ON dom_mutations (recording_id, timestamp);
|
|
449
|
+
CREATE INDEX IF NOT EXISTS idx_network_requests_recording_id ON network_requests (recording_id, timestamp);
|
|
450
|
+
CREATE INDEX IF NOT EXISTS idx_performance_metrics_recording_id ON performance_metrics (recording_id, timestamp);
|
|
451
|
+
`);
|
|
452
|
+
|
|
453
|
+
// Run migrations to update existing databases
|
|
454
|
+
this.runMigrations();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
runMigrations() {
|
|
458
|
+
// Check if workflow_recordings table has name column
|
|
459
|
+
const columns = this.db.pragma(`table_info(workflow_recordings)`);
|
|
460
|
+
const hasNameColumn = columns.some(col => col.name === 'name');
|
|
461
|
+
const hasScreenshotSettings = columns.some(col => col.name === 'screenshot_settings');
|
|
462
|
+
|
|
463
|
+
if (!hasNameColumn) {
|
|
464
|
+
console.log('[Database] Migrating workflow_recordings table to add name column');
|
|
465
|
+
this.db.exec(`ALTER TABLE workflow_recordings ADD COLUMN name TEXT`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!hasScreenshotSettings) {
|
|
469
|
+
console.log('[Database] Migrating workflow_recordings table to add screenshot_settings column');
|
|
470
|
+
this.db.exec(`ALTER TABLE workflow_recordings ADD COLUMN screenshot_settings TEXT`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Check if workflow_actions table has screenshot_data column
|
|
474
|
+
const actionColumns = this.db.pragma(`table_info(workflow_actions)`);
|
|
475
|
+
const hasScreenshotData = actionColumns.some(col => col.name === 'screenshot_data');
|
|
476
|
+
|
|
477
|
+
if (!hasScreenshotData) {
|
|
478
|
+
console.log('[Database] Migrating workflow_actions table to add screenshot_data column');
|
|
479
|
+
this.db.exec(`ALTER TABLE workflow_actions ADD COLUMN screenshot_data TEXT`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Check if console_logs table has sequence_number column
|
|
483
|
+
const logColumns = this.db.pragma(`table_info(console_logs)`);
|
|
484
|
+
const hasSequenceNumber = logColumns.some(col => col.name === 'sequence_number');
|
|
485
|
+
|
|
486
|
+
if (!hasSequenceNumber) {
|
|
487
|
+
console.log('[Database] Migrating console_logs table to add sequence_number column');
|
|
488
|
+
this.db.exec(`ALTER TABLE console_logs ADD COLUMN sequence_number INTEGER DEFAULT 0`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Migration for snapshot feature support
|
|
492
|
+
const recordingsColumns = this.db.pragma(`table_info(recordings)`);
|
|
493
|
+
const hasRecordingType = recordingsColumns.some(col => col.name === 'recording_type');
|
|
494
|
+
const hasUserNote = recordingsColumns.some(col => col.name === 'user_note');
|
|
495
|
+
const hasName = recordingsColumns.some(col => col.name === 'name');
|
|
496
|
+
|
|
497
|
+
if (!hasRecordingType) {
|
|
498
|
+
console.log('[Database] Migrating recordings table to add recording_type column');
|
|
499
|
+
this.db.exec(`ALTER TABLE recordings ADD COLUMN recording_type TEXT DEFAULT 'continuous'`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (!hasUserNote) {
|
|
503
|
+
console.log('[Database] Migrating recordings table to add user_note column');
|
|
504
|
+
this.db.exec(`ALTER TABLE recordings ADD COLUMN user_note TEXT`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (!hasName) {
|
|
508
|
+
console.log('[Database] Migrating recordings table to add name column');
|
|
509
|
+
this.db.exec(`ALTER TABLE recordings ADD COLUMN name TEXT`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Store a recording session
|
|
514
|
+
storeRecording(sessionId, type = 'frame_capture', recordingType = 'continuous', userNote = null, name = null) {
|
|
515
|
+
this.init();
|
|
516
|
+
|
|
517
|
+
const stmt = this.db.prepare(`
|
|
518
|
+
INSERT OR REPLACE INTO recordings (id, session_id, type, timestamp, updated_at, recording_type, user_note, name)
|
|
519
|
+
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?, ?)
|
|
520
|
+
`);
|
|
521
|
+
|
|
522
|
+
// Use base ID instead of prefixed ID
|
|
523
|
+
const recordingId = sessionId;
|
|
524
|
+
const timestamp = Date.now();
|
|
525
|
+
|
|
526
|
+
console.log(`[Database] Storing recording: ${recordingId} for session: ${sessionId}, type: ${recordingType}, name: ${name || 'none'}`);
|
|
527
|
+
const result = stmt.run(recordingId, sessionId, type, timestamp, recordingType, userNote, name);
|
|
528
|
+
console.log(`[Database] Recording stored with changes: ${result.changes}, lastInsertRowid: ${result.lastInsertRowid}`);
|
|
529
|
+
|
|
530
|
+
return recordingId;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/*
|
|
534
|
+
* SNAPSHOT FEATURE DISABLED (2025-10-01)
|
|
535
|
+
*
|
|
536
|
+
* Snapshot database methods disabled - see SNAPSHOT_FEATURE_DISABLED.md
|
|
537
|
+
*
|
|
538
|
+
* WHY DISABLED:
|
|
539
|
+
* - Snapshots without console logs are just screenshots (users can do this natively)
|
|
540
|
+
* - Console log capture requires always-on monitoring (privacy concern)
|
|
541
|
+
* - Core value proposition (screenshot + searchable logs) cannot be achieved cleanly
|
|
542
|
+
*
|
|
543
|
+
* NOTE: Database schema (recording_type column) is preserved to maintain data integrity.
|
|
544
|
+
* Existing snapshot data in the database is not deleted.
|
|
545
|
+
*
|
|
546
|
+
* TO RE-ENABLE:
|
|
547
|
+
* 1. Implement privacy-conscious always-on log monitoring
|
|
548
|
+
* 2. Uncomment these methods
|
|
549
|
+
* 3. Re-enable chrome-controller.js methods
|
|
550
|
+
* 4. Re-enable HTTP endpoint in http-server.js
|
|
551
|
+
* 5. Re-enable extension UI and handlers
|
|
552
|
+
*/
|
|
553
|
+
/*
|
|
554
|
+
storeSnapshot(sessionId, frameData, userNote = null) {
|
|
555
|
+
this.init();
|
|
556
|
+
|
|
557
|
+
// Create the recording as a snapshot type
|
|
558
|
+
const recordingId = this.storeRecording(sessionId, 'frame_capture', 'snapshot', userNote);
|
|
559
|
+
|
|
560
|
+
// Store the single frame
|
|
561
|
+
if (frameData) {
|
|
562
|
+
this.storeFrameBatch(sessionId, [frameData]);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
console.log(`[Database] Snapshot stored: ${recordingId} with note: ${userNote || 'none'}`);
|
|
566
|
+
return recordingId;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
getSnapshots(limit = 50) {
|
|
570
|
+
this.init();
|
|
571
|
+
|
|
572
|
+
const stmt = this.db.prepare(`
|
|
573
|
+
SELECT r.*, COUNT(f.id) as frame_count
|
|
574
|
+
FROM recordings r
|
|
575
|
+
LEFT JOIN frames f ON f.recording_id = r.id
|
|
576
|
+
WHERE r.recording_type = 'snapshot'
|
|
577
|
+
GROUP BY r.id
|
|
578
|
+
ORDER BY r.timestamp DESC
|
|
579
|
+
LIMIT ?
|
|
580
|
+
`);
|
|
581
|
+
|
|
582
|
+
return stmt.all(limit);
|
|
583
|
+
}
|
|
584
|
+
*/
|
|
585
|
+
|
|
586
|
+
// Store frame batch
|
|
587
|
+
storeFrameBatch(sessionId, frames, name = null) {
|
|
588
|
+
this.init();
|
|
589
|
+
|
|
590
|
+
// Use base ID instead of prefixed ID
|
|
591
|
+
const recordingId = sessionId;
|
|
592
|
+
|
|
593
|
+
console.log(`[storeFrameBatch] DEATH SPIRAL FIX: Processing ${frames.length} frames for session ${sessionId}`);
|
|
594
|
+
|
|
595
|
+
// Ensure recording exists (but don't overwrite existing)
|
|
596
|
+
const existingRecording = this.getRecording(sessionId);
|
|
597
|
+
if (!existingRecording) {
|
|
598
|
+
this.storeRecording(sessionId, 'frame_capture', 'continuous', null, name);
|
|
599
|
+
} else {
|
|
600
|
+
console.log(`[storeFrameBatch] Using existing recording: ${existingRecording.id}`);
|
|
601
|
+
// Update name if provided and different
|
|
602
|
+
if (name && name !== existingRecording.name) {
|
|
603
|
+
const updateStmt = this.db.prepare(`UPDATE recordings SET name = ? WHERE id = ?`);
|
|
604
|
+
updateStmt.run(name, sessionId);
|
|
605
|
+
console.log(`[storeFrameBatch] Updated recording name to: ${name}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Check existing frame count BEFORE insertion
|
|
610
|
+
const preCountStmt = this.db.prepare(`SELECT COUNT(*) as count FROM frames WHERE recording_id = ?`);
|
|
611
|
+
const preCount = preCountStmt.get(recordingId).count;
|
|
612
|
+
console.log(`[storeFrameBatch] PRE-INSERT: ${preCount} existing frames in database`);
|
|
613
|
+
|
|
614
|
+
// Use INSERT OR REPLACE instead of INSERT OR IGNORE to handle updates
|
|
615
|
+
const frameStmt = this.db.prepare(`
|
|
616
|
+
INSERT OR REPLACE INTO frames (recording_id, frame_index, timestamp, absolute_timestamp, image_data, interactions)
|
|
617
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
618
|
+
`);
|
|
619
|
+
|
|
620
|
+
const transaction = this.db.transaction((frames) => {
|
|
621
|
+
let insertedCount = 0;
|
|
622
|
+
let skippedCount = 0;
|
|
623
|
+
|
|
624
|
+
for (let i = 0; i < frames.length; i++) {
|
|
625
|
+
const frame = frames[i];
|
|
626
|
+
// Use frame.index from extension (properly tracked per session), fallback to i for backward compatibility
|
|
627
|
+
const frameIndex = frame.index !== undefined ? frame.index : i;
|
|
628
|
+
|
|
629
|
+
console.log(`[storeFrameBatch] Inserting frame ${frameIndex}: timestamp=${frame.timestamp}, absoluteTimestamp=${frame.absoluteTimestamp}`);
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
// Serialize interactions as JSON
|
|
633
|
+
const interactionsJson = JSON.stringify(frame.interactions || []);
|
|
634
|
+
|
|
635
|
+
const result = frameStmt.run(
|
|
636
|
+
recordingId,
|
|
637
|
+
frameIndex,
|
|
638
|
+
frame.timestamp || Date.now(),
|
|
639
|
+
frame.absoluteTimestamp || Date.now(),
|
|
640
|
+
frame.imageData,
|
|
641
|
+
interactionsJson
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
if (result.changes > 0) {
|
|
645
|
+
insertedCount++;
|
|
646
|
+
console.log(`[storeFrameBatch] SUCCESS: Frame ${frameIndex} inserted/updated`);
|
|
647
|
+
} else {
|
|
648
|
+
skippedCount++;
|
|
649
|
+
console.log(`[storeFrameBatch] WARNING: Frame ${frameIndex} not changed`);
|
|
650
|
+
}
|
|
651
|
+
} catch (error) {
|
|
652
|
+
console.error(`[storeFrameBatch] ERROR inserting frame ${frameIndex}:`, error.message);
|
|
653
|
+
throw error; // Fail fast on SQL errors
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
console.log(`[storeFrameBatch] Transaction complete: ${insertedCount} inserted, ${skippedCount} skipped`);
|
|
658
|
+
|
|
659
|
+
// Update total frames count
|
|
660
|
+
const updateStmt = this.db.prepare(`
|
|
661
|
+
UPDATE recordings
|
|
662
|
+
SET total_frames = (SELECT COUNT(*) FROM frames WHERE recording_id = ?),
|
|
663
|
+
updated_at = CURRENT_TIMESTAMP
|
|
664
|
+
WHERE id = ?
|
|
665
|
+
`);
|
|
666
|
+
updateStmt.run(recordingId, recordingId);
|
|
667
|
+
|
|
668
|
+
// Debug: Check the actual count after update
|
|
669
|
+
const finalCountStmt = this.db.prepare(`SELECT COUNT(*) as count FROM frames WHERE recording_id = ?`);
|
|
670
|
+
const finalCount = finalCountStmt.get(recordingId).count;
|
|
671
|
+
console.log(`[storeFrameBatch] FINAL: ${finalCount} frames in database (was ${preCount})`);
|
|
672
|
+
|
|
673
|
+
return { insertedCount, skippedCount, finalCount };
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
const result = transaction(frames);
|
|
677
|
+
console.log(`[storeFrameBatch] DEATH SPIRAL RESOLVED: ${result.insertedCount}/${frames.length} frames stored successfully`);
|
|
678
|
+
|
|
679
|
+
// Verify what was actually stored
|
|
680
|
+
const verifyStmt = this.db.prepare(`
|
|
681
|
+
SELECT frame_index, timestamp FROM frames
|
|
682
|
+
WHERE recording_id = ?
|
|
683
|
+
ORDER BY frame_index ASC
|
|
684
|
+
`);
|
|
685
|
+
const storedFrames = verifyStmt.all(recordingId);
|
|
686
|
+
console.log(`[storeFrameBatch] VERIFICATION: ${storedFrames.length} frames with indices: [${storedFrames.map(f => f.frame_index).join(', ')}]`);
|
|
687
|
+
|
|
688
|
+
if (storedFrames.length === 0) {
|
|
689
|
+
console.error(`[storeFrameBatch] DEATH SPIRAL DETECTED: NO FRAMES STORED! Check SQL constraints and frame data.`);
|
|
690
|
+
throw new Error(`Frame storage failed: 0 frames stored for session ${sessionId}`);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return this.getRecording(recordingId);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Associate logs with frames
|
|
697
|
+
associateLogsWithFrames(sessionId, logs) {
|
|
698
|
+
this.init();
|
|
699
|
+
|
|
700
|
+
// Use base ID instead of prefixed ID
|
|
701
|
+
const recordingId = sessionId;
|
|
702
|
+
|
|
703
|
+
// Clear existing logs for this recording
|
|
704
|
+
const clearLogsStmt = this.db.prepare(`
|
|
705
|
+
DELETE FROM console_logs
|
|
706
|
+
WHERE frame_id IN (SELECT id FROM frames WHERE recording_id = ?)
|
|
707
|
+
`);
|
|
708
|
+
clearLogsStmt.run(recordingId);
|
|
709
|
+
|
|
710
|
+
// Get all frames sorted by timestamp
|
|
711
|
+
const framesStmt = this.db.prepare(`
|
|
712
|
+
SELECT id, frame_index, absolute_timestamp
|
|
713
|
+
FROM frames
|
|
714
|
+
WHERE recording_id = ?
|
|
715
|
+
ORDER BY absolute_timestamp ASC
|
|
716
|
+
`);
|
|
717
|
+
const frames = framesStmt.all(recordingId);
|
|
718
|
+
|
|
719
|
+
console.log(`[associateLogsWithFrames] Found ${frames.length} frames for log association`);
|
|
720
|
+
console.log(`[associateLogsWithFrames] Frame indices found: [${frames.map(f => f.frame_index).join(', ')}]`);
|
|
721
|
+
|
|
722
|
+
// Debug: Check total frames before processing
|
|
723
|
+
const countStmt = this.db.prepare(`SELECT COUNT(*) as count FROM frames WHERE recording_id = ?`);
|
|
724
|
+
const frameCount = countStmt.get(recordingId).count;
|
|
725
|
+
console.log(`[associateLogsWithFrames] Total frames in database before log association: ${frameCount}`);
|
|
726
|
+
|
|
727
|
+
if (frames.length === 0) {
|
|
728
|
+
return { success: false, message: 'No frames found for session' };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Sort logs by timestamp
|
|
732
|
+
logs.sort((a, b) => a.timestamp - b.timestamp);
|
|
733
|
+
|
|
734
|
+
const logStmt = this.db.prepare(`
|
|
735
|
+
INSERT INTO console_logs (frame_id, level, message, relative_time)
|
|
736
|
+
VALUES (?, ?, ?, ?)
|
|
737
|
+
`);
|
|
738
|
+
|
|
739
|
+
const transaction = this.db.transaction((logs, frames) => {
|
|
740
|
+
let associatedCount = 0;
|
|
741
|
+
let frameBucketIndex = 0;
|
|
742
|
+
|
|
743
|
+
for (const log of logs) {
|
|
744
|
+
// Find the correct frame bucket for this log
|
|
745
|
+
while (frameBucketIndex < frames.length &&
|
|
746
|
+
frames[frameBucketIndex].absolute_timestamp < log.timestamp) {
|
|
747
|
+
frameBucketIndex++;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (frameBucketIndex < frames.length) {
|
|
751
|
+
const frame = frames[frameBucketIndex];
|
|
752
|
+
const relativeTime = log.timestamp - frame.absolute_timestamp;
|
|
753
|
+
|
|
754
|
+
// Truncate large log messages before storing
|
|
755
|
+
let message = log.message;
|
|
756
|
+
const maxMessageLength = 10000; // 10KB limit per log entry
|
|
757
|
+
|
|
758
|
+
if (message && message.length > maxMessageLength) {
|
|
759
|
+
// Check if it's a base64 image that was already truncated by the extension
|
|
760
|
+
if (message.includes('[Base64 Image:') && message.includes('truncated...]')) {
|
|
761
|
+
// Already truncated by extension, keep as is
|
|
762
|
+
} else {
|
|
763
|
+
// Additional truncation for any other large content
|
|
764
|
+
message = message.substring(0, maxMessageLength) + `... [DB TRUNCATED: ${message.length} total bytes]`;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
logStmt.run(frame.id, log.level, message, relativeTime);
|
|
769
|
+
associatedCount++;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return associatedCount;
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
const associatedCount = transaction(logs, frames);
|
|
777
|
+
|
|
778
|
+
console.log(`Associated ${associatedCount} of ${logs.length} logs with frames`);
|
|
779
|
+
|
|
780
|
+
// Debug: Check total frames after processing
|
|
781
|
+
const finalFrameCount = countStmt.get(recordingId).count;
|
|
782
|
+
console.log(`[associateLogsWithFrames] Total frames in database after log association: ${finalFrameCount}`);
|
|
783
|
+
|
|
784
|
+
return {
|
|
785
|
+
success: true,
|
|
786
|
+
sessionId: sessionId,
|
|
787
|
+
logsReceived: logs.length,
|
|
788
|
+
logsAssociated: associatedCount
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Stream logs to frames - optimized for real-time batched streaming
|
|
793
|
+
streamLogsToFrames(sessionId, logs) {
|
|
794
|
+
this.init();
|
|
795
|
+
|
|
796
|
+
// Use base ID instead of prefixed ID
|
|
797
|
+
const recordingId = sessionId;
|
|
798
|
+
|
|
799
|
+
console.log(`[streamLogsToFrames] Processing ${logs.length} logs for session: ${sessionId}`);
|
|
800
|
+
|
|
801
|
+
// Get all frames sorted by timestamp for efficient association
|
|
802
|
+
const framesStmt = this.db.prepare(`
|
|
803
|
+
SELECT id, frame_index, absolute_timestamp
|
|
804
|
+
FROM frames
|
|
805
|
+
WHERE recording_id = ?
|
|
806
|
+
ORDER BY absolute_timestamp ASC
|
|
807
|
+
`);
|
|
808
|
+
const frames = framesStmt.all(recordingId);
|
|
809
|
+
|
|
810
|
+
if (frames.length === 0) {
|
|
811
|
+
console.log(`[streamLogsToFrames] No frames found for session: ${sessionId}, storing ${logs.length} logs as deferred`);
|
|
812
|
+
return this.storeDeferredLogs(sessionId, logs);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Sort logs by sequence number for proper ordering
|
|
816
|
+
logs.sort((a, b) => a.sequence - b.sequence);
|
|
817
|
+
|
|
818
|
+
const logStmt = this.db.prepare(`
|
|
819
|
+
INSERT INTO console_logs (frame_id, level, message, relative_time, sequence_number)
|
|
820
|
+
VALUES (?, ?, ?, ?, ?)
|
|
821
|
+
`);
|
|
822
|
+
|
|
823
|
+
const transaction = this.db.transaction((logs, frames) => {
|
|
824
|
+
let associatedCount = 0;
|
|
825
|
+
|
|
826
|
+
for (const log of logs) {
|
|
827
|
+
// Find the best frame for this log using binary search approach
|
|
828
|
+
let targetFrame = null;
|
|
829
|
+
let minTimeDiff = Infinity;
|
|
830
|
+
|
|
831
|
+
// For each log, find the frame with the closest timestamp
|
|
832
|
+
for (let i = 0; i < frames.length; i++) {
|
|
833
|
+
const frame = frames[i];
|
|
834
|
+
const timeDiff = Math.abs(log.timestamp - frame.absolute_timestamp);
|
|
835
|
+
|
|
836
|
+
// If this frame is closer than previous best, use it
|
|
837
|
+
if (timeDiff < minTimeDiff) {
|
|
838
|
+
minTimeDiff = timeDiff;
|
|
839
|
+
targetFrame = frame;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// If we found a frame that's very close (within 50ms), use it
|
|
843
|
+
if (timeDiff <= 50) {
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Fallback: if no close frame found, use temporal ordering
|
|
849
|
+
if (!targetFrame || minTimeDiff > 1000) {
|
|
850
|
+
// Find frame that occurs before or at the log timestamp
|
|
851
|
+
for (let i = frames.length - 1; i >= 0; i--) {
|
|
852
|
+
if (frames[i].absolute_timestamp <= log.timestamp) {
|
|
853
|
+
targetFrame = frames[i];
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// If still no frame, use the first available frame
|
|
859
|
+
if (!targetFrame && frames.length > 0) {
|
|
860
|
+
targetFrame = frames[0];
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (targetFrame) {
|
|
865
|
+
const relativeTime = log.timestamp - targetFrame.absolute_timestamp;
|
|
866
|
+
|
|
867
|
+
// Truncate message if too long to prevent DB issues
|
|
868
|
+
let message = log.message;
|
|
869
|
+
const maxMessageLength = 50000; // 50KB limit
|
|
870
|
+
if (message && message.length > maxMessageLength) {
|
|
871
|
+
message = message.substring(0, maxMessageLength) + `... [STREAM TRUNCATED: ${message.length} total bytes]`;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Insert with sequence number for ordering
|
|
875
|
+
logStmt.run(targetFrame.id, log.level, message, relativeTime, log.sequence);
|
|
876
|
+
associatedCount++;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return associatedCount;
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
const associatedCount = transaction(logs, frames);
|
|
884
|
+
|
|
885
|
+
console.log(`[streamLogsToFrames] Streamed ${associatedCount} of ${logs.length} logs to frames`);
|
|
886
|
+
|
|
887
|
+
return {
|
|
888
|
+
success: true,
|
|
889
|
+
sessionId: sessionId,
|
|
890
|
+
logsReceived: logs.length,
|
|
891
|
+
logsStreamed: associatedCount,
|
|
892
|
+
method: 'stream'
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Store logs temporarily when frames don't exist yet
|
|
897
|
+
storeDeferredLogs(sessionId, logs) {
|
|
898
|
+
this.init();
|
|
899
|
+
|
|
900
|
+
console.log(`[storeDeferredLogs] Storing ${logs.length} deferred logs for session: ${sessionId}`);
|
|
901
|
+
|
|
902
|
+
const deferredLogStmt = this.db.prepare(`
|
|
903
|
+
INSERT INTO deferred_logs (session_id, level, message, timestamp, sequence_number, args)
|
|
904
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
905
|
+
`);
|
|
906
|
+
|
|
907
|
+
const transaction = this.db.transaction((logs) => {
|
|
908
|
+
let stored = 0;
|
|
909
|
+
for (const log of logs) {
|
|
910
|
+
try {
|
|
911
|
+
deferredLogStmt.run(
|
|
912
|
+
sessionId,
|
|
913
|
+
log.level,
|
|
914
|
+
log.message,
|
|
915
|
+
log.timestamp,
|
|
916
|
+
log.sequence || 0,
|
|
917
|
+
log.args ? JSON.stringify(log.args) : null
|
|
918
|
+
);
|
|
919
|
+
stored++;
|
|
920
|
+
} catch (error) {
|
|
921
|
+
console.error(`[storeDeferredLogs] Failed to store deferred log:`, error, log);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return stored;
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
const storedCount = transaction(logs);
|
|
928
|
+
|
|
929
|
+
console.log(`[storeDeferredLogs] Stored ${storedCount} of ${logs.length} deferred logs`);
|
|
930
|
+
|
|
931
|
+
return {
|
|
932
|
+
success: true,
|
|
933
|
+
sessionId: sessionId,
|
|
934
|
+
logsReceived: logs.length,
|
|
935
|
+
logsDeferred: storedCount,
|
|
936
|
+
method: 'deferred'
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Associate deferred logs with frames when frames become available
|
|
941
|
+
associateDeferredLogs(sessionId) {
|
|
942
|
+
this.init();
|
|
943
|
+
|
|
944
|
+
console.log(`[associateDeferredLogs] Checking for deferred logs for session: ${sessionId}`);
|
|
945
|
+
|
|
946
|
+
// Get deferred logs for this session
|
|
947
|
+
const deferredLogsStmt = this.db.prepare(`
|
|
948
|
+
SELECT level, message, timestamp, sequence_number, args
|
|
949
|
+
FROM deferred_logs
|
|
950
|
+
WHERE session_id = ?
|
|
951
|
+
ORDER BY sequence_number ASC
|
|
952
|
+
`);
|
|
953
|
+
const deferredLogs = deferredLogsStmt.all(sessionId);
|
|
954
|
+
|
|
955
|
+
if (deferredLogs.length === 0) {
|
|
956
|
+
console.log(`[associateDeferredLogs] No deferred logs found for session: ${sessionId}`);
|
|
957
|
+
return { success: true, logsAssociated: 0 };
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
console.log(`[associateDeferredLogs] Found ${deferredLogs.length} deferred logs, attempting to associate with frames`);
|
|
961
|
+
|
|
962
|
+
// Convert deferred logs back to the format expected by streamLogsToFrames
|
|
963
|
+
const logs = deferredLogs.map(log => ({
|
|
964
|
+
level: log.level,
|
|
965
|
+
message: log.message,
|
|
966
|
+
timestamp: log.timestamp,
|
|
967
|
+
sequence: log.sequence_number,
|
|
968
|
+
args: log.args ? JSON.parse(log.args) : []
|
|
969
|
+
}));
|
|
970
|
+
|
|
971
|
+
// Try to associate with frames (this will work if frames now exist)
|
|
972
|
+
const result = this.streamLogsToFrames(sessionId, logs);
|
|
973
|
+
|
|
974
|
+
if (result.success && result.method !== 'deferred') {
|
|
975
|
+
// Successfully associated - clean up deferred logs
|
|
976
|
+
const cleanupStmt = this.db.prepare(`DELETE FROM deferred_logs WHERE session_id = ?`);
|
|
977
|
+
cleanupStmt.run(sessionId);
|
|
978
|
+
console.log(`[associateDeferredLogs] Successfully associated ${result.logsStreamed} logs and cleaned up deferred storage`);
|
|
979
|
+
|
|
980
|
+
return {
|
|
981
|
+
success: true,
|
|
982
|
+
logsAssociated: result.logsStreamed,
|
|
983
|
+
logsDeferred: deferredLogs.length
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
console.log(`[associateDeferredLogs] Frames still not available for session: ${sessionId}`);
|
|
988
|
+
return { success: false, message: 'Frames still not available' };
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Get recording by ID
|
|
992
|
+
getRecording(recordingId) {
|
|
993
|
+
this.init();
|
|
994
|
+
|
|
995
|
+
console.log(`[getRecording] Looking for recording with ID: ${recordingId}`);
|
|
996
|
+
|
|
997
|
+
// Handle both old format (frame_<id>) and new format (base id)
|
|
998
|
+
let baseId = recordingId;
|
|
999
|
+
if (recordingId.startsWith('frame_')) {
|
|
1000
|
+
baseId = recordingId.substring(6); // Remove 'frame_' prefix
|
|
1001
|
+
console.log(`[getRecording] Converting old format ID ${recordingId} to base ID: ${baseId}`);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Priority order: exact id match, then session_id match with frames, then any session_id match
|
|
1005
|
+
// Check both the original recordingId and the baseId to handle dual compatibility
|
|
1006
|
+
const stmt = this.db.prepare(`
|
|
1007
|
+
SELECT * FROM recordings
|
|
1008
|
+
WHERE id = ? OR id = ? OR session_id = ? OR session_id = ?
|
|
1009
|
+
ORDER BY
|
|
1010
|
+
CASE
|
|
1011
|
+
WHEN id = ? THEN 1
|
|
1012
|
+
WHEN id = ? THEN 2
|
|
1013
|
+
WHEN session_id = ? THEN 3
|
|
1014
|
+
ELSE 4
|
|
1015
|
+
END,
|
|
1016
|
+
total_frames DESC,
|
|
1017
|
+
created_at DESC
|
|
1018
|
+
LIMIT 1
|
|
1019
|
+
`);
|
|
1020
|
+
const recording = stmt.get(recordingId, baseId, recordingId, baseId, recordingId, baseId, recordingId);
|
|
1021
|
+
|
|
1022
|
+
if (recording) {
|
|
1023
|
+
console.log(`[getRecording] Found recording with ID: ${recording.id}, total_frames: ${recording.total_frames}`);
|
|
1024
|
+
return recording;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
console.log(`[getRecording] No recording found for: ${recordingId} or base ID: ${baseId}`);
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Get frame session info
|
|
1032
|
+
getFrameSessionInfo(sessionId) {
|
|
1033
|
+
this.init();
|
|
1034
|
+
|
|
1035
|
+
const recording = this.getRecording(sessionId);
|
|
1036
|
+
if (!recording) return null;
|
|
1037
|
+
|
|
1038
|
+
const frameTimestampsStmt = this.db.prepare(`
|
|
1039
|
+
SELECT timestamp FROM frames
|
|
1040
|
+
WHERE recording_id = ?
|
|
1041
|
+
ORDER BY frame_index ASC
|
|
1042
|
+
`);
|
|
1043
|
+
const frameTimestamps = frameTimestampsStmt.all(recording.id).map(f => f.timestamp);
|
|
1044
|
+
|
|
1045
|
+
return {
|
|
1046
|
+
sessionId: recording.session_id,
|
|
1047
|
+
totalFrames: recording.total_frames,
|
|
1048
|
+
timestamp: recording.timestamp,
|
|
1049
|
+
frameTimestamps: frameTimestamps
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// Get specific frame
|
|
1054
|
+
getFrame(sessionId, frameIndex) {
|
|
1055
|
+
this.init();
|
|
1056
|
+
|
|
1057
|
+
const recording = this.getRecording(sessionId);
|
|
1058
|
+
if (!recording) return null;
|
|
1059
|
+
|
|
1060
|
+
const frameStmt = this.db.prepare(`
|
|
1061
|
+
SELECT * FROM frames
|
|
1062
|
+
WHERE recording_id = ? AND frame_index = ?
|
|
1063
|
+
`);
|
|
1064
|
+
const frame = frameStmt.get(recording.id, frameIndex);
|
|
1065
|
+
|
|
1066
|
+
if (!frame) return null;
|
|
1067
|
+
|
|
1068
|
+
// Get console logs for this frame
|
|
1069
|
+
const logsStmt = this.db.prepare(`
|
|
1070
|
+
SELECT level, message, relative_time
|
|
1071
|
+
FROM console_logs
|
|
1072
|
+
WHERE frame_id = ?
|
|
1073
|
+
ORDER BY relative_time ASC
|
|
1074
|
+
`);
|
|
1075
|
+
const rawLogs = logsStmt.all(frame.id);
|
|
1076
|
+
|
|
1077
|
+
// Convert snake_case field names to camelCase for frontend compatibility
|
|
1078
|
+
const logs = rawLogs.map(log => ({
|
|
1079
|
+
level: log.level,
|
|
1080
|
+
message: log.message,
|
|
1081
|
+
relativeTime: log.relative_time
|
|
1082
|
+
}));
|
|
1083
|
+
|
|
1084
|
+
// Deserialize interactions from the frame record (legacy embedded interactions)
|
|
1085
|
+
let embeddedInteractions = [];
|
|
1086
|
+
try {
|
|
1087
|
+
embeddedInteractions = JSON.parse(frame.interactions || '[]');
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
console.warn(`[getFrame] Failed to parse embedded interactions for frame ${frameIndex}:`, error);
|
|
1090
|
+
embeddedInteractions = [];
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Get timestamp-associated interactions using the new method
|
|
1094
|
+
const associatedInteractionsResult = this.getInteractionsForFrame(
|
|
1095
|
+
recording.id,
|
|
1096
|
+
frame.timestamp || frame.absolute_timestamp, // Use relative timestamp for proper association
|
|
1097
|
+
500 // ±500ms window
|
|
1098
|
+
);
|
|
1099
|
+
|
|
1100
|
+
return {
|
|
1101
|
+
timestamp: frame.timestamp,
|
|
1102
|
+
absoluteTimestamp: frame.absolute_timestamp,
|
|
1103
|
+
imageData: frame.image_data,
|
|
1104
|
+
logs: logs,
|
|
1105
|
+
interactions: embeddedInteractions, // Keep legacy field for backward compatibility
|
|
1106
|
+
associatedInteractions: associatedInteractionsResult.interactions,
|
|
1107
|
+
interactionMetadata: associatedInteractionsResult.metadata
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// Get entire frame session
|
|
1112
|
+
getFrameSession(sessionId) {
|
|
1113
|
+
this.init();
|
|
1114
|
+
|
|
1115
|
+
const recording = this.getRecording(sessionId);
|
|
1116
|
+
if (!recording) return null;
|
|
1117
|
+
|
|
1118
|
+
// Debug: Check what's actually in the database
|
|
1119
|
+
const allFramesStmt = this.db.prepare(`
|
|
1120
|
+
SELECT COUNT(*) as count, MIN(frame_index) as min_index, MAX(frame_index) as max_index
|
|
1121
|
+
FROM frames
|
|
1122
|
+
WHERE recording_id = ?
|
|
1123
|
+
`);
|
|
1124
|
+
const frameStats = allFramesStmt.get(recording.id);
|
|
1125
|
+
console.log(`[getFrameSession] Database stats for ${recording.id}: ${frameStats.count} frames, indices ${frameStats.min_index} to ${frameStats.max_index}`);
|
|
1126
|
+
|
|
1127
|
+
const framesStmt = this.db.prepare(`
|
|
1128
|
+
SELECT frame_index, timestamp, absolute_timestamp, image_data, interactions
|
|
1129
|
+
FROM frames
|
|
1130
|
+
WHERE recording_id = ?
|
|
1131
|
+
ORDER BY frame_index ASC
|
|
1132
|
+
`);
|
|
1133
|
+
const frames = framesStmt.all(recording.id);
|
|
1134
|
+
|
|
1135
|
+
console.log(`[getFrameSession] Found ${frames.length} frames in database for session ${sessionId}`);
|
|
1136
|
+
console.log(`[getFrameSession] Frame indices: [${frames.map(f => f.frame_index).join(', ')}]`);
|
|
1137
|
+
|
|
1138
|
+
// Get logs and interactions for all frames
|
|
1139
|
+
for (const frame of frames) {
|
|
1140
|
+
const frameRowStmt = this.db.prepare(`
|
|
1141
|
+
SELECT id FROM frames
|
|
1142
|
+
WHERE recording_id = ? AND frame_index = ?
|
|
1143
|
+
`);
|
|
1144
|
+
const frameRow = frameRowStmt.get(recording.id, frame.frame_index);
|
|
1145
|
+
|
|
1146
|
+
if (frameRow) {
|
|
1147
|
+
const logsStmt = this.db.prepare(`
|
|
1148
|
+
SELECT level, message, relative_time
|
|
1149
|
+
FROM console_logs
|
|
1150
|
+
WHERE frame_id = ?
|
|
1151
|
+
ORDER BY relative_time ASC
|
|
1152
|
+
`);
|
|
1153
|
+
const rawLogs = logsStmt.all(frameRow.id);
|
|
1154
|
+
|
|
1155
|
+
// Convert snake_case field names to camelCase for frontend compatibility
|
|
1156
|
+
frame.logs = rawLogs.map(log => ({
|
|
1157
|
+
level: log.level,
|
|
1158
|
+
message: log.message,
|
|
1159
|
+
relativeTime: log.relative_time
|
|
1160
|
+
}));
|
|
1161
|
+
} else {
|
|
1162
|
+
frame.logs = [];
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Rename fields to match expected format and deserialize embedded interactions
|
|
1166
|
+
frame.index = frame.frame_index;
|
|
1167
|
+
frame.imageData = frame.image_data;
|
|
1168
|
+
frame.absoluteTimestamp = frame.absolute_timestamp;
|
|
1169
|
+
|
|
1170
|
+
// Deserialize embedded interactions from JSON (legacy)
|
|
1171
|
+
let embeddedInteractions = [];
|
|
1172
|
+
try {
|
|
1173
|
+
embeddedInteractions = JSON.parse(frame.interactions || '[]');
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
console.warn(`[getFrameSession] Failed to parse embedded interactions for frame ${frame.frame_index}:`, error);
|
|
1176
|
+
embeddedInteractions = [];
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Get timestamp-associated interactions for this frame
|
|
1180
|
+
const associatedInteractionsResult = this.getInteractionsForFrame(
|
|
1181
|
+
recording.id,
|
|
1182
|
+
frame.timestamp || frame.absolute_timestamp, // Use relative timestamp for proper association
|
|
1183
|
+
500 // ±500ms window
|
|
1184
|
+
);
|
|
1185
|
+
|
|
1186
|
+
// Set interactions fields
|
|
1187
|
+
frame.interactions = embeddedInteractions; // Keep legacy field for backward compatibility
|
|
1188
|
+
frame.associatedInteractions = associatedInteractionsResult.interactions;
|
|
1189
|
+
frame.interactionMetadata = associatedInteractionsResult.metadata;
|
|
1190
|
+
|
|
1191
|
+
delete frame.frame_index;
|
|
1192
|
+
delete frame.image_data;
|
|
1193
|
+
delete frame.absolute_timestamp;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Get all interactions for this recording
|
|
1197
|
+
const interactionsStmt = this.db.prepare(`
|
|
1198
|
+
SELECT * FROM screen_interactions
|
|
1199
|
+
WHERE recording_id = ?
|
|
1200
|
+
ORDER BY timestamp ASC
|
|
1201
|
+
`);
|
|
1202
|
+
const interactions = interactionsStmt.all(recording.id);
|
|
1203
|
+
|
|
1204
|
+
return {
|
|
1205
|
+
type: 'frame_capture',
|
|
1206
|
+
sessionId: recording.session_id,
|
|
1207
|
+
timestamp: recording.timestamp,
|
|
1208
|
+
frames: frames,
|
|
1209
|
+
interactions: interactions
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// List all recordings
|
|
1214
|
+
listRecordings() {
|
|
1215
|
+
this.init();
|
|
1216
|
+
|
|
1217
|
+
const stmt = this.db.prepare(`
|
|
1218
|
+
SELECT session_id, total_frames, timestamp, name
|
|
1219
|
+
FROM recordings
|
|
1220
|
+
WHERE type = 'frame_capture'
|
|
1221
|
+
ORDER BY timestamp DESC
|
|
1222
|
+
`);
|
|
1223
|
+
|
|
1224
|
+
return stmt.all().map(r => ({
|
|
1225
|
+
id: r.session_id,
|
|
1226
|
+
sessionId: r.session_id,
|
|
1227
|
+
totalFrames: r.total_frames,
|
|
1228
|
+
timestamp: r.timestamp,
|
|
1229
|
+
name: r.name
|
|
1230
|
+
}));
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Get frame screenshot data only
|
|
1234
|
+
getFrameScreenshot(sessionId, frameIndex, includeMetadata = false) {
|
|
1235
|
+
this.init();
|
|
1236
|
+
|
|
1237
|
+
const recording = this.getRecording(sessionId);
|
|
1238
|
+
if (!recording) return null;
|
|
1239
|
+
|
|
1240
|
+
const frameStmt = this.db.prepare(`
|
|
1241
|
+
SELECT image_data, timestamp, absolute_timestamp FROM frames
|
|
1242
|
+
WHERE recording_id = ? AND frame_index = ?
|
|
1243
|
+
`);
|
|
1244
|
+
const frame = frameStmt.get(recording.id, frameIndex);
|
|
1245
|
+
|
|
1246
|
+
if (!frame || !frame.image_data) return null;
|
|
1247
|
+
|
|
1248
|
+
const result = {
|
|
1249
|
+
imageData: frame.image_data.toString('base64'),
|
|
1250
|
+
imageFormat: 'jpeg'
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
if (includeMetadata) {
|
|
1254
|
+
result.metadata = {
|
|
1255
|
+
sessionId: sessionId,
|
|
1256
|
+
frameIndex: frameIndex,
|
|
1257
|
+
timestamp: frame.timestamp,
|
|
1258
|
+
absoluteTimestamp: frame.absolute_timestamp,
|
|
1259
|
+
size: frame.image_data.length,
|
|
1260
|
+
encoding: 'base64'
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
return result;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// Delete recording
|
|
1268
|
+
deleteRecording(recordingId) {
|
|
1269
|
+
this.init();
|
|
1270
|
+
|
|
1271
|
+
const recording = this.getRecording(recordingId);
|
|
1272
|
+
if (!recording) return false;
|
|
1273
|
+
|
|
1274
|
+
const stmt = this.db.prepare(`DELETE FROM recordings WHERE id = ?`);
|
|
1275
|
+
const result = stmt.run(recording.id);
|
|
1276
|
+
|
|
1277
|
+
return result.changes > 0;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Delete workflow recording
|
|
1281
|
+
deleteWorkflowRecording(recordingId) {
|
|
1282
|
+
this.init();
|
|
1283
|
+
|
|
1284
|
+
const transaction = this.db.transaction(() => {
|
|
1285
|
+
// Delete associated workflow actions first
|
|
1286
|
+
const deleteActionsStmt = this.db.prepare(`DELETE FROM workflow_actions WHERE workflow_id = ?`);
|
|
1287
|
+
deleteActionsStmt.run(recordingId);
|
|
1288
|
+
|
|
1289
|
+
// Delete workflow recording
|
|
1290
|
+
const deleteRecordingStmt = this.db.prepare(`DELETE FROM workflow_recordings WHERE id = ?`);
|
|
1291
|
+
const result = deleteRecordingStmt.run(recordingId);
|
|
1292
|
+
|
|
1293
|
+
return result.changes > 0;
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
return transaction();
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// Get database stats
|
|
1300
|
+
getStats() {
|
|
1301
|
+
this.init();
|
|
1302
|
+
|
|
1303
|
+
const recordingsCount = this.db.prepare(`SELECT COUNT(*) as count FROM recordings`).get().count;
|
|
1304
|
+
const framesCount = this.db.prepare(`SELECT COUNT(*) as count FROM frames`).get().count;
|
|
1305
|
+
const logsCount = this.db.prepare(`SELECT COUNT(*) as count FROM console_logs`).get().count;
|
|
1306
|
+
|
|
1307
|
+
return {
|
|
1308
|
+
recordings: recordingsCount,
|
|
1309
|
+
frames: framesCount,
|
|
1310
|
+
logs: logsCount,
|
|
1311
|
+
dbPath: DB_PATH
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Store workflow recording
|
|
1316
|
+
storeWorkflowRecording(sessionId, url, title, includeLogs = false, name = null, screenshotSettings = null) {
|
|
1317
|
+
this.init();
|
|
1318
|
+
|
|
1319
|
+
console.log('[Database] Storing workflow recording with name:', name, 'sessionId:', sessionId);
|
|
1320
|
+
|
|
1321
|
+
const stmt = this.db.prepare(`
|
|
1322
|
+
INSERT OR REPLACE INTO workflow_recordings (id, session_id, name, url, title, timestamp, include_logs, screenshot_settings, updated_at)
|
|
1323
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
1324
|
+
`);
|
|
1325
|
+
|
|
1326
|
+
const recordingId = `workflow_${sessionId}`;
|
|
1327
|
+
const timestamp = Date.now();
|
|
1328
|
+
|
|
1329
|
+
stmt.run(
|
|
1330
|
+
recordingId,
|
|
1331
|
+
sessionId,
|
|
1332
|
+
name,
|
|
1333
|
+
url,
|
|
1334
|
+
title,
|
|
1335
|
+
timestamp,
|
|
1336
|
+
includeLogs ? 1 : 0,
|
|
1337
|
+
screenshotSettings ? JSON.stringify(screenshotSettings) : null
|
|
1338
|
+
);
|
|
1339
|
+
return recordingId;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Store workflow actions
|
|
1343
|
+
storeWorkflowActions(workflowId, actions) {
|
|
1344
|
+
this.init();
|
|
1345
|
+
|
|
1346
|
+
const stmt = this.db.prepare(`
|
|
1347
|
+
INSERT OR REPLACE INTO workflow_actions
|
|
1348
|
+
(workflow_id, action_index, type, selector, x, y, value, text, placeholder, timestamp,
|
|
1349
|
+
screenshot_data, component, args, stack, element_html, component_data,
|
|
1350
|
+
event_handlers, element_state, performance_metrics)
|
|
1351
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1352
|
+
`);
|
|
1353
|
+
|
|
1354
|
+
const transaction = this.db.transaction((actions) => {
|
|
1355
|
+
let storedCount = 0;
|
|
1356
|
+
|
|
1357
|
+
for (const action of actions) {
|
|
1358
|
+
// Helper function to safely stringify and truncate JSON data
|
|
1359
|
+
const safeJsonStringify = (data, maxSize) => {
|
|
1360
|
+
if (!data) return null;
|
|
1361
|
+
let jsonStr = typeof data === 'string' ? data : JSON.stringify(data);
|
|
1362
|
+
if (jsonStr.length > maxSize) {
|
|
1363
|
+
console.warn(`[Database] Truncating data field from ${jsonStr.length} to ${maxSize} bytes`);
|
|
1364
|
+
jsonStr = jsonStr.substring(0, maxSize - 50) + '...[TRUNCATED]';
|
|
1365
|
+
}
|
|
1366
|
+
return jsonStr;
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
// Helper function to safely get enhanced field data with dual naming convention support
|
|
1370
|
+
const getEnhancedFieldData = (action, camelCaseName, snakeCaseName) => {
|
|
1371
|
+
return action[camelCaseName] || action[snakeCaseName] || null;
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
stmt.run(
|
|
1375
|
+
workflowId,
|
|
1376
|
+
action.index,
|
|
1377
|
+
action.type,
|
|
1378
|
+
action.selector || '', // Provide empty string as default for selector
|
|
1379
|
+
action.x || null,
|
|
1380
|
+
action.y || null,
|
|
1381
|
+
action.value || null,
|
|
1382
|
+
action.text || null,
|
|
1383
|
+
action.placeholder || null,
|
|
1384
|
+
action.timestamp,
|
|
1385
|
+
action.screenshot_data || null,
|
|
1386
|
+
action.component || null, // For function traces
|
|
1387
|
+
action.args ? JSON.stringify(action.args) : null, // Store args as JSON string
|
|
1388
|
+
action.stack || null, // For function traces
|
|
1389
|
+
// Enhanced click tracking data with size limits - supports both camelCase and snake_case
|
|
1390
|
+
safeJsonStringify(getEnhancedFieldData(action, 'elementHTML', 'element_html'), 10240), // 10KB limit
|
|
1391
|
+
safeJsonStringify(getEnhancedFieldData(action, 'componentData', 'component_data'), 3072), // 3KB limit
|
|
1392
|
+
safeJsonStringify(getEnhancedFieldData(action, 'eventHandlers', 'event_handlers'), 2048), // 2KB limit
|
|
1393
|
+
safeJsonStringify(getEnhancedFieldData(action, 'elementState', 'element_state'), 2048), // 2KB limit
|
|
1394
|
+
safeJsonStringify(getEnhancedFieldData(action, 'performanceMetrics', 'performance_metrics'), 1024) // 1KB limit
|
|
1395
|
+
);
|
|
1396
|
+
storedCount++;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
return storedCount;
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
const storedCount = transaction(actions);
|
|
1403
|
+
|
|
1404
|
+
// Update total actions count
|
|
1405
|
+
const updateStmt = this.db.prepare(`
|
|
1406
|
+
UPDATE workflow_recordings
|
|
1407
|
+
SET total_actions = ?, updated_at = CURRENT_TIMESTAMP
|
|
1408
|
+
WHERE id = ?
|
|
1409
|
+
`);
|
|
1410
|
+
updateStmt.run(storedCount, workflowId);
|
|
1411
|
+
|
|
1412
|
+
return { success: true, storedCount };
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// Store workflow logs
|
|
1416
|
+
storeWorkflowLogs(workflowId, logs) {
|
|
1417
|
+
this.init();
|
|
1418
|
+
|
|
1419
|
+
const stmt = this.db.prepare(`
|
|
1420
|
+
INSERT INTO workflow_logs (workflow_id, level, message, timestamp, relative_time)
|
|
1421
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1422
|
+
`);
|
|
1423
|
+
|
|
1424
|
+
const transaction = this.db.transaction((logs) => {
|
|
1425
|
+
let storedCount = 0;
|
|
1426
|
+
|
|
1427
|
+
for (const log of logs) {
|
|
1428
|
+
// Truncate large log messages before storing
|
|
1429
|
+
let message = log.message;
|
|
1430
|
+
const maxMessageLength = 10000; // 10KB limit per log entry
|
|
1431
|
+
|
|
1432
|
+
if (message && message.length > maxMessageLength) {
|
|
1433
|
+
// Check if it's a base64 image that was already truncated by the extension
|
|
1434
|
+
if (message.includes('[Base64 Image:') && message.includes('truncated...]')) {
|
|
1435
|
+
// Already truncated by extension, keep as is
|
|
1436
|
+
} else {
|
|
1437
|
+
// Additional truncation for any other large content
|
|
1438
|
+
message = message.substring(0, maxMessageLength) + `... [DB TRUNCATED: ${message.length} total bytes]`;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
stmt.run(
|
|
1443
|
+
workflowId,
|
|
1444
|
+
log.level,
|
|
1445
|
+
message,
|
|
1446
|
+
log.timestamp,
|
|
1447
|
+
log.relativeTime || 0
|
|
1448
|
+
);
|
|
1449
|
+
storedCount++;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
return storedCount;
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
const storedCount = transaction(logs);
|
|
1456
|
+
|
|
1457
|
+
return { success: true, storedCount };
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// Get workflow recording info
|
|
1461
|
+
getWorkflowRecordingInfo(sessionId) {
|
|
1462
|
+
this.init();
|
|
1463
|
+
|
|
1464
|
+
const stmt = this.db.prepare(`
|
|
1465
|
+
SELECT * FROM workflow_recordings
|
|
1466
|
+
WHERE session_id = ?
|
|
1467
|
+
`);
|
|
1468
|
+
|
|
1469
|
+
const recording = stmt.get(sessionId);
|
|
1470
|
+
|
|
1471
|
+
if (!recording) {
|
|
1472
|
+
return { error: 'Workflow recording not found' };
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
return {
|
|
1476
|
+
sessionId: recording.session_id,
|
|
1477
|
+
name: recording.name,
|
|
1478
|
+
url: recording.url,
|
|
1479
|
+
title: recording.title,
|
|
1480
|
+
timestamp: recording.timestamp,
|
|
1481
|
+
totalActions: recording.total_actions,
|
|
1482
|
+
includeLogs: recording.include_logs,
|
|
1483
|
+
screenshotSettings: recording.screenshot_settings ? JSON.parse(recording.screenshot_settings) : null
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Get workflow actions
|
|
1488
|
+
getWorkflowActions(sessionId) {
|
|
1489
|
+
this.init();
|
|
1490
|
+
|
|
1491
|
+
const workflowStmt = this.db.prepare(`
|
|
1492
|
+
SELECT id FROM workflow_recordings
|
|
1493
|
+
WHERE session_id = ?
|
|
1494
|
+
`);
|
|
1495
|
+
|
|
1496
|
+
const recording = workflowStmt.get(sessionId);
|
|
1497
|
+
|
|
1498
|
+
if (!recording) {
|
|
1499
|
+
return { error: 'Workflow recording not found' };
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
const actionsStmt = this.db.prepare(`
|
|
1503
|
+
SELECT * FROM workflow_actions
|
|
1504
|
+
WHERE workflow_id = ?
|
|
1505
|
+
ORDER BY action_index ASC
|
|
1506
|
+
`);
|
|
1507
|
+
|
|
1508
|
+
const actions = actionsStmt.all(recording.id);
|
|
1509
|
+
|
|
1510
|
+
return actions.map(action => {
|
|
1511
|
+
const result = {
|
|
1512
|
+
type: action.type,
|
|
1513
|
+
selector: action.selector,
|
|
1514
|
+
x: action.x,
|
|
1515
|
+
y: action.y,
|
|
1516
|
+
value: action.value,
|
|
1517
|
+
text: action.text,
|
|
1518
|
+
placeholder: action.placeholder,
|
|
1519
|
+
timestamp: action.timestamp,
|
|
1520
|
+
screenshot_data: action.screenshot_data
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
// Add function trace fields if they exist
|
|
1524
|
+
if (action.component) {
|
|
1525
|
+
result.component = action.component;
|
|
1526
|
+
}
|
|
1527
|
+
if (action.args) {
|
|
1528
|
+
try {
|
|
1529
|
+
result.args = JSON.parse(action.args);
|
|
1530
|
+
} catch (e) {
|
|
1531
|
+
result.args = action.args;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (action.stack) {
|
|
1535
|
+
result.stack = action.stack;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// Add enhanced click tracking fields if they exist
|
|
1539
|
+
if (action.element_html) {
|
|
1540
|
+
try {
|
|
1541
|
+
result.elementHTML = JSON.parse(action.element_html);
|
|
1542
|
+
} catch (e) {
|
|
1543
|
+
result.elementHTML = action.element_html;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if (action.component_data) {
|
|
1547
|
+
try {
|
|
1548
|
+
result.componentData = JSON.parse(action.component_data);
|
|
1549
|
+
} catch (e) {
|
|
1550
|
+
result.componentData = action.component_data;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
if (action.event_handlers) {
|
|
1554
|
+
try {
|
|
1555
|
+
result.eventHandlers = JSON.parse(action.event_handlers);
|
|
1556
|
+
} catch (e) {
|
|
1557
|
+
result.eventHandlers = action.event_handlers;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
if (action.element_state) {
|
|
1561
|
+
try {
|
|
1562
|
+
result.elementState = JSON.parse(action.element_state);
|
|
1563
|
+
} catch (e) {
|
|
1564
|
+
result.elementState = action.element_state;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
if (action.performance_metrics) {
|
|
1568
|
+
try {
|
|
1569
|
+
result.performanceMetrics = JSON.parse(action.performance_metrics);
|
|
1570
|
+
} catch (e) {
|
|
1571
|
+
result.performanceMetrics = action.performance_metrics;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
return result;
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Get workflow logs
|
|
1580
|
+
getWorkflowLogs(sessionId) {
|
|
1581
|
+
this.init();
|
|
1582
|
+
|
|
1583
|
+
const workflowStmt = this.db.prepare(`
|
|
1584
|
+
SELECT id FROM workflow_recordings
|
|
1585
|
+
WHERE session_id = ?
|
|
1586
|
+
`);
|
|
1587
|
+
|
|
1588
|
+
const recording = workflowStmt.get(sessionId);
|
|
1589
|
+
|
|
1590
|
+
if (!recording) {
|
|
1591
|
+
return { error: 'Workflow recording not found' };
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
const logsStmt = this.db.prepare(`
|
|
1595
|
+
SELECT * FROM workflow_logs
|
|
1596
|
+
WHERE workflow_id = ?
|
|
1597
|
+
ORDER BY timestamp ASC
|
|
1598
|
+
`);
|
|
1599
|
+
|
|
1600
|
+
const logs = logsStmt.all(recording.id);
|
|
1601
|
+
|
|
1602
|
+
return logs.map(log => ({
|
|
1603
|
+
level: log.level,
|
|
1604
|
+
message: log.message,
|
|
1605
|
+
timestamp: log.timestamp,
|
|
1606
|
+
relativeTime: log.relative_time
|
|
1607
|
+
}));
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// Get complete workflow recording
|
|
1611
|
+
getWorkflowRecording(sessionId) {
|
|
1612
|
+
this.init();
|
|
1613
|
+
|
|
1614
|
+
const info = this.getWorkflowRecordingInfo(sessionId);
|
|
1615
|
+
if (info.error) {
|
|
1616
|
+
return info;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const actions = this.getWorkflowActions(sessionId);
|
|
1620
|
+
const logs = info.includeLogs ? this.getWorkflowLogs(sessionId) : [];
|
|
1621
|
+
|
|
1622
|
+
return {
|
|
1623
|
+
type: 'workflow_recording',
|
|
1624
|
+
sessionId: info.sessionId,
|
|
1625
|
+
name: info.name,
|
|
1626
|
+
url: info.url,
|
|
1627
|
+
title: info.title,
|
|
1628
|
+
timestamp: info.timestamp,
|
|
1629
|
+
totalActions: info.totalActions,
|
|
1630
|
+
screenshotSettings: info.screenshotSettings,
|
|
1631
|
+
actions: actions,
|
|
1632
|
+
logs: logs
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// List all workflow recordings
|
|
1637
|
+
listWorkflowRecordings() {
|
|
1638
|
+
this.init();
|
|
1639
|
+
|
|
1640
|
+
const stmt = this.db.prepare(`
|
|
1641
|
+
SELECT id, session_id, name, url, title, total_actions, timestamp, screenshot_settings
|
|
1642
|
+
FROM workflow_recordings
|
|
1643
|
+
ORDER BY timestamp DESC
|
|
1644
|
+
`);
|
|
1645
|
+
|
|
1646
|
+
return stmt.all().map(r => ({
|
|
1647
|
+
id: r.id,
|
|
1648
|
+
session_id: r.session_id,
|
|
1649
|
+
name: r.name,
|
|
1650
|
+
url: r.url,
|
|
1651
|
+
title: r.title,
|
|
1652
|
+
total_actions: r.total_actions,
|
|
1653
|
+
timestamp: r.timestamp,
|
|
1654
|
+
screenshot_settings: r.screenshot_settings ? JSON.parse(r.screenshot_settings) : null
|
|
1655
|
+
}));
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
// Clear all workflow recordings
|
|
1659
|
+
clearWorkflowRecordings() {
|
|
1660
|
+
this.init();
|
|
1661
|
+
|
|
1662
|
+
const transaction = this.db.transaction(() => {
|
|
1663
|
+
// Delete all workflow logs
|
|
1664
|
+
const deleteLogsStmt = this.db.prepare(`DELETE FROM workflow_logs`);
|
|
1665
|
+
deleteLogsStmt.run();
|
|
1666
|
+
|
|
1667
|
+
// Delete all workflow actions
|
|
1668
|
+
const deleteActionsStmt = this.db.prepare(`DELETE FROM workflow_actions`);
|
|
1669
|
+
deleteActionsStmt.run();
|
|
1670
|
+
|
|
1671
|
+
// Delete all restore points
|
|
1672
|
+
const deleteRestorePointsStmt = this.db.prepare(`DELETE FROM restore_points`);
|
|
1673
|
+
deleteRestorePointsStmt.run();
|
|
1674
|
+
|
|
1675
|
+
// Delete all workflow recordings
|
|
1676
|
+
const deleteRecordingsStmt = this.db.prepare(`DELETE FROM workflow_recordings`);
|
|
1677
|
+
const result = deleteRecordingsStmt.run();
|
|
1678
|
+
|
|
1679
|
+
console.log(`[Database] Cleared workflow recordings: ${result.changes} recordings deleted`);
|
|
1680
|
+
return result.changes;
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
return transaction();
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// Store restore point
|
|
1687
|
+
storeRestorePoint(workflowId, actionIndex, restoreData) {
|
|
1688
|
+
this.init();
|
|
1689
|
+
|
|
1690
|
+
const stmt = this.db.prepare(`
|
|
1691
|
+
INSERT OR REPLACE INTO restore_points
|
|
1692
|
+
(id, workflow_id, action_index, timestamp, url, title, dom_snapshot,
|
|
1693
|
+
scroll_x, scroll_y, local_storage, session_storage, cookies, console_logs)
|
|
1694
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1695
|
+
`);
|
|
1696
|
+
|
|
1697
|
+
const restorePointId = `restore_${workflowId}_${actionIndex}_${Date.now()}`;
|
|
1698
|
+
|
|
1699
|
+
try {
|
|
1700
|
+
stmt.run(
|
|
1701
|
+
restorePointId,
|
|
1702
|
+
workflowId,
|
|
1703
|
+
actionIndex,
|
|
1704
|
+
Date.now(),
|
|
1705
|
+
restoreData.url,
|
|
1706
|
+
restoreData.title,
|
|
1707
|
+
JSON.stringify(restoreData.domSnapshot),
|
|
1708
|
+
restoreData.scrollX || 0,
|
|
1709
|
+
restoreData.scrollY || 0,
|
|
1710
|
+
JSON.stringify(restoreData.localStorage || {}),
|
|
1711
|
+
JSON.stringify(restoreData.sessionStorage || {}),
|
|
1712
|
+
JSON.stringify(restoreData.cookies || []),
|
|
1713
|
+
JSON.stringify(restoreData.consoleLogs || [])
|
|
1714
|
+
);
|
|
1715
|
+
|
|
1716
|
+
return { success: true, restorePointId };
|
|
1717
|
+
} catch (error) {
|
|
1718
|
+
console.error('Error storing restore point:', error);
|
|
1719
|
+
return { success: false, error: error.message };
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// Get restore point
|
|
1724
|
+
getRestorePoint(restorePointId) {
|
|
1725
|
+
this.init();
|
|
1726
|
+
|
|
1727
|
+
const stmt = this.db.prepare(`
|
|
1728
|
+
SELECT * FROM restore_points WHERE id = ?
|
|
1729
|
+
`);
|
|
1730
|
+
|
|
1731
|
+
const restorePoint = stmt.get(restorePointId);
|
|
1732
|
+
|
|
1733
|
+
if (!restorePoint) {
|
|
1734
|
+
return { error: 'Restore point not found' };
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
return {
|
|
1738
|
+
id: restorePoint.id,
|
|
1739
|
+
workflowId: restorePoint.workflow_id,
|
|
1740
|
+
actionIndex: restorePoint.action_index,
|
|
1741
|
+
timestamp: restorePoint.timestamp,
|
|
1742
|
+
url: restorePoint.url,
|
|
1743
|
+
title: restorePoint.title,
|
|
1744
|
+
domSnapshot: JSON.parse(restorePoint.dom_snapshot),
|
|
1745
|
+
scrollX: restorePoint.scroll_x,
|
|
1746
|
+
scrollY: restorePoint.scroll_y,
|
|
1747
|
+
localStorage: JSON.parse(restorePoint.local_storage),
|
|
1748
|
+
sessionStorage: JSON.parse(restorePoint.session_storage),
|
|
1749
|
+
cookies: JSON.parse(restorePoint.cookies),
|
|
1750
|
+
consoleLogs: JSON.parse(restorePoint.console_logs)
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// List restore points for a workflow
|
|
1755
|
+
listRestorePoints(workflowId) {
|
|
1756
|
+
this.init();
|
|
1757
|
+
|
|
1758
|
+
const stmt = this.db.prepare(`
|
|
1759
|
+
SELECT id, action_index, timestamp, url, title
|
|
1760
|
+
FROM restore_points
|
|
1761
|
+
WHERE workflow_id = ?
|
|
1762
|
+
ORDER BY action_index ASC, timestamp DESC
|
|
1763
|
+
`);
|
|
1764
|
+
|
|
1765
|
+
return stmt.all(workflowId).map(rp => ({
|
|
1766
|
+
id: rp.id,
|
|
1767
|
+
actionIndex: rp.action_index,
|
|
1768
|
+
timestamp: rp.timestamp,
|
|
1769
|
+
url: rp.url,
|
|
1770
|
+
title: rp.title
|
|
1771
|
+
}));
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// Close database
|
|
1775
|
+
// Server instance tracking methods
|
|
1776
|
+
async registerServerInstance(pid, mode) {
|
|
1777
|
+
this.init();
|
|
1778
|
+
|
|
1779
|
+
// Clean up old entries first
|
|
1780
|
+
await this.cleanupDeadServerInstances();
|
|
1781
|
+
|
|
1782
|
+
const stmt = this.db.prepare(`
|
|
1783
|
+
INSERT OR REPLACE INTO server_instances (pid, mode)
|
|
1784
|
+
VALUES (?, ?)
|
|
1785
|
+
`);
|
|
1786
|
+
|
|
1787
|
+
stmt.run(pid, mode);
|
|
1788
|
+
console.log(`Registered server instance: PID ${pid}, mode: ${mode}`);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
async getSingleServerInstance() {
|
|
1792
|
+
this.init();
|
|
1793
|
+
|
|
1794
|
+
// Clean up dead processes first
|
|
1795
|
+
await this.cleanupDeadServerInstances();
|
|
1796
|
+
|
|
1797
|
+
const stmt = this.db.prepare(`
|
|
1798
|
+
SELECT pid FROM server_instances
|
|
1799
|
+
WHERE mode = 'single-server'
|
|
1800
|
+
ORDER BY started_at DESC
|
|
1801
|
+
LIMIT 1
|
|
1802
|
+
`);
|
|
1803
|
+
|
|
1804
|
+
const result = stmt.get();
|
|
1805
|
+
return result ? result.pid : null;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
async cleanupDeadServerInstances() {
|
|
1809
|
+
this.init();
|
|
1810
|
+
|
|
1811
|
+
const stmt = this.db.prepare(`SELECT pid FROM server_instances`);
|
|
1812
|
+
const instances = stmt.all();
|
|
1813
|
+
|
|
1814
|
+
const { exec } = await import('child_process');
|
|
1815
|
+
const { promisify } = await import('util');
|
|
1816
|
+
const execAsync = promisify(exec);
|
|
1817
|
+
|
|
1818
|
+
for (const instance of instances) {
|
|
1819
|
+
try {
|
|
1820
|
+
// Check if process is still running
|
|
1821
|
+
await execAsync(`ps -p ${instance.pid}`);
|
|
1822
|
+
// Process is still running, keep it
|
|
1823
|
+
} catch (error) {
|
|
1824
|
+
// Process is dead, remove it
|
|
1825
|
+
const deleteStmt = this.db.prepare(`DELETE FROM server_instances WHERE pid = ?`);
|
|
1826
|
+
deleteStmt.run(instance.pid);
|
|
1827
|
+
console.log(`Cleaned up dead server instance: PID ${instance.pid}`);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
unregisterServerInstance(pid) {
|
|
1833
|
+
this.init();
|
|
1834
|
+
|
|
1835
|
+
const stmt = this.db.prepare(`DELETE FROM server_instances WHERE pid = ?`);
|
|
1836
|
+
const result = stmt.run(pid);
|
|
1837
|
+
|
|
1838
|
+
if (result.changes > 0) {
|
|
1839
|
+
console.log(`Unregistered server instance: PID ${pid}`);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// Store screen interactions for a recording
|
|
1844
|
+
storeScreenInteractions(recordingId, interactions) {
|
|
1845
|
+
this.init();
|
|
1846
|
+
|
|
1847
|
+
// Ensure a recording exists for this ID
|
|
1848
|
+
const checkStmt = this.db.prepare(`SELECT id FROM recordings WHERE id = ?`);
|
|
1849
|
+
const existingRecord = checkStmt.get(recordingId);
|
|
1850
|
+
|
|
1851
|
+
if (!existingRecord) {
|
|
1852
|
+
// Create a recording record if it doesn't exist
|
|
1853
|
+
const insertRecordingStmt = this.db.prepare(`
|
|
1854
|
+
INSERT INTO recordings (id, session_id, type, timestamp, updated_at)
|
|
1855
|
+
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
1856
|
+
`);
|
|
1857
|
+
insertRecordingStmt.run(recordingId, recordingId, 'screen_recording', Date.now());
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
const stmt = this.db.prepare(`
|
|
1861
|
+
INSERT OR REPLACE INTO screen_interactions
|
|
1862
|
+
(recording_id, interaction_index, type, selector, xpath, x, y, value, text, key, timestamp, frame_index,
|
|
1863
|
+
element_html, component_data, event_handlers, element_state, performance_metrics)
|
|
1864
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1865
|
+
`);
|
|
1866
|
+
|
|
1867
|
+
const transaction = this.db.transaction(() => {
|
|
1868
|
+
interactions.forEach((interaction, index) => {
|
|
1869
|
+
stmt.run(
|
|
1870
|
+
recordingId,
|
|
1871
|
+
index,
|
|
1872
|
+
interaction.type,
|
|
1873
|
+
interaction.selector || null,
|
|
1874
|
+
interaction.xpath || null,
|
|
1875
|
+
interaction.x || null,
|
|
1876
|
+
interaction.y || null,
|
|
1877
|
+
interaction.value || null,
|
|
1878
|
+
interaction.text || null,
|
|
1879
|
+
interaction.key || null,
|
|
1880
|
+
interaction.timestamp,
|
|
1881
|
+
interaction.frameIndex || null,
|
|
1882
|
+
// Enhanced fields - handle mixed string/object types correctly
|
|
1883
|
+
interaction.element_html || null, // This is a string, store directly
|
|
1884
|
+
interaction.component_data ? JSON.stringify(interaction.component_data) : null,
|
|
1885
|
+
interaction.event_handlers ? JSON.stringify(interaction.event_handlers) : null,
|
|
1886
|
+
interaction.element_state ? JSON.stringify(interaction.element_state) : null,
|
|
1887
|
+
interaction.performance_metrics ? JSON.stringify(interaction.performance_metrics) : null
|
|
1888
|
+
);
|
|
1889
|
+
});
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1892
|
+
try {
|
|
1893
|
+
transaction();
|
|
1894
|
+
return { success: true, count: interactions.length };
|
|
1895
|
+
} catch (error) {
|
|
1896
|
+
console.error('Error storing screen interactions:', error);
|
|
1897
|
+
return { success: false, error: error.message };
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
// Get screen interactions for a recording
|
|
1902
|
+
getScreenInteractions(recordingId) {
|
|
1903
|
+
this.init();
|
|
1904
|
+
|
|
1905
|
+
// Apply same ID resolution logic as getRecording() method
|
|
1906
|
+
// Handle both old format (frame_<id>) and new format (base id)
|
|
1907
|
+
let baseId = recordingId;
|
|
1908
|
+
if (recordingId.startsWith('frame_')) {
|
|
1909
|
+
baseId = recordingId.substring(6); // Remove 'frame_' prefix
|
|
1910
|
+
console.log(`[getScreenInteractions] Converting old format ID ${recordingId} to base ID: ${baseId}`);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// Priority order: exact recording_id match, then baseId match
|
|
1914
|
+
// Check both the original recordingId and the baseId to handle dual compatibility
|
|
1915
|
+
const stmt = this.db.prepare(`
|
|
1916
|
+
SELECT * FROM screen_interactions
|
|
1917
|
+
WHERE recording_id = ? OR recording_id = ?
|
|
1918
|
+
ORDER BY
|
|
1919
|
+
CASE
|
|
1920
|
+
WHEN recording_id = ? THEN 1
|
|
1921
|
+
WHEN recording_id = ? THEN 2
|
|
1922
|
+
ELSE 3
|
|
1923
|
+
END,
|
|
1924
|
+
interaction_index ASC
|
|
1925
|
+
`);
|
|
1926
|
+
|
|
1927
|
+
return stmt.all(recordingId, baseId, recordingId, baseId);
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
// Get interactions for a specific frame
|
|
1931
|
+
getFrameInteractions(recordingId, frameIndex) {
|
|
1932
|
+
this.init();
|
|
1933
|
+
|
|
1934
|
+
const stmt = this.db.prepare(`
|
|
1935
|
+
SELECT * FROM screen_interactions
|
|
1936
|
+
WHERE recording_id = ? AND frame_index = ?
|
|
1937
|
+
ORDER BY timestamp ASC
|
|
1938
|
+
`);
|
|
1939
|
+
|
|
1940
|
+
return stmt.all(recordingId, frameIndex);
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// Get interactions associated with a frame by timestamp window
|
|
1944
|
+
getInteractionsForFrame(recordingId, frameTimestamp, windowMs = 500) {
|
|
1945
|
+
this.init();
|
|
1946
|
+
|
|
1947
|
+
const lowerBound = frameTimestamp - windowMs;
|
|
1948
|
+
const upperBound = frameTimestamp + windowMs;
|
|
1949
|
+
|
|
1950
|
+
const stmt = this.db.prepare(`
|
|
1951
|
+
SELECT
|
|
1952
|
+
id,
|
|
1953
|
+
interaction_index,
|
|
1954
|
+
type,
|
|
1955
|
+
selector,
|
|
1956
|
+
xpath,
|
|
1957
|
+
x,
|
|
1958
|
+
y,
|
|
1959
|
+
value,
|
|
1960
|
+
text,
|
|
1961
|
+
key,
|
|
1962
|
+
timestamp,
|
|
1963
|
+
frame_index,
|
|
1964
|
+
ABS(timestamp - ?) as time_diff
|
|
1965
|
+
FROM screen_interactions
|
|
1966
|
+
WHERE recording_id = ?
|
|
1967
|
+
AND timestamp BETWEEN ? AND ?
|
|
1968
|
+
ORDER BY time_diff ASC, timestamp ASC
|
|
1969
|
+
LIMIT 10
|
|
1970
|
+
`);
|
|
1971
|
+
|
|
1972
|
+
const interactions = stmt.all(frameTimestamp, recordingId, lowerBound, upperBound);
|
|
1973
|
+
|
|
1974
|
+
// Diagnostic logging for interaction association debugging
|
|
1975
|
+
console.log(`[getInteractionsForFrame] DEBUGGING - recordingId: ${recordingId}, frameTimestamp: ${frameTimestamp}, window: ±${windowMs}ms`);
|
|
1976
|
+
console.log(`[getInteractionsForFrame] Search range: ${lowerBound} to ${upperBound}`);
|
|
1977
|
+
console.log(`[getInteractionsForFrame] Found ${interactions.length} interactions`);
|
|
1978
|
+
if (interactions.length > 0) {
|
|
1979
|
+
console.log(`[getInteractionsForFrame] First interaction:`, interactions[0]);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// Convert to InteractionData format with metadata
|
|
1983
|
+
const result = interactions.map(interaction => ({
|
|
1984
|
+
id: interaction.id,
|
|
1985
|
+
index: interaction.interaction_index,
|
|
1986
|
+
type: interaction.type,
|
|
1987
|
+
selector: interaction.selector,
|
|
1988
|
+
xpath: interaction.xpath,
|
|
1989
|
+
x: interaction.x,
|
|
1990
|
+
y: interaction.y,
|
|
1991
|
+
value: interaction.value,
|
|
1992
|
+
text: interaction.text,
|
|
1993
|
+
key: interaction.key,
|
|
1994
|
+
timestamp: interaction.timestamp,
|
|
1995
|
+
frameIndex: interaction.frame_index,
|
|
1996
|
+
timeDifference: interaction.time_diff
|
|
1997
|
+
}));
|
|
1998
|
+
|
|
1999
|
+
// Add metadata
|
|
2000
|
+
const totalFoundStmt = this.db.prepare(`
|
|
2001
|
+
SELECT COUNT(*) as count
|
|
2002
|
+
FROM screen_interactions
|
|
2003
|
+
WHERE recording_id = ?
|
|
2004
|
+
AND timestamp BETWEEN ? AND ?
|
|
2005
|
+
`);
|
|
2006
|
+
const totalFound = totalFoundStmt.get(recordingId, lowerBound, upperBound).count;
|
|
2007
|
+
|
|
2008
|
+
return {
|
|
2009
|
+
interactions: result,
|
|
2010
|
+
metadata: {
|
|
2011
|
+
totalFound: totalFound,
|
|
2012
|
+
displayed: result.length,
|
|
2013
|
+
timeWindow: windowMs,
|
|
2014
|
+
frameTimestamp: frameTimestamp,
|
|
2015
|
+
searchRange: {
|
|
2016
|
+
start: lowerBound,
|
|
2017
|
+
end: upperBound
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// Store execution trace data
|
|
2024
|
+
storeExecutionTrace(recordingId, event) {
|
|
2025
|
+
this.init();
|
|
2026
|
+
|
|
2027
|
+
// Generate a trace_index if not provided (use timestamp + random for uniqueness)
|
|
2028
|
+
const traceIndex = event.trace_index || Date.now() + Math.floor(Math.random() * 1000);
|
|
2029
|
+
|
|
2030
|
+
const stmt = this.db.prepare(`
|
|
2031
|
+
INSERT OR REPLACE INTO execution_traces (
|
|
2032
|
+
recording_id, trace_index, timestamp, function_name, function_source,
|
|
2033
|
+
arguments, return_value, error, call_stack, execution_time_ms
|
|
2034
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2035
|
+
`);
|
|
2036
|
+
|
|
2037
|
+
// Build call stack info from the event data
|
|
2038
|
+
const callStackInfo = {
|
|
2039
|
+
call_id: event.call_id,
|
|
2040
|
+
call_depth: event.call_depth || 0,
|
|
2041
|
+
parent_call_id: event.parent_call_id,
|
|
2042
|
+
category: event.category || 'unknown',
|
|
2043
|
+
metadata: event.metadata || {}
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
return stmt.run(
|
|
2047
|
+
recordingId,
|
|
2048
|
+
traceIndex,
|
|
2049
|
+
event.timestamp || Date.now(),
|
|
2050
|
+
event.function_name,
|
|
2051
|
+
event.function_source || null, // Source location if available
|
|
2052
|
+
JSON.stringify(event.arguments || []),
|
|
2053
|
+
JSON.stringify(event.return_value),
|
|
2054
|
+
event.error,
|
|
2055
|
+
JSON.stringify(callStackInfo),
|
|
2056
|
+
event.execution_time_ms || 0
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// Store variable state data
|
|
2061
|
+
storeVariableState(recordingId, event) {
|
|
2062
|
+
this.init();
|
|
2063
|
+
|
|
2064
|
+
const stmt = this.db.prepare(`
|
|
2065
|
+
INSERT INTO variable_states (
|
|
2066
|
+
recording_id, timestamp, scope_chain, variables, heap_snapshot
|
|
2067
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
2068
|
+
`);
|
|
2069
|
+
|
|
2070
|
+
// Build variables object from event data
|
|
2071
|
+
const variables = {
|
|
2072
|
+
[event.variable_name || 'unknown']: event.variable_value,
|
|
2073
|
+
call_id: event.call_id,
|
|
2074
|
+
capture_point: event.capture_point || 'unknown'
|
|
2075
|
+
};
|
|
2076
|
+
|
|
2077
|
+
const scopeChain = {
|
|
2078
|
+
scope: event.scope || 'unknown',
|
|
2079
|
+
call_id: event.call_id
|
|
2080
|
+
};
|
|
2081
|
+
|
|
2082
|
+
return stmt.run(
|
|
2083
|
+
recordingId,
|
|
2084
|
+
event.timestamp || Date.now(),
|
|
2085
|
+
JSON.stringify(scopeChain),
|
|
2086
|
+
JSON.stringify(variables),
|
|
2087
|
+
null // heap_snapshot - not captured yet
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// Store DOM mutation data
|
|
2092
|
+
storeDomMutation(recordingId, event) {
|
|
2093
|
+
this.init();
|
|
2094
|
+
|
|
2095
|
+
const stmt = this.db.prepare(`
|
|
2096
|
+
INSERT INTO dom_mutations (
|
|
2097
|
+
recording_id, timestamp, mutation_type, target_selector,
|
|
2098
|
+
target_xpath, old_value, new_value, attributes_changed
|
|
2099
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
2100
|
+
`);
|
|
2101
|
+
|
|
2102
|
+
// Build attributes changed object
|
|
2103
|
+
const attributesChanged = {
|
|
2104
|
+
attribute_name: event.attribute_name,
|
|
2105
|
+
added_nodes: event.added_nodes || [],
|
|
2106
|
+
removed_nodes: event.removed_nodes || []
|
|
2107
|
+
};
|
|
2108
|
+
|
|
2109
|
+
return stmt.run(
|
|
2110
|
+
recordingId,
|
|
2111
|
+
event.timestamp || Date.now(),
|
|
2112
|
+
event.mutation_type,
|
|
2113
|
+
event.target_selector,
|
|
2114
|
+
event.target_xpath,
|
|
2115
|
+
event.old_value,
|
|
2116
|
+
event.new_value,
|
|
2117
|
+
JSON.stringify(attributesChanged)
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// Store network request data
|
|
2122
|
+
storeNetworkRequest(recordingId, event) {
|
|
2123
|
+
this.init();
|
|
2124
|
+
|
|
2125
|
+
const stmt = this.db.prepare(`
|
|
2126
|
+
INSERT INTO network_requests (
|
|
2127
|
+
recording_id, timestamp, request_id, method, url,
|
|
2128
|
+
headers, body, response_status, response_headers,
|
|
2129
|
+
response_body, duration_ms
|
|
2130
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2131
|
+
`);
|
|
2132
|
+
|
|
2133
|
+
return stmt.run(
|
|
2134
|
+
recordingId,
|
|
2135
|
+
event.timestamp || Date.now(),
|
|
2136
|
+
event.request_id || 'req_' + Date.now(),
|
|
2137
|
+
event.method,
|
|
2138
|
+
event.url,
|
|
2139
|
+
JSON.stringify(event.request_headers || {}),
|
|
2140
|
+
event.request_body,
|
|
2141
|
+
event.status_code || event.response_status,
|
|
2142
|
+
JSON.stringify(event.response_headers || {}),
|
|
2143
|
+
event.response_body,
|
|
2144
|
+
event.duration_ms || 0
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// Store performance metric data
|
|
2149
|
+
storePerformanceMetric(recordingId, event) {
|
|
2150
|
+
this.init();
|
|
2151
|
+
|
|
2152
|
+
const stmt = this.db.prepare(`
|
|
2153
|
+
INSERT INTO performance_metrics (
|
|
2154
|
+
recording_id, timestamp, metric_type, metric_name,
|
|
2155
|
+
metric_value, metadata
|
|
2156
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
2157
|
+
`);
|
|
2158
|
+
|
|
2159
|
+
return stmt.run(
|
|
2160
|
+
recordingId,
|
|
2161
|
+
event.timestamp || Date.now(),
|
|
2162
|
+
event.metric_type || 'unknown',
|
|
2163
|
+
event.metric_name,
|
|
2164
|
+
event.metric_value,
|
|
2165
|
+
JSON.stringify(event.metadata || {})
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
close() {
|
|
2170
|
+
if (this.db) {
|
|
2171
|
+
this.db.close();
|
|
2172
|
+
this.initialized = false;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// Export singleton instance
|
|
2178
|
+
export const database = new ChromePilotDatabase();
|