dev3000 0.0.22 → 0.0.26
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/dist/cdp-monitor.d.ts +39 -0
- package/dist/cdp-monitor.d.ts.map +1 -0
- package/dist/cdp-monitor.js +697 -0
- package/dist/cdp-monitor.js.map +1 -0
- package/dist/cli.js +16 -4
- package/dist/cli.js.map +1 -1
- package/dist/dev-environment.d.ts +4 -17
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +74 -571
- package/dist/dev-environment.js.map +1 -1
- package/mcp-server/app/api/replay/route.ts +412 -0
- package/mcp-server/app/logs/LogsClient.tsx +308 -39
- package/mcp-server/app/replay/ReplayClient.tsx +274 -0
- package/mcp-server/app/replay/page.tsx +5 -0
- package/package.json +8 -5
|
@@ -0,0 +1,697 @@
|
|
|
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';
|
|
6
|
+
export class CDPMonitor {
|
|
7
|
+
browser = null;
|
|
8
|
+
connection = null;
|
|
9
|
+
debugPort = 9222;
|
|
10
|
+
eventHandlers = new Map();
|
|
11
|
+
profileDir;
|
|
12
|
+
logger;
|
|
13
|
+
debug = false;
|
|
14
|
+
isShuttingDown = false;
|
|
15
|
+
constructor(profileDir, logger, debug = false) {
|
|
16
|
+
this.profileDir = profileDir;
|
|
17
|
+
this.logger = logger;
|
|
18
|
+
this.debug = debug;
|
|
19
|
+
}
|
|
20
|
+
debugLog(message) {
|
|
21
|
+
if (this.debug) {
|
|
22
|
+
console.log(`[CDP DEBUG] ${message}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async start() {
|
|
26
|
+
// Launch Chrome with CDP enabled
|
|
27
|
+
this.debugLog('Starting Chrome launch process');
|
|
28
|
+
await this.launchChrome();
|
|
29
|
+
this.debugLog('Chrome launch completed');
|
|
30
|
+
// Connect to Chrome DevTools Protocol
|
|
31
|
+
this.debugLog('Starting CDP connection');
|
|
32
|
+
await this.connectToCDP();
|
|
33
|
+
this.debugLog('CDP connection completed');
|
|
34
|
+
// Enable all the CDP domains we need for comprehensive monitoring
|
|
35
|
+
this.debugLog('Starting CDP domain enablement');
|
|
36
|
+
await this.enableCDPDomains();
|
|
37
|
+
this.debugLog('CDP domain enablement completed');
|
|
38
|
+
// Setup event handlers for comprehensive logging
|
|
39
|
+
this.debugLog('Setting up CDP event handlers');
|
|
40
|
+
this.setupEventHandlers();
|
|
41
|
+
this.debugLog('CDP event handlers setup completed');
|
|
42
|
+
}
|
|
43
|
+
createLoadingPage() {
|
|
44
|
+
const loadingDir = join(tmpdir(), 'dev3000-loading');
|
|
45
|
+
if (!existsSync(loadingDir)) {
|
|
46
|
+
mkdirSync(loadingDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
const loadingPath = join(loadingDir, 'loading.html');
|
|
49
|
+
const loadingHtml = `<!DOCTYPE html>
|
|
50
|
+
<html>
|
|
51
|
+
<head>
|
|
52
|
+
<title>dev3000 - Starting...</title>
|
|
53
|
+
<style>
|
|
54
|
+
body {
|
|
55
|
+
margin: 0;
|
|
56
|
+
padding: 0;
|
|
57
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
58
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
justify-content: center;
|
|
62
|
+
height: 100vh;
|
|
63
|
+
color: white;
|
|
64
|
+
}
|
|
65
|
+
.container {
|
|
66
|
+
text-align: center;
|
|
67
|
+
padding: 40px;
|
|
68
|
+
border-radius: 12px;
|
|
69
|
+
background: rgba(255,255,255,0.1);
|
|
70
|
+
backdrop-filter: blur(10px);
|
|
71
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
|
72
|
+
}
|
|
73
|
+
.spinner {
|
|
74
|
+
width: 40px;
|
|
75
|
+
height: 40px;
|
|
76
|
+
border: 3px solid rgba(255,255,255,0.3);
|
|
77
|
+
border-top: 3px solid white;
|
|
78
|
+
border-radius: 50%;
|
|
79
|
+
animation: spin 1s linear infinite;
|
|
80
|
+
margin: 0 auto 20px;
|
|
81
|
+
}
|
|
82
|
+
@keyframes spin {
|
|
83
|
+
0% { transform: rotate(0deg); }
|
|
84
|
+
100% { transform: rotate(360deg); }
|
|
85
|
+
}
|
|
86
|
+
h1 { margin: 0 0 10px; font-size: 24px; font-weight: 600; }
|
|
87
|
+
p { margin: 0; opacity: 0.9; font-size: 16px; }
|
|
88
|
+
</style>
|
|
89
|
+
</head>
|
|
90
|
+
<body>
|
|
91
|
+
<div class="container">
|
|
92
|
+
<div class="spinner"></div>
|
|
93
|
+
<h1>dev3000</h1>
|
|
94
|
+
<p>Starting your development environment...</p>
|
|
95
|
+
</div>
|
|
96
|
+
</body>
|
|
97
|
+
</html>`;
|
|
98
|
+
writeFileSync(loadingPath, loadingHtml);
|
|
99
|
+
return `file://${loadingPath}`;
|
|
100
|
+
}
|
|
101
|
+
async launchChrome() {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
// Try different Chrome executables based on platform
|
|
104
|
+
const chromeCommands = [
|
|
105
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
106
|
+
'google-chrome',
|
|
107
|
+
'chrome',
|
|
108
|
+
'chromium'
|
|
109
|
+
];
|
|
110
|
+
this.debugLog(`Attempting to launch Chrome for CDP monitoring on port ${this.debugPort}`);
|
|
111
|
+
this.debugLog(`Profile directory: ${this.profileDir}`);
|
|
112
|
+
let chromePath = chromeCommands[0]; // Default to macOS path
|
|
113
|
+
this.debugLog(`Using Chrome path: ${chromePath}`);
|
|
114
|
+
this.browser = spawn(chromePath, [
|
|
115
|
+
`--remote-debugging-port=${this.debugPort}`,
|
|
116
|
+
`--user-data-dir=${this.profileDir}`,
|
|
117
|
+
'--no-first-run',
|
|
118
|
+
this.createLoadingPage()
|
|
119
|
+
], {
|
|
120
|
+
stdio: 'pipe',
|
|
121
|
+
detached: false
|
|
122
|
+
});
|
|
123
|
+
if (!this.browser) {
|
|
124
|
+
reject(new Error('Failed to launch Chrome'));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.browser.on('error', (error) => {
|
|
128
|
+
this.debugLog(`Chrome launch error: ${error.message}`);
|
|
129
|
+
if (!this.isShuttingDown) {
|
|
130
|
+
reject(error);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
this.browser.stderr?.on('data', (data) => {
|
|
134
|
+
this.debugLog(`Chrome stderr: ${data.toString().trim()}`);
|
|
135
|
+
});
|
|
136
|
+
this.browser.stdout?.on('data', (data) => {
|
|
137
|
+
this.debugLog(`Chrome stdout: ${data.toString().trim()}`);
|
|
138
|
+
});
|
|
139
|
+
// Give Chrome time to start up
|
|
140
|
+
setTimeout(() => {
|
|
141
|
+
this.debugLog('Chrome startup timeout reached, assuming success');
|
|
142
|
+
resolve();
|
|
143
|
+
}, 3000);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async connectToCDP() {
|
|
147
|
+
this.debugLog(`Attempting to connect to CDP on port ${this.debugPort}`);
|
|
148
|
+
// Retry connection with exponential backoff
|
|
149
|
+
let retryCount = 0;
|
|
150
|
+
const maxRetries = 5;
|
|
151
|
+
while (retryCount < maxRetries) {
|
|
152
|
+
try {
|
|
153
|
+
// Get the WebSocket URL from Chrome's debug endpoint
|
|
154
|
+
const targetsResponse = await fetch(`http://localhost:${this.debugPort}/json`);
|
|
155
|
+
const targets = await targetsResponse.json();
|
|
156
|
+
// Find the first page target (tab)
|
|
157
|
+
const pageTarget = targets.find((target) => target.type === 'page');
|
|
158
|
+
if (!pageTarget) {
|
|
159
|
+
throw new Error('No page target found in Chrome');
|
|
160
|
+
}
|
|
161
|
+
const wsUrl = pageTarget.webSocketDebuggerUrl;
|
|
162
|
+
this.debugLog(`Found page target: ${pageTarget.title || 'Unknown'} - ${pageTarget.url}`);
|
|
163
|
+
this.debugLog(`Got CDP WebSocket URL: ${wsUrl}`);
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
this.debugLog(`Creating WebSocket connection to: ${wsUrl}`);
|
|
166
|
+
const ws = new WebSocket(wsUrl);
|
|
167
|
+
// Increase max listeners to prevent warnings
|
|
168
|
+
ws.setMaxListeners(20);
|
|
169
|
+
ws.on('open', () => {
|
|
170
|
+
this.debugLog('WebSocket connection opened successfully');
|
|
171
|
+
this.connection = {
|
|
172
|
+
ws,
|
|
173
|
+
sessionId: null,
|
|
174
|
+
nextId: 1
|
|
175
|
+
};
|
|
176
|
+
resolve();
|
|
177
|
+
});
|
|
178
|
+
ws.on('error', (error) => {
|
|
179
|
+
this.debugLog(`WebSocket connection error: ${error}`);
|
|
180
|
+
reject(error);
|
|
181
|
+
});
|
|
182
|
+
ws.on('message', (data) => {
|
|
183
|
+
try {
|
|
184
|
+
const message = JSON.parse(data.toString());
|
|
185
|
+
this.handleCDPMessage(message);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
this.logger('browser', `[CDP ERROR] Failed to parse message: ${error}`);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
ws.on('close', (code, reason) => {
|
|
192
|
+
this.debugLog(`WebSocket closed with code ${code}, reason: ${reason}`);
|
|
193
|
+
if (!this.isShuttingDown) {
|
|
194
|
+
this.logger('browser', `[CDP] Connection closed unexpectedly (code: ${code}, reason: ${reason})`);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
// Connection timeout
|
|
198
|
+
setTimeout(() => {
|
|
199
|
+
this.debugLog(`WebSocket readyState: ${ws.readyState} (CONNECTING=0, OPEN=1, CLOSING=2, CLOSED=3)`);
|
|
200
|
+
if (ws.readyState === WebSocket.CONNECTING) {
|
|
201
|
+
this.debugLog('WebSocket connection timed out, closing');
|
|
202
|
+
ws.close();
|
|
203
|
+
reject(new Error('CDP connection timeout'));
|
|
204
|
+
}
|
|
205
|
+
}, 5000);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
retryCount++;
|
|
210
|
+
this.debugLog(`CDP connection attempt ${retryCount} failed: ${error}`);
|
|
211
|
+
if (retryCount >= maxRetries) {
|
|
212
|
+
throw new Error(`Failed to connect to CDP after ${maxRetries} attempts: ${error}`);
|
|
213
|
+
}
|
|
214
|
+
// Exponential backoff
|
|
215
|
+
const delay = Math.min(1000 * Math.pow(2, retryCount - 1), 5000);
|
|
216
|
+
this.debugLog(`Retrying CDP connection in ${delay}ms`);
|
|
217
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async sendCDPCommand(method, params = {}) {
|
|
222
|
+
if (!this.connection) {
|
|
223
|
+
throw new Error('No CDP connection available');
|
|
224
|
+
}
|
|
225
|
+
return new Promise((resolve, reject) => {
|
|
226
|
+
const id = this.connection.nextId++;
|
|
227
|
+
const command = {
|
|
228
|
+
id,
|
|
229
|
+
method,
|
|
230
|
+
params,
|
|
231
|
+
};
|
|
232
|
+
const messageHandler = (data) => {
|
|
233
|
+
try {
|
|
234
|
+
const message = JSON.parse(data.toString());
|
|
235
|
+
if (message.id === id) {
|
|
236
|
+
this.connection.ws.removeListener('message', messageHandler);
|
|
237
|
+
if (message.error) {
|
|
238
|
+
reject(new Error(message.error.message));
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
resolve(message.result);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
this.connection.ws.removeListener('message', messageHandler);
|
|
247
|
+
reject(error);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
this.connection.ws.on('message', messageHandler);
|
|
251
|
+
// Command timeout
|
|
252
|
+
const timeout = setTimeout(() => {
|
|
253
|
+
this.connection.ws.removeListener('message', messageHandler);
|
|
254
|
+
reject(new Error(`CDP command timeout: ${method}`));
|
|
255
|
+
}, 10000);
|
|
256
|
+
// Clear timeout if command succeeds/fails
|
|
257
|
+
const originalResolve = resolve;
|
|
258
|
+
const originalReject = reject;
|
|
259
|
+
resolve = (value) => {
|
|
260
|
+
clearTimeout(timeout);
|
|
261
|
+
originalResolve(value);
|
|
262
|
+
};
|
|
263
|
+
reject = (reason) => {
|
|
264
|
+
clearTimeout(timeout);
|
|
265
|
+
originalReject(reason);
|
|
266
|
+
};
|
|
267
|
+
this.connection.ws.send(JSON.stringify(command));
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
async enableCDPDomains() {
|
|
271
|
+
const domains = [
|
|
272
|
+
'Runtime', // Console logs, exceptions
|
|
273
|
+
'Network', // Network requests/responses
|
|
274
|
+
'Page', // Page events, navigation
|
|
275
|
+
'DOM', // DOM mutations
|
|
276
|
+
'Performance', // Performance metrics
|
|
277
|
+
'Security' // Security events
|
|
278
|
+
];
|
|
279
|
+
for (const domain of domains) {
|
|
280
|
+
try {
|
|
281
|
+
this.debugLog(`Enabling CDP domain: ${domain}`);
|
|
282
|
+
await this.sendCDPCommand(`${domain}.enable`);
|
|
283
|
+
this.debugLog(`Successfully enabled CDP domain: ${domain}`);
|
|
284
|
+
this.logger('browser', `[CDP] Enabled ${domain} domain`);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
this.debugLog(`Failed to enable CDP domain ${domain}: ${error}`);
|
|
288
|
+
this.logger('browser', `[CDP ERROR] Failed to enable ${domain}: ${error}`);
|
|
289
|
+
// Continue with other domains instead of throwing
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
this.debugLog('Setting up input event capturing');
|
|
293
|
+
await this.sendCDPCommand('Input.setIgnoreInputEvents', { ignore: false });
|
|
294
|
+
this.debugLog('Enabling runtime for console and exception capture');
|
|
295
|
+
await this.sendCDPCommand('Runtime.enable');
|
|
296
|
+
await this.sendCDPCommand('Runtime.setAsyncCallStackDepth', { maxDepth: 32 });
|
|
297
|
+
this.debugLog('CDP domains enabled successfully');
|
|
298
|
+
}
|
|
299
|
+
setupEventHandlers() {
|
|
300
|
+
// Console messages with full context
|
|
301
|
+
this.onCDPEvent('Runtime.consoleAPICalled', (event) => {
|
|
302
|
+
const { type, args, stackTrace } = event.params;
|
|
303
|
+
// Check if this is our interaction tracking
|
|
304
|
+
if (args.length > 0 && args[0].value?.includes('[DEV3000_INTERACTION]')) {
|
|
305
|
+
const interaction = args[0].value.replace('[DEV3000_INTERACTION] ', '');
|
|
306
|
+
this.logger('browser', `[INTERACTION] ${interaction}`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
// Log regular console messages with enhanced context
|
|
310
|
+
const values = args.map((arg) => {
|
|
311
|
+
if (arg.type === 'object' && arg.preview) {
|
|
312
|
+
return JSON.stringify(arg.preview);
|
|
313
|
+
}
|
|
314
|
+
return arg.value || '[object]';
|
|
315
|
+
}).join(' ');
|
|
316
|
+
let logMsg = `[CONSOLE ${type.toUpperCase()}] ${values}`;
|
|
317
|
+
// Add stack trace for errors
|
|
318
|
+
if (stackTrace && (type === 'error' || type === 'assert')) {
|
|
319
|
+
logMsg += `\n[STACK] ${stackTrace.callFrames.slice(0, 3).map((frame) => `${frame.functionName || 'anonymous'}@${frame.url}:${frame.lineNumber}`).join(' -> ')}`;
|
|
320
|
+
}
|
|
321
|
+
this.logger('browser', logMsg);
|
|
322
|
+
});
|
|
323
|
+
// Runtime exceptions with full stack traces
|
|
324
|
+
this.onCDPEvent('Runtime.exceptionThrown', (event) => {
|
|
325
|
+
const { exceptionDetails } = event.params;
|
|
326
|
+
const { text, lineNumber, columnNumber, url, stackTrace } = exceptionDetails;
|
|
327
|
+
let errorMsg = `[RUNTIME ERROR] ${text}`;
|
|
328
|
+
if (url)
|
|
329
|
+
errorMsg += ` at ${url}:${lineNumber}:${columnNumber}`;
|
|
330
|
+
if (stackTrace) {
|
|
331
|
+
errorMsg += `\n[STACK] ${stackTrace.callFrames.slice(0, 5).map((frame) => `${frame.functionName || 'anonymous'}@${frame.url}:${frame.lineNumber}`).join(' -> ')}`;
|
|
332
|
+
}
|
|
333
|
+
this.logger('browser', errorMsg);
|
|
334
|
+
});
|
|
335
|
+
// Network requests with full details
|
|
336
|
+
this.onCDPEvent('Network.requestWillBeSent', (event) => {
|
|
337
|
+
const { request, type, initiator } = event.params;
|
|
338
|
+
const { url, method, headers, postData } = request;
|
|
339
|
+
let logMsg = `[NETWORK REQUEST] ${method} ${url}`;
|
|
340
|
+
if (type)
|
|
341
|
+
logMsg += ` (${type})`;
|
|
342
|
+
if (initiator?.type)
|
|
343
|
+
logMsg += ` initiated by ${initiator.type}`;
|
|
344
|
+
// Log important headers
|
|
345
|
+
const importantHeaders = ['content-type', 'authorization', 'cookie'];
|
|
346
|
+
const headerInfo = importantHeaders
|
|
347
|
+
.filter(h => headers[h])
|
|
348
|
+
.map(h => `${h}: ${headers[h].slice(0, 50)}${headers[h].length > 50 ? '...' : ''}`)
|
|
349
|
+
.join(', ');
|
|
350
|
+
if (headerInfo)
|
|
351
|
+
logMsg += ` [${headerInfo}]`;
|
|
352
|
+
if (postData)
|
|
353
|
+
logMsg += ` body: ${postData.slice(0, 100)}${postData.length > 100 ? '...' : ''}`;
|
|
354
|
+
this.logger('browser', logMsg);
|
|
355
|
+
});
|
|
356
|
+
// Network responses with full details
|
|
357
|
+
this.onCDPEvent('Network.responseReceived', (event) => {
|
|
358
|
+
const { response, type } = event.params;
|
|
359
|
+
const { url, status, statusText, mimeType } = response;
|
|
360
|
+
let logMsg = `[NETWORK RESPONSE] ${status} ${statusText} ${url}`;
|
|
361
|
+
if (type)
|
|
362
|
+
logMsg += ` (${type})`;
|
|
363
|
+
if (mimeType)
|
|
364
|
+
logMsg += ` [${mimeType}]`;
|
|
365
|
+
// Add timing info if available
|
|
366
|
+
const timing = response.timing;
|
|
367
|
+
if (timing) {
|
|
368
|
+
const totalTime = Math.round(timing.receiveHeadersEnd - timing.requestTime);
|
|
369
|
+
if (totalTime > 0)
|
|
370
|
+
logMsg += ` (${totalTime}ms)`;
|
|
371
|
+
}
|
|
372
|
+
this.logger('browser', logMsg);
|
|
373
|
+
});
|
|
374
|
+
// Page navigation with full context
|
|
375
|
+
this.onCDPEvent('Page.frameNavigated', (event) => {
|
|
376
|
+
const { frame } = event.params;
|
|
377
|
+
if (frame.parentId)
|
|
378
|
+
return; // Only log main frame navigation
|
|
379
|
+
this.logger('browser', `[NAVIGATION] ${frame.url}`);
|
|
380
|
+
// Take screenshot after navigation
|
|
381
|
+
setTimeout(() => {
|
|
382
|
+
this.takeScreenshot('navigation');
|
|
383
|
+
}, 1000);
|
|
384
|
+
});
|
|
385
|
+
// DOM mutations for interaction context
|
|
386
|
+
this.onCDPEvent('DOM.documentUpdated', () => {
|
|
387
|
+
// Document structure changed - useful for SPA routing
|
|
388
|
+
this.logger('browser', '[DOM] Document updated');
|
|
389
|
+
});
|
|
390
|
+
// Performance metrics - disabled to reduce log noise
|
|
391
|
+
// this.onCDPEvent('Performance.metrics', (event) => {
|
|
392
|
+
// const metrics = event.params.metrics;
|
|
393
|
+
// const importantMetrics = metrics.filter((m: any) =>
|
|
394
|
+
// ['JSHeapUsedSize', 'JSHeapTotalSize', 'Nodes', 'Documents'].includes(m.name)
|
|
395
|
+
// );
|
|
396
|
+
//
|
|
397
|
+
// if (importantMetrics.length > 0) {
|
|
398
|
+
// const metricsStr = importantMetrics
|
|
399
|
+
// .map((m: any) => `${m.name}:${Math.round(m.value)}`)
|
|
400
|
+
// .join(' ');
|
|
401
|
+
// this.logger('browser', `[PERFORMANCE] ${metricsStr}`);
|
|
402
|
+
// }
|
|
403
|
+
// });
|
|
404
|
+
}
|
|
405
|
+
onCDPEvent(method, handler) {
|
|
406
|
+
this.eventHandlers.set(method, handler);
|
|
407
|
+
}
|
|
408
|
+
handleCDPMessage(message) {
|
|
409
|
+
if (message.method) {
|
|
410
|
+
const handler = this.eventHandlers.get(message.method);
|
|
411
|
+
if (handler) {
|
|
412
|
+
const event = {
|
|
413
|
+
method: message.method,
|
|
414
|
+
params: message.params || {},
|
|
415
|
+
timestamp: Date.now(),
|
|
416
|
+
sessionId: message.sessionId
|
|
417
|
+
};
|
|
418
|
+
handler(event);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async navigateToApp(port) {
|
|
423
|
+
if (!this.connection) {
|
|
424
|
+
throw new Error('No CDP connection available');
|
|
425
|
+
}
|
|
426
|
+
this.debugLog(`Navigating to http://localhost:${port}`);
|
|
427
|
+
// Navigate to the app
|
|
428
|
+
await this.sendCDPCommand('Page.navigate', {
|
|
429
|
+
url: `http://localhost:${port}`
|
|
430
|
+
});
|
|
431
|
+
this.debugLog('Navigation command sent successfully');
|
|
432
|
+
this.debugLog('Setting up interaction tracking');
|
|
433
|
+
// Enable interaction tracking via Runtime.evaluate
|
|
434
|
+
await this.setupInteractionTracking();
|
|
435
|
+
this.debugLog('Interaction tracking setup completed');
|
|
436
|
+
}
|
|
437
|
+
async setupInteractionTracking() {
|
|
438
|
+
// Inject comprehensive interaction tracking
|
|
439
|
+
const trackingScript = `
|
|
440
|
+
// Only inject once
|
|
441
|
+
if (window.__dev3000_cdp_tracking) return;
|
|
442
|
+
window.__dev3000_cdp_tracking = true;
|
|
443
|
+
|
|
444
|
+
// Track all mouse events
|
|
445
|
+
['click', 'mousedown', 'mouseup', 'mousemove'].forEach(eventType => {
|
|
446
|
+
document.addEventListener(eventType, (event) => {
|
|
447
|
+
const target = event.target;
|
|
448
|
+
const rect = target.getBoundingClientRect();
|
|
449
|
+
|
|
450
|
+
const interactionData = {
|
|
451
|
+
type: eventType.toUpperCase(),
|
|
452
|
+
timestamp: Date.now(),
|
|
453
|
+
coordinates: {
|
|
454
|
+
x: event.clientX,
|
|
455
|
+
y: event.clientY,
|
|
456
|
+
elementX: event.clientX - rect.left,
|
|
457
|
+
elementY: event.clientY - rect.top
|
|
458
|
+
},
|
|
459
|
+
target: {
|
|
460
|
+
selector: target.tagName.toLowerCase() +
|
|
461
|
+
(target.id ? '#' + target.id : '') +
|
|
462
|
+
(target.className ? '.' + target.className.split(' ').join('.') : ''),
|
|
463
|
+
text: target.textContent?.slice(0, 100) || null,
|
|
464
|
+
attributes: {
|
|
465
|
+
id: target.id || null,
|
|
466
|
+
className: target.className || null,
|
|
467
|
+
type: target.type || null,
|
|
468
|
+
href: target.href || null
|
|
469
|
+
},
|
|
470
|
+
bounds: {
|
|
471
|
+
x: Math.round(rect.left),
|
|
472
|
+
y: Math.round(rect.top),
|
|
473
|
+
width: Math.round(rect.width),
|
|
474
|
+
height: Math.round(rect.height)
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
viewport: {
|
|
478
|
+
width: window.innerWidth,
|
|
479
|
+
height: window.innerHeight
|
|
480
|
+
},
|
|
481
|
+
scroll: {
|
|
482
|
+
x: window.scrollX,
|
|
483
|
+
y: window.scrollY
|
|
484
|
+
},
|
|
485
|
+
modifiers: {
|
|
486
|
+
ctrl: event.ctrlKey,
|
|
487
|
+
alt: event.altKey,
|
|
488
|
+
shift: event.shiftKey,
|
|
489
|
+
meta: event.metaKey
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
console.log('[DEV3000_INTERACTION] ' + JSON.stringify(interactionData));
|
|
494
|
+
}, true);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Track keyboard events with enhanced context
|
|
498
|
+
document.addEventListener('keydown', (event) => {
|
|
499
|
+
const target = event.target;
|
|
500
|
+
|
|
501
|
+
const interactionData = {
|
|
502
|
+
type: 'KEYDOWN',
|
|
503
|
+
timestamp: Date.now(),
|
|
504
|
+
key: event.key,
|
|
505
|
+
code: event.code,
|
|
506
|
+
target: {
|
|
507
|
+
selector: target.tagName.toLowerCase() +
|
|
508
|
+
(target.id ? '#' + target.id : '') +
|
|
509
|
+
(target.className ? '.' + target.className.split(' ').join('.') : ''),
|
|
510
|
+
value: target.value?.slice(0, 50) || null,
|
|
511
|
+
attributes: {
|
|
512
|
+
type: target.type || null,
|
|
513
|
+
placeholder: target.placeholder || null
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
modifiers: {
|
|
517
|
+
ctrl: event.ctrlKey,
|
|
518
|
+
alt: event.altKey,
|
|
519
|
+
shift: event.shiftKey,
|
|
520
|
+
meta: event.metaKey
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// Only log special keys and form interactions
|
|
525
|
+
if (event.key.length > 1 || target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
|
|
526
|
+
console.log('[DEV3000_INTERACTION] ' + JSON.stringify(interactionData));
|
|
527
|
+
}
|
|
528
|
+
}, true);
|
|
529
|
+
|
|
530
|
+
// Track scroll events with momentum detection
|
|
531
|
+
let scrollTimeout;
|
|
532
|
+
let lastScrollTime = 0;
|
|
533
|
+
let scrollStartTime = 0;
|
|
534
|
+
let lastScrollPos = { x: window.scrollX, y: window.scrollY };
|
|
535
|
+
|
|
536
|
+
document.addEventListener('scroll', () => {
|
|
537
|
+
const now = Date.now();
|
|
538
|
+
|
|
539
|
+
if (now - lastScrollTime > 100) { // New scroll session
|
|
540
|
+
scrollStartTime = now;
|
|
541
|
+
}
|
|
542
|
+
lastScrollTime = now;
|
|
543
|
+
|
|
544
|
+
clearTimeout(scrollTimeout);
|
|
545
|
+
scrollTimeout = setTimeout(() => {
|
|
546
|
+
const endPos = { x: window.scrollX, y: window.scrollY };
|
|
547
|
+
const distance = Math.round(Math.sqrt(
|
|
548
|
+
Math.pow(endPos.x - lastScrollPos.x, 2) +
|
|
549
|
+
Math.pow(endPos.y - lastScrollPos.y, 2)
|
|
550
|
+
));
|
|
551
|
+
|
|
552
|
+
if (distance > 10) {
|
|
553
|
+
const direction = endPos.y > lastScrollPos.y ? 'DOWN' :
|
|
554
|
+
endPos.y < lastScrollPos.y ? 'UP' :
|
|
555
|
+
endPos.x > lastScrollPos.x ? 'RIGHT' : 'LEFT';
|
|
556
|
+
|
|
557
|
+
const interactionData = {
|
|
558
|
+
type: 'SCROLL',
|
|
559
|
+
timestamp: now,
|
|
560
|
+
direction,
|
|
561
|
+
distance,
|
|
562
|
+
duration: now - scrollStartTime,
|
|
563
|
+
from: lastScrollPos,
|
|
564
|
+
to: endPos,
|
|
565
|
+
viewport: {
|
|
566
|
+
width: window.innerWidth,
|
|
567
|
+
height: window.innerHeight
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
console.log('[DEV3000_INTERACTION] ' + JSON.stringify(interactionData));
|
|
572
|
+
lastScrollPos = endPos;
|
|
573
|
+
}
|
|
574
|
+
}, 150); // Wait for scroll to finish
|
|
575
|
+
}, true);
|
|
576
|
+
|
|
577
|
+
// Track form submissions
|
|
578
|
+
document.addEventListener('submit', (event) => {
|
|
579
|
+
const form = event.target;
|
|
580
|
+
const formData = new FormData(form);
|
|
581
|
+
const fields = {};
|
|
582
|
+
|
|
583
|
+
for (const [key, value] of formData.entries()) {
|
|
584
|
+
// Don't log actual values, just field names for privacy
|
|
585
|
+
fields[key] = typeof value === 'string' ? \`<\${value.length} chars>\` : '<file>';
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const interactionData = {
|
|
589
|
+
type: 'FORM_SUBMIT',
|
|
590
|
+
timestamp: Date.now(),
|
|
591
|
+
target: {
|
|
592
|
+
action: form.action || window.location.href,
|
|
593
|
+
method: form.method || 'GET',
|
|
594
|
+
fields: Object.keys(fields)
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
console.log('[DEV3000_INTERACTION] ' + JSON.stringify(interactionData));
|
|
599
|
+
}, true);
|
|
600
|
+
|
|
601
|
+
console.log('[DEV3000_INTERACTION] CDP tracking initialized');
|
|
602
|
+
`;
|
|
603
|
+
await this.sendCDPCommand('Runtime.evaluate', {
|
|
604
|
+
expression: trackingScript,
|
|
605
|
+
includeCommandLineAPI: false
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
async takeScreenshot(event) {
|
|
609
|
+
try {
|
|
610
|
+
const result = await this.sendCDPCommand('Page.captureScreenshot', {
|
|
611
|
+
format: 'png',
|
|
612
|
+
quality: 80,
|
|
613
|
+
clip: undefined, // Full viewport
|
|
614
|
+
fromSurface: true
|
|
615
|
+
});
|
|
616
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
617
|
+
const filename = `${timestamp}-${event}.png`;
|
|
618
|
+
const screenshotPath = `/tmp/dev3000-screenshot-${filename}`;
|
|
619
|
+
// Save the base64 image
|
|
620
|
+
const buffer = Buffer.from(result.data, 'base64');
|
|
621
|
+
writeFileSync(screenshotPath, buffer);
|
|
622
|
+
return filename;
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
this.logger('browser', `[CDP ERROR] Screenshot failed: ${error}`);
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
// Enhanced replay functionality using CDP
|
|
630
|
+
async executeInteraction(interaction) {
|
|
631
|
+
if (!this.connection) {
|
|
632
|
+
throw new Error('No CDP connection available');
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
switch (interaction.type) {
|
|
636
|
+
case 'CLICK':
|
|
637
|
+
await this.sendCDPCommand('Input.dispatchMouseEvent', {
|
|
638
|
+
type: 'mousePressed',
|
|
639
|
+
x: interaction.coordinates.x,
|
|
640
|
+
y: interaction.coordinates.y,
|
|
641
|
+
button: 'left',
|
|
642
|
+
clickCount: 1
|
|
643
|
+
});
|
|
644
|
+
await this.sendCDPCommand('Input.dispatchMouseEvent', {
|
|
645
|
+
type: 'mouseReleased',
|
|
646
|
+
x: interaction.coordinates.x,
|
|
647
|
+
y: interaction.coordinates.y,
|
|
648
|
+
button: 'left',
|
|
649
|
+
clickCount: 1
|
|
650
|
+
});
|
|
651
|
+
break;
|
|
652
|
+
case 'KEYDOWN':
|
|
653
|
+
await this.sendCDPCommand('Input.dispatchKeyEvent', {
|
|
654
|
+
type: 'keyDown',
|
|
655
|
+
key: interaction.key,
|
|
656
|
+
code: interaction.code,
|
|
657
|
+
...interaction.modifiers
|
|
658
|
+
});
|
|
659
|
+
break;
|
|
660
|
+
case 'SCROLL':
|
|
661
|
+
await this.sendCDPCommand('Input.dispatchMouseEvent', {
|
|
662
|
+
type: 'mouseWheel',
|
|
663
|
+
x: interaction.to.x,
|
|
664
|
+
y: interaction.to.y,
|
|
665
|
+
deltaX: interaction.to.x - interaction.from.x,
|
|
666
|
+
deltaY: interaction.to.y - interaction.from.y
|
|
667
|
+
});
|
|
668
|
+
break;
|
|
669
|
+
default:
|
|
670
|
+
this.logger('browser', `[REPLAY] Unknown interaction type: ${interaction.type}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
this.logger('browser', `[REPLAY ERROR] Failed to execute ${interaction.type}: ${error}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async shutdown() {
|
|
678
|
+
this.isShuttingDown = true;
|
|
679
|
+
// Close CDP connection
|
|
680
|
+
if (this.connection) {
|
|
681
|
+
this.connection.ws.close();
|
|
682
|
+
this.connection = null;
|
|
683
|
+
}
|
|
684
|
+
// Close browser
|
|
685
|
+
if (this.browser) {
|
|
686
|
+
this.browser.kill('SIGTERM');
|
|
687
|
+
// Force kill after 2 seconds if not closed
|
|
688
|
+
setTimeout(() => {
|
|
689
|
+
if (this.browser) {
|
|
690
|
+
this.browser.kill('SIGKILL');
|
|
691
|
+
}
|
|
692
|
+
}, 2000);
|
|
693
|
+
this.browser = null;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
//# sourceMappingURL=cdp-monitor.js.map
|