dev3000 0.0.38 → 0.0.41

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.
@@ -1,8 +1,9 @@
1
- import { spawn } from 'child_process';
2
- import { WebSocket } from 'ws';
3
- import { writeFileSync, existsSync, mkdirSync } from 'fs';
4
- import { join } from 'path';
5
- import { tmpdir } from 'os';
1
+ import { spawn } from "child_process";
2
+ import { WebSocket } from "ws";
3
+ import { writeFileSync, existsSync, mkdirSync, readFileSync } from "fs";
4
+ import { join, dirname } from "path";
5
+ import { tmpdir } from "os";
6
+ import { fileURLToPath } from "url";
6
7
  export class CDPMonitor {
7
8
  browser = null;
8
9
  connection = null;
@@ -28,132 +29,49 @@ export class CDPMonitor {
28
29
  }
29
30
  async start() {
30
31
  // Launch Chrome with CDP enabled
31
- this.debugLog('Starting Chrome launch process');
32
+ this.debugLog("Starting Chrome launch process");
32
33
  await this.launchChrome();
33
- this.debugLog('Chrome launch completed');
34
+ this.debugLog("Chrome launch completed");
34
35
  // Connect to Chrome DevTools Protocol
35
- this.debugLog('Starting CDP connection');
36
+ this.debugLog("Starting CDP connection");
36
37
  await this.connectToCDP();
37
- this.debugLog('CDP connection completed');
38
+ this.debugLog("CDP connection completed");
38
39
  // Enable all the CDP domains we need for comprehensive monitoring
39
- this.debugLog('Starting CDP domain enablement');
40
+ this.debugLog("Starting CDP domain enablement");
40
41
  await this.enableCDPDomains();
41
- this.debugLog('CDP domain enablement completed');
42
+ this.debugLog("CDP domain enablement completed");
42
43
  // Setup event handlers for comprehensive logging
43
- this.debugLog('Setting up CDP event handlers');
44
+ this.debugLog("Setting up CDP event handlers");
44
45
  this.setupEventHandlers();
45
- this.debugLog('CDP event handlers setup completed');
46
+ this.debugLog("CDP event handlers setup completed");
46
47
  }
47
48
  createLoadingPage() {
48
- const loadingDir = join(tmpdir(), 'dev3000-loading');
49
+ const loadingDir = join(tmpdir(), "dev3000-loading");
49
50
  if (!existsSync(loadingDir)) {
50
51
  mkdirSync(loadingDir, { recursive: true });
51
52
  }
52
- const loadingPath = join(loadingDir, 'loading.html');
53
- const loadingHtml = `<!DOCTYPE html>
53
+ const loadingPath = join(loadingDir, "loading.html");
54
+ // Read the loading HTML from the source file
55
+ const currentFile = fileURLToPath(import.meta.url);
56
+ const currentDir = dirname(currentFile);
57
+ const loadingHtmlPath = join(currentDir, "src/loading.html");
58
+ let loadingHtml;
59
+ try {
60
+ loadingHtml = readFileSync(loadingHtmlPath, 'utf-8');
61
+ }
62
+ catch (error) {
63
+ // Fallback to a simple loading page if file not found
64
+ loadingHtml = `<!DOCTYPE html>
54
65
  <html>
55
- <head>
56
- <title>dev3000 - Starting...</title>
57
- <style>
58
- * {
59
- box-sizing: border-box;
60
- }
61
- body {
62
- margin: 0;
63
- padding: 0;
64
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
65
- background: #000000;
66
- display: flex;
67
- align-items: center;
68
- justify-content: center;
69
- height: 100vh;
70
- color: #fafafa;
71
- }
72
- .container {
73
- text-align: center;
74
- padding: 48px;
75
- max-width: 400px;
76
- }
77
- .logo-container {
78
- margin-bottom: 32px;
79
- position: relative;
80
- }
81
- .triangle {
82
- font-size: 48px;
83
- color: #fafafa;
84
- margin-bottom: 24px;
85
- display: block;
86
- animation: pulse 2s ease-in-out infinite;
87
- line-height: 1;
88
- }
89
- .spinner-ring {
90
- position: absolute;
91
- top: 50%;
92
- left: 50%;
93
- transform: translate(-50%, -50%);
94
- width: 80px;
95
- height: 80px;
96
- border: 2px solid rgba(250, 250, 250, 0.1);
97
- border-top: 2px solid #fafafa;
98
- border-radius: 50%;
99
- animation: spin 1.5s linear infinite;
100
- }
101
- @keyframes spin {
102
- 0% { transform: translate(-50%, -50%) rotate(0deg); }
103
- 100% { transform: translate(-50%, -50%) rotate(360deg); }
104
- }
105
- @keyframes pulse {
106
- 0%, 100% { opacity: 1; transform: scale(1); }
107
- 50% { opacity: 0.8; transform: scale(1.05); }
108
- }
109
- h1 {
110
- margin: 0 0 16px;
111
- font-size: 28px;
112
- font-weight: 500;
113
- letter-spacing: -0.5px;
114
- }
115
- .tagline {
116
- margin: 0 0 8px;
117
- font-size: 16px;
118
- color: #a1a1aa;
119
- font-weight: 400;
120
- }
121
- .subtitle {
122
- margin: 0;
123
- font-size: 14px;
124
- color: #71717a;
125
- font-weight: 400;
126
- }
127
- .footer {
128
- position: absolute;
129
- bottom: 24px;
130
- left: 50%;
131
- transform: translateX(-50%);
132
- font-size: 12px;
133
- color: #52525b;
134
- opacity: 0.8;
135
- }
136
- .triangle-small {
137
- font-size: 10px;
138
- margin-right: 6px;
139
- }
140
- </style>
141
- </head>
142
- <body>
143
- <div class="container">
144
- <div class="logo-container">
145
- <div class="triangle">▲</div>
146
- <div class="spinner-ring"></div>
147
- </div>
66
+ <head><title>dev3000 - Starting...</title></head>
67
+ <body style="font-family: system-ui; background: #1e1e1e; color: #fff; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0;">
68
+ <div style="text-align: center;">
148
69
  <h1>dev3000</h1>
149
- <p class="tagline">Your development environment is starting...</p>
150
- <p class="subtitle">Getting ready to capture everything 📸</p>
151
- </div>
152
- <div class="footer">
153
- <span class="triangle-small">▲</span>Powered by Vercel Labs
70
+ <p>Starting development environment...</p>
154
71
  </div>
155
72
  </body>
156
73
  </html>`;
74
+ }
157
75
  writeFileSync(loadingPath, loadingHtml);
158
76
  return `file://${loadingPath}`;
159
77
  }
@@ -161,10 +79,10 @@ export class CDPMonitor {
161
79
  return new Promise((resolve, reject) => {
162
80
  // Try different Chrome executables based on platform
163
81
  const chromeCommands = [
164
- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
165
- 'google-chrome',
166
- 'chrome',
167
- 'chromium'
82
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
83
+ "google-chrome",
84
+ "chrome",
85
+ "chromium",
168
86
  ];
169
87
  this.debugLog(`Attempting to launch Chrome for CDP monitoring on port ${this.debugPort}`);
170
88
  this.debugLog(`Profile directory: ${this.profileDir}`);
@@ -173,31 +91,31 @@ export class CDPMonitor {
173
91
  this.browser = spawn(chromePath, [
174
92
  `--remote-debugging-port=${this.debugPort}`,
175
93
  `--user-data-dir=${this.profileDir}`,
176
- '--no-first-run',
177
- this.createLoadingPage()
94
+ "--no-first-run",
95
+ this.createLoadingPage(),
178
96
  ], {
179
- stdio: 'pipe',
180
- detached: false
97
+ stdio: "pipe",
98
+ detached: false,
181
99
  });
182
100
  if (!this.browser) {
183
- reject(new Error('Failed to launch Chrome'));
101
+ reject(new Error("Failed to launch Chrome"));
184
102
  return;
185
103
  }
186
- this.browser.on('error', (error) => {
104
+ this.browser.on("error", (error) => {
187
105
  this.debugLog(`Chrome launch error: ${error.message}`);
188
106
  if (!this.isShuttingDown) {
189
107
  reject(error);
190
108
  }
191
109
  });
192
- this.browser.stderr?.on('data', (data) => {
110
+ this.browser.stderr?.on("data", (data) => {
193
111
  this.debugLog(`Chrome stderr: ${data.toString().trim()}`);
194
112
  });
195
- this.browser.stdout?.on('data', (data) => {
113
+ this.browser.stdout?.on("data", (data) => {
196
114
  this.debugLog(`Chrome stdout: ${data.toString().trim()}`);
197
115
  });
198
116
  // Give Chrome time to start up
199
117
  setTimeout(() => {
200
- this.debugLog('Chrome startup timeout reached, assuming success');
118
+ this.debugLog("Chrome startup timeout reached, assuming success");
201
119
  resolve();
202
120
  }, 3000);
203
121
  });
@@ -213,53 +131,53 @@ export class CDPMonitor {
213
131
  const targetsResponse = await fetch(`http://localhost:${this.debugPort}/json`);
214
132
  const targets = await targetsResponse.json();
215
133
  // Find the first page target (tab)
216
- const pageTarget = targets.find((target) => target.type === 'page');
134
+ const pageTarget = targets.find((target) => target.type === "page");
217
135
  if (!pageTarget) {
218
- throw new Error('No page target found in Chrome');
136
+ throw new Error("No page target found in Chrome");
219
137
  }
220
138
  const wsUrl = pageTarget.webSocketDebuggerUrl;
221
- this.debugLog(`Found page target: ${pageTarget.title || 'Unknown'} - ${pageTarget.url}`);
139
+ this.debugLog(`Found page target: ${pageTarget.title || "Unknown"} - ${pageTarget.url}`);
222
140
  this.debugLog(`Got CDP WebSocket URL: ${wsUrl}`);
223
141
  return new Promise((resolve, reject) => {
224
142
  this.debugLog(`Creating WebSocket connection to: ${wsUrl}`);
225
143
  const ws = new WebSocket(wsUrl);
226
144
  // Increase max listeners to prevent warnings
227
145
  ws.setMaxListeners(20);
228
- ws.on('open', () => {
229
- this.debugLog('WebSocket connection opened successfully');
146
+ ws.on("open", () => {
147
+ this.debugLog("WebSocket connection opened successfully");
230
148
  this.connection = {
231
149
  ws,
232
150
  sessionId: null,
233
- nextId: 1
151
+ nextId: 1,
234
152
  };
235
153
  resolve();
236
154
  });
237
- ws.on('error', (error) => {
155
+ ws.on("error", (error) => {
238
156
  this.debugLog(`WebSocket connection error: ${error}`);
239
157
  reject(error);
240
158
  });
241
- ws.on('message', (data) => {
159
+ ws.on("message", (data) => {
242
160
  try {
243
161
  const message = JSON.parse(data.toString());
244
162
  this.handleCDPMessage(message);
245
163
  }
246
164
  catch (error) {
247
- this.logger('browser', `[CDP ERROR] Failed to parse message: ${error}`);
165
+ this.logger("browser", `[CDP ERROR] Failed to parse message: ${error}`);
248
166
  }
249
167
  });
250
- ws.on('close', (code, reason) => {
168
+ ws.on("close", (code, reason) => {
251
169
  this.debugLog(`WebSocket closed with code ${code}, reason: ${reason}`);
252
170
  if (!this.isShuttingDown) {
253
- this.logger('browser', `[CDP] Connection closed unexpectedly (code: ${code}, reason: ${reason})`);
171
+ this.logger("browser", `[CDP] Connection closed unexpectedly (code: ${code}, reason: ${reason})`);
254
172
  }
255
173
  });
256
174
  // Connection timeout
257
175
  setTimeout(() => {
258
176
  this.debugLog(`WebSocket readyState: ${ws.readyState} (CONNECTING=0, OPEN=1, CLOSING=2, CLOSED=3)`);
259
177
  if (ws.readyState === WebSocket.CONNECTING) {
260
- this.debugLog('WebSocket connection timed out, closing');
178
+ this.debugLog("WebSocket connection timed out, closing");
261
179
  ws.close();
262
- reject(new Error('CDP connection timeout'));
180
+ reject(new Error("CDP connection timeout"));
263
181
  }
264
182
  }, 5000);
265
183
  });
@@ -273,13 +191,13 @@ export class CDPMonitor {
273
191
  // Exponential backoff
274
192
  const delay = Math.min(1000 * Math.pow(2, retryCount - 1), 5000);
275
193
  this.debugLog(`Retrying CDP connection in ${delay}ms`);
276
- await new Promise(resolve => setTimeout(resolve, delay));
194
+ await new Promise((resolve) => setTimeout(resolve, delay));
277
195
  }
278
196
  }
279
197
  }
280
198
  async sendCDPCommand(method, params = {}) {
281
199
  if (!this.connection) {
282
- throw new Error('No CDP connection available');
200
+ throw new Error("No CDP connection available");
283
201
  }
284
202
  return new Promise((resolve, reject) => {
285
203
  const id = this.connection.nextId++;
@@ -292,7 +210,7 @@ export class CDPMonitor {
292
210
  try {
293
211
  const message = JSON.parse(data.toString());
294
212
  if (message.id === id) {
295
- this.connection.ws.removeListener('message', messageHandler);
213
+ this.connection.ws.removeListener("message", messageHandler);
296
214
  if (message.error) {
297
215
  reject(new Error(message.error.message));
298
216
  }
@@ -302,14 +220,14 @@ export class CDPMonitor {
302
220
  }
303
221
  }
304
222
  catch (error) {
305
- this.connection.ws.removeListener('message', messageHandler);
223
+ this.connection.ws.removeListener("message", messageHandler);
306
224
  reject(error);
307
225
  }
308
226
  };
309
- this.connection.ws.on('message', messageHandler);
227
+ this.connection.ws.on("message", messageHandler);
310
228
  // Command timeout
311
229
  const timeout = setTimeout(() => {
312
- this.connection.ws.removeListener('message', messageHandler);
230
+ this.connection.ws.removeListener("message", messageHandler);
313
231
  reject(new Error(`CDP command timeout: ${method}`));
314
232
  }, 10000);
315
233
  // Clear timeout if command succeeds/fails
@@ -328,76 +246,97 @@ export class CDPMonitor {
328
246
  }
329
247
  async enableCDPDomains() {
330
248
  const domains = [
331
- 'Runtime', // Console logs, exceptions
332
- 'Network', // Network requests/responses
333
- 'Page', // Page events, navigation
334
- 'DOM', // DOM mutations
335
- 'Performance', // Performance metrics
336
- 'Security', // Security events
337
- 'Log' // Browser console logs
249
+ "Runtime", // Console logs, exceptions
250
+ "Network", // Network requests/responses
251
+ "Page", // Page events, navigation
252
+ "DOM", // DOM mutations
253
+ "Performance", // Performance metrics
254
+ "Security", // Security events
255
+ "Log", // Browser console logs
256
+ "Input", // Mouse and keyboard input events
338
257
  ];
339
258
  for (const domain of domains) {
340
259
  try {
341
260
  this.debugLog(`Enabling CDP domain: ${domain}`);
342
261
  await this.sendCDPCommand(`${domain}.enable`);
343
262
  this.debugLog(`Successfully enabled CDP domain: ${domain}`);
344
- this.logger('browser', `[CDP] Enabled ${domain} domain`);
263
+ this.logger("browser", `[CDP] Enabled ${domain} domain`);
345
264
  }
346
265
  catch (error) {
347
266
  this.debugLog(`Failed to enable CDP domain ${domain}: ${error}`);
348
- this.logger('browser', `[CDP ERROR] Failed to enable ${domain}: ${error}`);
267
+ this.logger("browser", `[CDP ERROR] Failed to enable ${domain}: ${error}`);
349
268
  // Continue with other domains instead of throwing
350
269
  }
351
270
  }
352
- this.debugLog('Setting up input event capturing');
353
- await this.sendCDPCommand('Input.setIgnoreInputEvents', { ignore: false });
354
- this.debugLog('Enabling runtime for console and exception capture');
355
- await this.sendCDPCommand('Runtime.enable');
356
- await this.sendCDPCommand('Runtime.setAsyncCallStackDepth', { maxDepth: 32 });
357
- this.debugLog('CDP domains enabled successfully');
271
+ this.debugLog("Setting up input event capturing");
272
+ await this.sendCDPCommand("Input.setIgnoreInputEvents", { ignore: false });
273
+ this.debugLog("Enabling runtime for console and exception capture");
274
+ await this.sendCDPCommand("Runtime.enable");
275
+ await this.sendCDPCommand("Runtime.setAsyncCallStackDepth", {
276
+ maxDepth: 32,
277
+ });
278
+ this.debugLog("CDP domains enabled successfully");
358
279
  }
359
280
  setupEventHandlers() {
360
281
  // Console messages with full context
361
- this.onCDPEvent('Runtime.consoleAPICalled', (event) => {
282
+ this.onCDPEvent("Runtime.consoleAPICalled", (event) => {
362
283
  this.debugLog(`Runtime.consoleAPICalled event received: ${event.params.type}`);
363
284
  const { type, args, stackTrace } = event.params;
285
+ // Debug: Log all console messages to see if tracking script is working
286
+ if (args.length > 0) {
287
+ this.debugLog(`Console message value: ${args[0].value}`);
288
+ this.debugLog(`Console message full arg: ${JSON.stringify(args[0])}`);
289
+ }
364
290
  // Check if this is our interaction tracking
365
- if (args.length > 0 && args[0].value?.includes('[DEV3000_INTERACTION]')) {
366
- const interaction = args[0].value.replace('[DEV3000_INTERACTION] ', '');
367
- this.logger('browser', `[INTERACTION] ${interaction}`);
291
+ if (args.length > 0 && args[0].value?.includes("[DEV3000_INTERACTION]")) {
292
+ const interaction = args[0].value.replace("[DEV3000_INTERACTION] ", "");
293
+ this.logger("browser", `[INTERACTION] ${interaction}`);
368
294
  return;
369
295
  }
296
+ // Debug: Log all console messages to see if tracking script is even running
297
+ if (args.length > 0 &&
298
+ args[0].value?.includes("CDP tracking initialized")) {
299
+ this.logger("browser", `[DEBUG] Interaction tracking script loaded successfully`);
300
+ }
370
301
  // Log regular console messages with enhanced context
371
- const values = args.map((arg) => {
372
- if (arg.type === 'object' && arg.preview) {
302
+ const values = args
303
+ .map((arg) => {
304
+ if (arg.type === "object" && arg.preview) {
373
305
  return JSON.stringify(arg.preview);
374
306
  }
375
- return arg.value || '[object]';
376
- }).join(' ');
307
+ return arg.value || "[object]";
308
+ })
309
+ .join(" ");
377
310
  let logMsg = `[CONSOLE ${type.toUpperCase()}] ${values}`;
378
311
  // Add stack trace for errors
379
- if (stackTrace && (type === 'error' || type === 'assert')) {
380
- logMsg += `\n[STACK] ${stackTrace.callFrames.slice(0, 3).map((frame) => `${frame.functionName || 'anonymous'}@${frame.url}:${frame.lineNumber}`).join(' -> ')}`;
312
+ if (stackTrace && (type === "error" || type === "assert")) {
313
+ logMsg += `\n[STACK] ${stackTrace.callFrames
314
+ .slice(0, 3)
315
+ .map((frame) => `${frame.functionName || "anonymous"}@${frame.url}:${frame.lineNumber}`)
316
+ .join(" -> ")}`;
381
317
  }
382
- this.logger('browser', logMsg);
318
+ this.logger("browser", logMsg);
383
319
  });
384
320
  // Runtime exceptions with full stack traces
385
- this.onCDPEvent('Runtime.exceptionThrown', (event) => {
386
- this.debugLog('Runtime.exceptionThrown event received');
321
+ this.onCDPEvent("Runtime.exceptionThrown", (event) => {
322
+ this.debugLog("Runtime.exceptionThrown event received");
387
323
  const { exceptionDetails } = event.params;
388
324
  const { text, lineNumber, columnNumber, url, stackTrace } = exceptionDetails;
389
325
  let errorMsg = `[RUNTIME ERROR] ${text}`;
390
326
  if (url)
391
327
  errorMsg += ` at ${url}:${lineNumber}:${columnNumber}`;
392
328
  if (stackTrace) {
393
- errorMsg += `\n[STACK] ${stackTrace.callFrames.slice(0, 5).map((frame) => `${frame.functionName || 'anonymous'}@${frame.url}:${frame.lineNumber}`).join(' -> ')}`;
329
+ errorMsg += `\n[STACK] ${stackTrace.callFrames
330
+ .slice(0, 5)
331
+ .map((frame) => `${frame.functionName || "anonymous"}@${frame.url}:${frame.lineNumber}`)
332
+ .join(" -> ")}`;
394
333
  }
395
- this.logger('browser', errorMsg);
334
+ this.logger("browser", errorMsg);
396
335
  // Take screenshot immediately on errors (no delay needed)
397
- this.takeScreenshot('error');
336
+ this.takeScreenshot("error");
398
337
  });
399
338
  // Browser console logs via Log domain (additional capture method)
400
- this.onCDPEvent('Log.entryAdded', (event) => {
339
+ this.onCDPEvent("Log.entryAdded", (event) => {
401
340
  const { entry } = event.params;
402
341
  const { level, text, url, lineNumber } = entry;
403
342
  let logMsg = `[CONSOLE ${level.toUpperCase()}] ${text}`;
@@ -405,12 +344,12 @@ export class CDPMonitor {
405
344
  logMsg += ` at ${url}:${lineNumber}`;
406
345
  }
407
346
  // Only log if it's an error/warning or if we're not already capturing it via Runtime
408
- if (level === 'error' || level === 'warning') {
409
- this.logger('browser', logMsg);
347
+ if (level === "error" || level === "warning") {
348
+ this.logger("browser", logMsg);
410
349
  }
411
350
  });
412
351
  // Network requests with full details
413
- this.onCDPEvent('Network.requestWillBeSent', (event) => {
352
+ this.onCDPEvent("Network.requestWillBeSent", (event) => {
414
353
  const { request, type, initiator } = event.params;
415
354
  const { url, method, headers, postData } = request;
416
355
  let logMsg = `[NETWORK REQUEST] ${method} ${url}`;
@@ -419,19 +358,19 @@ export class CDPMonitor {
419
358
  if (initiator?.type)
420
359
  logMsg += ` initiated by ${initiator.type}`;
421
360
  // Log important headers
422
- const importantHeaders = ['content-type', 'authorization', 'cookie'];
361
+ const importantHeaders = ["content-type", "authorization", "cookie"];
423
362
  const headerInfo = importantHeaders
424
- .filter(h => headers[h])
425
- .map(h => `${h}: ${headers[h].slice(0, 50)}${headers[h].length > 50 ? '...' : ''}`)
426
- .join(', ');
363
+ .filter((h) => headers[h])
364
+ .map((h) => `${h}: ${headers[h].slice(0, 50)}${headers[h].length > 50 ? "..." : ""}`)
365
+ .join(", ");
427
366
  if (headerInfo)
428
367
  logMsg += ` [${headerInfo}]`;
429
368
  if (postData)
430
- logMsg += ` body: ${postData.slice(0, 100)}${postData.length > 100 ? '...' : ''}`;
431
- this.logger('browser', logMsg);
369
+ logMsg += ` body: ${postData.slice(0, 100)}${postData.length > 100 ? "..." : ""}`;
370
+ this.logger("browser", logMsg);
432
371
  });
433
372
  // Network responses with full details
434
- this.onCDPEvent('Network.responseReceived', (event) => {
373
+ this.onCDPEvent("Network.responseReceived", (event) => {
435
374
  const { response, type } = event.params;
436
375
  const { url, status, statusText, mimeType } = response;
437
376
  let logMsg = `[NETWORK RESPONSE] ${status} ${statusText} ${url}`;
@@ -446,52 +385,64 @@ export class CDPMonitor {
446
385
  if (totalTime > 0)
447
386
  logMsg += ` (${totalTime}ms)`;
448
387
  }
449
- this.logger('browser', logMsg);
388
+ this.logger("browser", logMsg);
450
389
  });
451
390
  // Page navigation with full context
452
- this.onCDPEvent('Page.frameNavigated', (event) => {
391
+ this.onCDPEvent("Page.frameNavigated", (event) => {
453
392
  const { frame } = event.params;
454
393
  if (frame.parentId)
455
394
  return; // Only log main frame navigation
456
- this.logger('browser', `[NAVIGATION] ${frame.url}`);
395
+ this.logger("browser", `[NAVIGATION] ${frame.url}`);
396
+ // Take screenshot on navigation to catch initial render
397
+ setTimeout(() => {
398
+ this.takeScreenshot("frame-navigated");
399
+ }, 200);
457
400
  });
458
401
  // Page load events for better screenshot timing
459
- this.onCDPEvent('Page.loadEventFired', (event) => {
460
- this.logger('browser', '[PAGE] Load event fired');
461
- this.takeScreenshot('page-loaded');
402
+ this.onCDPEvent("Page.loadEventFired", async (event) => {
403
+ this.logger("browser", "[PAGE] Load event fired");
404
+ this.takeScreenshot("page-loaded");
405
+ // Reinject interaction tracking on page load
406
+ await this.setupInteractionTracking();
462
407
  });
463
- this.onCDPEvent('Page.domContentEventFired', (event) => {
464
- this.logger('browser', '[PAGE] DOM content loaded');
465
- // Don't take screenshot here since loadEventFired will handle it
408
+ this.onCDPEvent("Page.domContentEventFired", async (event) => {
409
+ this.logger("browser", "[PAGE] DOM content loaded");
410
+ // Take screenshot on DOM content loaded too for earlier capture
411
+ this.takeScreenshot("dom-content-loaded");
412
+ // Reinject interaction tracking on DOM content loaded
413
+ await this.setupInteractionTracking();
466
414
  });
467
415
  // Network activity tracking for better screenshot timing
468
- this.onCDPEvent('Network.requestWillBeSent', (event) => {
416
+ this.onCDPEvent("Network.requestWillBeSent", (event) => {
469
417
  this.pendingRequests++;
470
418
  if (this.networkIdleTimer) {
471
419
  clearTimeout(this.networkIdleTimer);
472
420
  this.networkIdleTimer = null;
473
421
  }
474
422
  });
475
- this.onCDPEvent('Network.loadingFinished', (event) => {
423
+ this.onCDPEvent("Network.loadingFinished", (event) => {
476
424
  this.pendingRequests--;
477
425
  this.scheduleNetworkIdleScreenshot();
478
426
  });
479
- this.onCDPEvent('Network.loadingFailed', (event) => {
427
+ this.onCDPEvent("Network.loadingFailed", (event) => {
480
428
  this.pendingRequests--;
481
429
  this.scheduleNetworkIdleScreenshot();
482
430
  });
483
431
  // DOM mutations for interaction context
484
- this.onCDPEvent('DOM.documentUpdated', () => {
432
+ this.onCDPEvent("DOM.documentUpdated", () => {
485
433
  // Document structure changed - useful for SPA routing
486
- this.logger('browser', '[DOM] Document updated');
434
+ this.logger("browser", "[DOM] Document updated");
487
435
  });
436
+ // Note: Input.dispatchMouseEvent and Input.dispatchKeyEvent are for SENDING events, not capturing them
437
+ // We need to rely on JavaScript injection for user input capture since CDP doesn't have
438
+ // direct "user input monitoring" events - it's designed for automation, not monitoring
488
439
  // Performance metrics - disabled to reduce log noise
489
440
  // this.onCDPEvent('Performance.metrics', (event) => {
490
441
  // const metrics = event.params.metrics;
491
- // const importantMetrics = metrics.filter((m: any) =>
442
+ // const importantMetrics = metrics.filter((m: any) =>
492
443
  // ['JSHeapUsedSize', 'JSHeapTotalSize', 'Nodes', 'Documents'].includes(m.name)
493
444
  // );
494
- //
445
+ //
495
446
  // if (importantMetrics.length > 0) {
496
447
  // const metricsStr = importantMetrics
497
448
  // .map((m: any) => `${m.name}:${Math.round(m.value)}`)
@@ -511,7 +462,7 @@ export class CDPMonitor {
511
462
  method: message.method,
512
463
  params: message.params || {},
513
464
  timestamp: Date.now(),
514
- sessionId: message.sessionId
465
+ sessionId: message.sessionId,
515
466
  };
516
467
  handler(event);
517
468
  }
@@ -519,190 +470,174 @@ export class CDPMonitor {
519
470
  }
520
471
  async navigateToApp(port) {
521
472
  if (!this.connection) {
522
- throw new Error('No CDP connection available');
473
+ throw new Error("No CDP connection available");
523
474
  }
524
475
  this.debugLog(`Navigating to http://localhost:${port}`);
525
476
  // Navigate to the app
526
- await this.sendCDPCommand('Page.navigate', {
527
- url: `http://localhost:${port}`
477
+ await this.sendCDPCommand("Page.navigate", {
478
+ url: `http://localhost:${port}`,
528
479
  });
529
- this.debugLog('Navigation command sent successfully');
530
- this.debugLog('Setting up interaction tracking');
531
- // Enable interaction tracking via Runtime.evaluate
480
+ this.debugLog("Navigation command sent successfully");
481
+ // Take an immediate screenshot after navigation command
482
+ setTimeout(() => {
483
+ this.takeScreenshot("navigation-immediate");
484
+ }, 100);
485
+ // Take backup screenshots with increasing delays to catch different loading states
486
+ setTimeout(() => {
487
+ this.takeScreenshot("navigation-1s");
488
+ }, 1000);
489
+ setTimeout(() => {
490
+ this.takeScreenshot("navigation-3s");
491
+ }, 3000);
492
+ this.debugLog("Setting up interaction tracking");
493
+ // Enable interaction tracking via Runtime.evaluate - inject with delays to ensure it works
532
494
  await this.setupInteractionTracking();
533
- this.debugLog('Interaction tracking setup completed');
534
- // Initial screenshot will be taken by Page.loadEventFired event handler
495
+ // Also inject on DOM content loaded and page loaded events to ensure it gets set up
496
+ setTimeout(async () => {
497
+ await this.setupInteractionTracking();
498
+ }, 1000);
499
+ setTimeout(async () => {
500
+ await this.setupInteractionTracking();
501
+ }, 2000);
502
+ this.debugLog("Interaction tracking setup completed");
503
+ // Multiple screenshot triggers will ensure we catch the initial page load
535
504
  }
536
505
  async setupInteractionTracking() {
537
- // Inject comprehensive interaction tracking
538
- const trackingScript = `
539
- // Only inject once
540
- if (window.__dev3000_cdp_tracking) return;
541
- window.__dev3000_cdp_tracking = true;
542
-
543
- // Track all mouse events
544
- ['click', 'mousedown', 'mouseup', 'mousemove'].forEach(eventType => {
545
- document.addEventListener(eventType, (event) => {
546
- const target = event.target;
547
- const rect = target.getBoundingClientRect();
548
-
549
- const interactionData = {
550
- type: eventType.toUpperCase(),
551
- timestamp: Date.now(),
552
- coordinates: {
553
- x: event.clientX,
554
- y: event.clientY,
555
- elementX: event.clientX - rect.left,
556
- elementY: event.clientY - rect.top
557
- },
558
- target: {
559
- selector: target.tagName.toLowerCase() +
560
- (target.id ? '#' + target.id : '') +
561
- (target.className ? '.' + target.className.split(' ').join('.') : ''),
562
- text: target.textContent?.slice(0, 100) || null,
563
- attributes: {
564
- id: target.id || null,
565
- className: target.className || null,
566
- type: target.type || null,
567
- href: target.href || null
568
- },
569
- bounds: {
570
- x: Math.round(rect.left),
571
- y: Math.round(rect.top),
572
- width: Math.round(rect.width),
573
- height: Math.round(rect.height)
506
+ try {
507
+ // Full interaction tracking script with element details for replay
508
+ const trackingScript = `
509
+ try {
510
+ if (!window.__dev3000_cdp_tracking) {
511
+ window.__dev3000_cdp_tracking = true;
512
+
513
+ // Helper function to generate CSS selector for element
514
+ function getElementSelector(el) {
515
+ if (!el || el === document) return 'document';
516
+
517
+ // Try ID first (most reliable)
518
+ if (el.id) return '#' + el.id;
519
+
520
+ // Build path with tag + classes
521
+ let selector = el.tagName.toLowerCase();
522
+ if (el.className && typeof el.className === 'string') {
523
+ let classes = el.className.trim().split(/\\\\s+/).filter(c => c.length > 0);
524
+ if (classes.length > 0) selector += '.' + classes.join('.');
574
525
  }
575
- },
576
- viewport: {
577
- width: window.innerWidth,
578
- height: window.innerHeight
579
- },
580
- scroll: {
581
- x: window.scrollX,
582
- y: window.scrollY
583
- },
584
- modifiers: {
585
- ctrl: event.ctrlKey,
586
- alt: event.altKey,
587
- shift: event.shiftKey,
588
- meta: event.metaKey
526
+
527
+ // Add nth-child if needed to make unique
528
+ if (el.parentNode) {
529
+ let siblings = Array.from(el.parentNode.children).filter(child =>
530
+ child.tagName === el.tagName &&
531
+ child.className === el.className
532
+ );
533
+ if (siblings.length > 1) {
534
+ let index = siblings.indexOf(el) + 1;
535
+ selector += ':nth-child(' + index + ')';
536
+ }
537
+ }
538
+
539
+ return selector;
589
540
  }
590
- };
591
-
592
- console.log('[DEV3000_INTERACTION] ' + JSON.stringify(interactionData));
593
- }, true);
594
- });
595
-
596
- // Track keyboard events with enhanced context
597
- document.addEventListener('keydown', (event) => {
598
- const target = event.target;
599
-
600
- const interactionData = {
601
- type: 'KEYDOWN',
602
- timestamp: Date.now(),
603
- key: event.key,
604
- code: event.code,
605
- target: {
606
- selector: target.tagName.toLowerCase() +
607
- (target.id ? '#' + target.id : '') +
608
- (target.className ? '.' + target.className.split(' ').join('.') : ''),
609
- value: target.value?.slice(0, 50) || null,
610
- attributes: {
611
- type: target.type || null,
612
- placeholder: target.placeholder || null
541
+
542
+ // Helper to get element details for replay
543
+ function getElementDetails(el) {
544
+ let details = {
545
+ selector: getElementSelector(el),
546
+ tag: el.tagName.toLowerCase(),
547
+ text: el.textContent ? el.textContent.trim().substring(0, 50) : '',
548
+ id: el.id || '',
549
+ className: el.className || '',
550
+ name: el.name || '',
551
+ type: el.type || '',
552
+ value: el.value || ''
553
+ };
554
+ return JSON.stringify(details);
613
555
  }
614
- },
615
- modifiers: {
616
- ctrl: event.ctrlKey,
617
- alt: event.altKey,
618
- shift: event.shiftKey,
619
- meta: event.metaKey
556
+
557
+ // Scroll coalescing variables
558
+ let scrollTimeout = null;
559
+ let lastScrollX = window.scrollX;
560
+ let lastScrollY = window.scrollY;
561
+ let scrollStartX = window.scrollX;
562
+ let scrollStartY = window.scrollY;
563
+ let scrollTarget = 'document';
564
+
565
+ // Add click tracking with element details
566
+ document.addEventListener('click', function(e) {
567
+ let details = getElementDetails(e.target);
568
+ console.log('[DEV3000_INTERACTION] CLICK at ' + e.clientX + ',' + e.clientY + ' on ' + details);
569
+ });
570
+
571
+ // Add key tracking with element details
572
+ document.addEventListener('keydown', function(e) {
573
+ let details = getElementDetails(e.target);
574
+ console.log('[DEV3000_INTERACTION] KEY ' + e.key + ' in ' + details);
575
+ });
576
+
577
+ // Add coalesced scroll tracking
578
+ document.addEventListener('scroll', function(e) {
579
+ let target = e.target === document ? 'document' : getElementSelector(e.target);
580
+
581
+ // If this is the first scroll event or different target, reset
582
+ if (scrollTimeout === null) {
583
+ scrollStartX = lastScrollX;
584
+ scrollStartY = lastScrollY;
585
+ scrollTarget = target;
586
+ } else {
587
+ clearTimeout(scrollTimeout);
588
+ }
589
+
590
+ // Update current position
591
+ lastScrollX = window.scrollX;
592
+ lastScrollY = window.scrollY;
593
+
594
+ // Set timeout to log scroll after 150ms of no scrolling
595
+ scrollTimeout = setTimeout(function() {
596
+ console.log('[DEV3000_INTERACTION] SCROLL from ' + scrollStartX + ',' + scrollStartY + ' to ' + lastScrollX + ',' + lastScrollY + ' in ' + target);
597
+ scrollTimeout = null;
598
+ }, 150);
599
+ });
620
600
  }
621
- };
622
-
623
- // Only log special keys and form interactions
624
- if (event.key.length > 1 || target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
625
- console.log('[DEV3000_INTERACTION] ' + JSON.stringify(interactionData));
601
+ } catch (err) {
602
+ console.log('[DEV3000_INTERACTION] ERROR: ' + err.message);
626
603
  }
627
- }, true);
628
-
629
- // Track scroll events with momentum detection
630
- let scrollTimeout;
631
- let lastScrollTime = 0;
632
- let scrollStartTime = 0;
633
- let lastScrollPos = { x: window.scrollX, y: window.scrollY };
634
-
635
- document.addEventListener('scroll', () => {
636
- const now = Date.now();
637
-
638
- if (now - lastScrollTime > 100) { // New scroll session
639
- scrollStartTime = now;
604
+ `;
605
+ this.debugLog("About to inject tracking script...");
606
+ // Validate JavaScript syntax before injection
607
+ try {
608
+ new Function(trackingScript);
609
+ this.debugLog("JavaScript syntax validation passed");
610
+ }
611
+ catch (syntaxError) {
612
+ const errorMessage = syntaxError instanceof Error ? syntaxError.message : String(syntaxError);
613
+ this.debugLog(`JavaScript syntax error detected: ${errorMessage}`);
614
+ this.logger("browser", `[CDP ERROR] Tracking script syntax error: ${errorMessage}`);
615
+ throw new Error(`Invalid tracking script syntax: ${errorMessage}`);
616
+ }
617
+ // First try a simple test
618
+ const simpleTest = `console.log('DEV3000_TEST: Simple script execution working!');`;
619
+ const testResult = await this.sendCDPCommand("Runtime.evaluate", {
620
+ expression: simpleTest,
621
+ includeCommandLineAPI: false,
622
+ });
623
+ this.debugLog(`Simple test result: ${JSON.stringify(testResult)}`);
624
+ this.logger("browser", `[DEBUG] Simple test result: ${testResult.result?.value || "undefined"}`);
625
+ const result = await this.sendCDPCommand("Runtime.evaluate", {
626
+ expression: trackingScript,
627
+ includeCommandLineAPI: false,
628
+ });
629
+ this.debugLog(`Interaction tracking script injected. Result: ${JSON.stringify(result)}`);
630
+ this.logger("browser", `[DEBUG] Script injection result: ${result.result?.value || "undefined"}`);
631
+ // Log any errors from the script injection
632
+ if (result.exceptionDetails) {
633
+ this.debugLog(`Script injection exception: ${JSON.stringify(result.exceptionDetails)}`);
634
+ this.logger("browser", `[DEBUG] Script injection exception: ${result.exceptionDetails.exception?.description || "Unknown error"}`);
635
+ }
640
636
  }
641
- lastScrollTime = now;
642
-
643
- clearTimeout(scrollTimeout);
644
- scrollTimeout = setTimeout(() => {
645
- const endPos = { x: window.scrollX, y: window.scrollY };
646
- const distance = Math.round(Math.sqrt(
647
- Math.pow(endPos.x - lastScrollPos.x, 2) +
648
- Math.pow(endPos.y - lastScrollPos.y, 2)
649
- ));
650
-
651
- if (distance > 10) {
652
- const direction = endPos.y > lastScrollPos.y ? 'DOWN' :
653
- endPos.y < lastScrollPos.y ? 'UP' :
654
- endPos.x > lastScrollPos.x ? 'RIGHT' : 'LEFT';
655
-
656
- const interactionData = {
657
- type: 'SCROLL',
658
- timestamp: now,
659
- direction,
660
- distance,
661
- duration: now - scrollStartTime,
662
- from: lastScrollPos,
663
- to: endPos,
664
- viewport: {
665
- width: window.innerWidth,
666
- height: window.innerHeight
667
- }
668
- };
669
-
670
- console.log('[DEV3000_INTERACTION] ' + JSON.stringify(interactionData));
671
- lastScrollPos = endPos;
672
- }
673
- }, 150); // Wait for scroll to finish
674
- }, true);
675
-
676
- // Track form submissions
677
- document.addEventListener('submit', (event) => {
678
- const form = event.target;
679
- const formData = new FormData(form);
680
- const fields = {};
681
-
682
- for (const [key, value] of formData.entries()) {
683
- // Don't log actual values, just field names for privacy
684
- fields[key] = typeof value === 'string' ? \`<\${value.length} chars>\` : '<file>';
637
+ catch (error) {
638
+ this.debugLog(`Failed to inject interaction tracking: ${error}`);
639
+ this.logger("browser", `[CDP ERROR] Interaction tracking failed: ${error}`);
685
640
  }
686
-
687
- const interactionData = {
688
- type: 'FORM_SUBMIT',
689
- timestamp: Date.now(),
690
- target: {
691
- action: form.action || window.location.href,
692
- method: form.method || 'GET',
693
- fields: Object.keys(fields)
694
- }
695
- };
696
-
697
- console.log('[DEV3000_INTERACTION] ' + JSON.stringify(interactionData));
698
- }, true);
699
-
700
- console.log('[DEV3000_INTERACTION] CDP tracking initialized');
701
- `;
702
- await this.sendCDPCommand('Runtime.evaluate', {
703
- expression: trackingScript,
704
- includeCommandLineAPI: false
705
- });
706
641
  }
707
642
  scheduleNetworkIdleScreenshot() {
708
643
  // Only schedule if we have 0 pending requests
@@ -712,80 +647,80 @@ export class CDPMonitor {
712
647
  }
713
648
  // Wait 500ms of network idle before taking screenshot
714
649
  this.networkIdleTimer = setTimeout(() => {
715
- this.takeScreenshot('network-idle');
650
+ this.takeScreenshot("network-idle");
716
651
  this.networkIdleTimer = null;
717
652
  }, 500);
718
653
  }
719
654
  }
720
655
  async takeScreenshot(event) {
721
656
  try {
722
- const result = await this.sendCDPCommand('Page.captureScreenshot', {
723
- format: 'png',
657
+ const result = await this.sendCDPCommand("Page.captureScreenshot", {
658
+ format: "png",
724
659
  quality: 80,
725
660
  clip: undefined, // Full viewport
726
- fromSurface: true
661
+ fromSurface: true,
727
662
  });
728
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
663
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
729
664
  const filename = `${timestamp}-${event}.png`;
730
665
  const screenshotPath = join(this.screenshotDir, filename);
731
666
  // Save the base64 image
732
- const buffer = Buffer.from(result.data, 'base64');
667
+ const buffer = Buffer.from(result.data, "base64");
733
668
  writeFileSync(screenshotPath, buffer);
734
669
  // Log screenshot with proper format that dev3000 expects
735
- this.logger('browser', `[SCREENSHOT] ${filename}`);
670
+ this.logger("browser", `[SCREENSHOT] ${filename}`);
736
671
  return filename;
737
672
  }
738
673
  catch (error) {
739
- this.logger('browser', `[CDP ERROR] Screenshot failed: ${error}`);
674
+ this.logger("browser", `[CDP ERROR] Screenshot failed: ${error}`);
740
675
  return null;
741
676
  }
742
677
  }
743
678
  // Enhanced replay functionality using CDP
744
679
  async executeInteraction(interaction) {
745
680
  if (!this.connection) {
746
- throw new Error('No CDP connection available');
681
+ throw new Error("No CDP connection available");
747
682
  }
748
683
  try {
749
684
  switch (interaction.type) {
750
- case 'CLICK':
751
- await this.sendCDPCommand('Input.dispatchMouseEvent', {
752
- type: 'mousePressed',
685
+ case "CLICK":
686
+ await this.sendCDPCommand("Input.dispatchMouseEvent", {
687
+ type: "mousePressed",
753
688
  x: interaction.coordinates.x,
754
689
  y: interaction.coordinates.y,
755
- button: 'left',
756
- clickCount: 1
690
+ button: "left",
691
+ clickCount: 1,
757
692
  });
758
- await this.sendCDPCommand('Input.dispatchMouseEvent', {
759
- type: 'mouseReleased',
693
+ await this.sendCDPCommand("Input.dispatchMouseEvent", {
694
+ type: "mouseReleased",
760
695
  x: interaction.coordinates.x,
761
696
  y: interaction.coordinates.y,
762
- button: 'left',
763
- clickCount: 1
697
+ button: "left",
698
+ clickCount: 1,
764
699
  });
765
700
  break;
766
- case 'KEYDOWN':
767
- await this.sendCDPCommand('Input.dispatchKeyEvent', {
768
- type: 'keyDown',
701
+ case "KEYDOWN":
702
+ await this.sendCDPCommand("Input.dispatchKeyEvent", {
703
+ type: "keyDown",
769
704
  key: interaction.key,
770
705
  code: interaction.code,
771
- ...interaction.modifiers
706
+ ...interaction.modifiers,
772
707
  });
773
708
  break;
774
- case 'SCROLL':
775
- await this.sendCDPCommand('Input.dispatchMouseEvent', {
776
- type: 'mouseWheel',
709
+ case "SCROLL":
710
+ await this.sendCDPCommand("Input.dispatchMouseEvent", {
711
+ type: "mouseWheel",
777
712
  x: interaction.to.x,
778
713
  y: interaction.to.y,
779
714
  deltaX: interaction.to.x - interaction.from.x,
780
- deltaY: interaction.to.y - interaction.from.y
715
+ deltaY: interaction.to.y - interaction.from.y,
781
716
  });
782
717
  break;
783
718
  default:
784
- this.logger('browser', `[REPLAY] Unknown interaction type: ${interaction.type}`);
719
+ this.logger("browser", `[REPLAY] Unknown interaction type: ${interaction.type}`);
785
720
  }
786
721
  }
787
722
  catch (error) {
788
- this.logger('browser', `[REPLAY ERROR] Failed to execute ${interaction.type}: ${error}`);
723
+ this.logger("browser", `[REPLAY ERROR] Failed to execute ${interaction.type}: ${error}`);
789
724
  }
790
725
  }
791
726
  async shutdown() {
@@ -797,11 +732,11 @@ export class CDPMonitor {
797
732
  }
798
733
  // Close browser
799
734
  if (this.browser) {
800
- this.browser.kill('SIGTERM');
735
+ this.browser.kill("SIGTERM");
801
736
  // Force kill after 2 seconds if not closed
802
737
  setTimeout(() => {
803
738
  if (this.browser) {
804
- this.browser.kill('SIGKILL');
739
+ this.browser.kill("SIGKILL");
805
740
  }
806
741
  }, 2000);
807
742
  this.browser = null;