cursorflow 1.3.7__py3-none-any.whl → 2.0.0__py3-none-any.whl
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.
- cursorflow/core/browser_controller.py +1316 -63
- cursorflow/core/error_context_collector.py +590 -0
- cursorflow/core/hmr_detector.py +439 -0
- cursorflow/core/trace_manager.py +209 -0
- cursorflow-2.0.0.dist-info/METADATA +293 -0
- {cursorflow-1.3.7.dist-info → cursorflow-2.0.0.dist-info}/RECORD +10 -7
- cursorflow-1.3.7.dist-info/METADATA +0 -249
- {cursorflow-1.3.7.dist-info → cursorflow-2.0.0.dist-info}/WHEEL +0 -0
- {cursorflow-1.3.7.dist-info → cursorflow-2.0.0.dist-info}/entry_points.txt +0 -0
- {cursorflow-1.3.7.dist-info → cursorflow-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {cursorflow-1.3.7.dist-info → cursorflow-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,439 @@
|
|
1
|
+
"""
|
2
|
+
CursorFlow v2.0 Hot Module Replacement (HMR) Detection System
|
3
|
+
|
4
|
+
This module provides intelligent detection and monitoring of Hot Module Replacement
|
5
|
+
events across different development frameworks, enabling precision timing for CSS
|
6
|
+
iteration workflows.
|
7
|
+
|
8
|
+
Core Philosophy: Pure observation of HMR events - we listen but never trigger.
|
9
|
+
"""
|
10
|
+
|
11
|
+
import asyncio
|
12
|
+
import json
|
13
|
+
import logging
|
14
|
+
import time
|
15
|
+
from typing import Dict, List, Optional, Any, Callable
|
16
|
+
from urllib.parse import urlparse
|
17
|
+
import websockets
|
18
|
+
from websockets.exceptions import ConnectionClosed, InvalidURI
|
19
|
+
|
20
|
+
|
21
|
+
class HMRDetector:
|
22
|
+
"""
|
23
|
+
v2.0 Enhancement: Intelligent Hot Module Replacement event detection
|
24
|
+
|
25
|
+
Supports auto-detection and monitoring for:
|
26
|
+
- Vite (port 5173, WebSocket path /__vite_hmr)
|
27
|
+
- Webpack Dev Server (port 3000, WebSocket path /sockjs-node)
|
28
|
+
- Next.js (port 3000, WebSocket path /_next/webpack-hmr)
|
29
|
+
- Parcel (port 1234, WebSocket path /hmr)
|
30
|
+
- Laravel Mix (port 3000, WebSocket path /browser-sync/socket.io)
|
31
|
+
"""
|
32
|
+
|
33
|
+
def __init__(self, base_url: str):
|
34
|
+
self.base_url = base_url
|
35
|
+
self.logger = logging.getLogger(self.__class__.__name__)
|
36
|
+
|
37
|
+
# Parse the base URL to get host and port
|
38
|
+
parsed_url = urlparse(base_url)
|
39
|
+
self.host = parsed_url.hostname or 'localhost'
|
40
|
+
self.port = parsed_url.port or 3000
|
41
|
+
|
42
|
+
# HMR detection state
|
43
|
+
self.detected_framework = None
|
44
|
+
self.hmr_config = None
|
45
|
+
self.websocket = None
|
46
|
+
self.is_monitoring = False
|
47
|
+
self.hmr_events = []
|
48
|
+
self.event_callbacks = []
|
49
|
+
|
50
|
+
# Framework configurations
|
51
|
+
self.framework_configs = {
|
52
|
+
'vite': {
|
53
|
+
'name': 'Vite',
|
54
|
+
'default_port': 5173,
|
55
|
+
'ws_paths': ['/__vite_hmr', '/vite-hmr'],
|
56
|
+
'event_patterns': ['vite:beforeUpdate', 'vite:afterUpdate', 'css-update'],
|
57
|
+
'css_update_indicators': ['css-update', 'style-update']
|
58
|
+
},
|
59
|
+
'webpack': {
|
60
|
+
'name': 'Webpack Dev Server',
|
61
|
+
'default_port': 3000,
|
62
|
+
'ws_paths': ['/sockjs-node', '/webpack-hmr', '/ws'],
|
63
|
+
'event_patterns': ['webpackHotUpdate', 'hot', 'hash'],
|
64
|
+
'css_update_indicators': ['css', 'style', 'hot-update']
|
65
|
+
},
|
66
|
+
'nextjs': {
|
67
|
+
'name': 'Next.js',
|
68
|
+
'default_port': 3000,
|
69
|
+
'ws_paths': ['/_next/webpack-hmr', '/_next/static/hmr'],
|
70
|
+
'event_patterns': ['building', 'built', 'sync'],
|
71
|
+
'css_update_indicators': ['css', 'style']
|
72
|
+
},
|
73
|
+
'parcel': {
|
74
|
+
'name': 'Parcel',
|
75
|
+
'default_port': 1234,
|
76
|
+
'ws_paths': ['/hmr', '/parcel-hmr'],
|
77
|
+
'event_patterns': ['buildSuccess', 'buildError', 'hmr:update'],
|
78
|
+
'css_update_indicators': ['css', 'style']
|
79
|
+
},
|
80
|
+
'laravel_mix': {
|
81
|
+
'name': 'Laravel Mix',
|
82
|
+
'default_port': 3000,
|
83
|
+
'ws_paths': ['/browser-sync/socket.io', '/browsersync'],
|
84
|
+
'event_patterns': ['file:changed', 'browser:reload'],
|
85
|
+
'css_update_indicators': ['css', 'scss', 'sass']
|
86
|
+
}
|
87
|
+
}
|
88
|
+
|
89
|
+
async def auto_detect_framework(self) -> Optional[str]:
|
90
|
+
"""
|
91
|
+
Auto-detect the development framework by probing WebSocket endpoints
|
92
|
+
|
93
|
+
Returns the detected framework key or None if no framework detected
|
94
|
+
"""
|
95
|
+
self.logger.info(f"Auto-detecting HMR framework for {self.base_url}")
|
96
|
+
|
97
|
+
# Try to detect based on port first
|
98
|
+
port_hints = {
|
99
|
+
5173: ['vite'],
|
100
|
+
3000: ['webpack', 'nextjs', 'laravel_mix'],
|
101
|
+
1234: ['parcel']
|
102
|
+
}
|
103
|
+
|
104
|
+
frameworks_to_check = port_hints.get(self.port, list(self.framework_configs.keys()))
|
105
|
+
|
106
|
+
for framework_key in frameworks_to_check:
|
107
|
+
config = self.framework_configs[framework_key]
|
108
|
+
|
109
|
+
# Test WebSocket connections for this framework
|
110
|
+
for ws_path in config['ws_paths']:
|
111
|
+
ws_url = f"ws://{self.host}:{self.port}{ws_path}"
|
112
|
+
|
113
|
+
try:
|
114
|
+
self.logger.debug(f"Testing WebSocket connection: {ws_url}")
|
115
|
+
|
116
|
+
# Quick connection test with timeout
|
117
|
+
websocket = await asyncio.wait_for(
|
118
|
+
websockets.connect(ws_url, ping_interval=None),
|
119
|
+
timeout=2.0
|
120
|
+
)
|
121
|
+
|
122
|
+
# Connection successful - this is likely our framework
|
123
|
+
await websocket.close()
|
124
|
+
|
125
|
+
self.detected_framework = framework_key
|
126
|
+
self.hmr_config = {
|
127
|
+
'framework': framework_key,
|
128
|
+
'name': config['name'],
|
129
|
+
'ws_url': ws_url,
|
130
|
+
'ws_path': ws_path,
|
131
|
+
'event_patterns': config['event_patterns'],
|
132
|
+
'css_indicators': config['css_update_indicators']
|
133
|
+
}
|
134
|
+
|
135
|
+
self.logger.info(f"✅ Detected HMR framework: {config['name']} at {ws_url}")
|
136
|
+
return framework_key
|
137
|
+
|
138
|
+
except (ConnectionClosed, InvalidURI, OSError, asyncio.TimeoutError) as e:
|
139
|
+
self.logger.debug(f"WebSocket test failed for {ws_url}: {e}")
|
140
|
+
continue
|
141
|
+
|
142
|
+
self.logger.warning("❌ No HMR framework detected - CSS iteration will use fallback timing")
|
143
|
+
return None
|
144
|
+
|
145
|
+
async def start_monitoring(self, on_hmr_event: Optional[Callable] = None) -> bool:
|
146
|
+
"""
|
147
|
+
Start monitoring HMR events
|
148
|
+
|
149
|
+
Args:
|
150
|
+
on_hmr_event: Optional callback function for HMR events
|
151
|
+
|
152
|
+
Returns:
|
153
|
+
True if monitoring started successfully, False otherwise
|
154
|
+
"""
|
155
|
+
if not self.hmr_config:
|
156
|
+
await self.auto_detect_framework()
|
157
|
+
|
158
|
+
if not self.hmr_config:
|
159
|
+
self.logger.warning("Cannot start HMR monitoring - no framework detected")
|
160
|
+
return False
|
161
|
+
|
162
|
+
try:
|
163
|
+
ws_url = self.hmr_config['ws_url']
|
164
|
+
self.logger.info(f"Starting HMR monitoring for {self.hmr_config['name']} at {ws_url}")
|
165
|
+
|
166
|
+
self.websocket = await websockets.connect(ws_url, ping_interval=20)
|
167
|
+
self.is_monitoring = True
|
168
|
+
|
169
|
+
if on_hmr_event:
|
170
|
+
self.event_callbacks.append(on_hmr_event)
|
171
|
+
|
172
|
+
# Start the monitoring loop
|
173
|
+
asyncio.create_task(self._monitor_hmr_events())
|
174
|
+
|
175
|
+
self.logger.info("✅ HMR monitoring started successfully")
|
176
|
+
return True
|
177
|
+
|
178
|
+
except Exception as e:
|
179
|
+
self.logger.error(f"Failed to start HMR monitoring: {e}")
|
180
|
+
self.is_monitoring = False
|
181
|
+
return False
|
182
|
+
|
183
|
+
async def _monitor_hmr_events(self):
|
184
|
+
"""Internal method to monitor WebSocket messages for HMR events"""
|
185
|
+
try:
|
186
|
+
async for message in self.websocket:
|
187
|
+
await self._process_hmr_message(message)
|
188
|
+
|
189
|
+
except ConnectionClosed:
|
190
|
+
self.logger.info("HMR WebSocket connection closed")
|
191
|
+
self.is_monitoring = False
|
192
|
+
except Exception as e:
|
193
|
+
self.logger.error(f"HMR monitoring error: {e}")
|
194
|
+
self.is_monitoring = False
|
195
|
+
|
196
|
+
async def _process_hmr_message(self, message: str):
|
197
|
+
"""Process incoming HMR WebSocket messages"""
|
198
|
+
try:
|
199
|
+
# Parse message (could be JSON or plain text depending on framework)
|
200
|
+
try:
|
201
|
+
data = json.loads(message)
|
202
|
+
message_type = 'json'
|
203
|
+
except json.JSONDecodeError:
|
204
|
+
data = message
|
205
|
+
message_type = 'text'
|
206
|
+
|
207
|
+
# Create HMR event record
|
208
|
+
hmr_event = {
|
209
|
+
'timestamp': time.time(),
|
210
|
+
'framework': self.hmr_config['framework'],
|
211
|
+
'message_type': message_type,
|
212
|
+
'raw_message': message,
|
213
|
+
'parsed_data': data if message_type == 'json' else None,
|
214
|
+
'event_type': self._classify_hmr_event(data, message),
|
215
|
+
'is_css_update': self._is_css_update(data, message)
|
216
|
+
}
|
217
|
+
|
218
|
+
# Store the event
|
219
|
+
self.hmr_events.append(hmr_event)
|
220
|
+
|
221
|
+
# Keep only last 100 events to prevent memory issues
|
222
|
+
if len(self.hmr_events) > 100:
|
223
|
+
self.hmr_events = self.hmr_events[-100:]
|
224
|
+
|
225
|
+
# Notify callbacks
|
226
|
+
for callback in self.event_callbacks:
|
227
|
+
try:
|
228
|
+
await callback(hmr_event)
|
229
|
+
except Exception as e:
|
230
|
+
self.logger.error(f"HMR event callback error: {e}")
|
231
|
+
|
232
|
+
# Log important events
|
233
|
+
if hmr_event['is_css_update']:
|
234
|
+
self.logger.info(f"🎨 CSS update detected: {hmr_event['event_type']}")
|
235
|
+
elif hmr_event['event_type'] != 'heartbeat':
|
236
|
+
self.logger.debug(f"HMR event: {hmr_event['event_type']}")
|
237
|
+
|
238
|
+
except Exception as e:
|
239
|
+
self.logger.error(f"Error processing HMR message: {e}")
|
240
|
+
|
241
|
+
def _classify_hmr_event(self, data: Any, raw_message: str) -> str:
|
242
|
+
"""Classify the type of HMR event based on message content"""
|
243
|
+
if not self.hmr_config:
|
244
|
+
return 'unknown'
|
245
|
+
|
246
|
+
framework = self.hmr_config['framework']
|
247
|
+
event_patterns = self.hmr_config['event_patterns']
|
248
|
+
|
249
|
+
# Convert to string for pattern matching
|
250
|
+
message_str = str(data).lower() if data else raw_message.lower()
|
251
|
+
|
252
|
+
# Framework-specific event classification
|
253
|
+
if framework == 'vite':
|
254
|
+
if 'vite:beforeupdate' in message_str:
|
255
|
+
return 'build_start'
|
256
|
+
elif 'vite:afterupdate' in message_str or 'css-update' in message_str:
|
257
|
+
return 'css_update'
|
258
|
+
elif 'connected' in message_str:
|
259
|
+
return 'connection'
|
260
|
+
elif 'ping' in message_str or 'pong' in message_str:
|
261
|
+
return 'heartbeat'
|
262
|
+
|
263
|
+
elif framework == 'webpack':
|
264
|
+
if 'webpackhotupdate' in message_str:
|
265
|
+
return 'hot_update'
|
266
|
+
elif 'building' in message_str:
|
267
|
+
return 'build_start'
|
268
|
+
elif 'built' in message_str:
|
269
|
+
return 'build_complete'
|
270
|
+
elif 'hash' in message_str:
|
271
|
+
return 'hash_update'
|
272
|
+
|
273
|
+
elif framework == 'nextjs':
|
274
|
+
if 'building' in message_str:
|
275
|
+
return 'build_start'
|
276
|
+
elif 'built' in message_str:
|
277
|
+
return 'build_complete'
|
278
|
+
elif 'sync' in message_str:
|
279
|
+
return 'sync'
|
280
|
+
|
281
|
+
elif framework == 'parcel':
|
282
|
+
if 'buildsuccess' in message_str:
|
283
|
+
return 'build_success'
|
284
|
+
elif 'builderror' in message_str:
|
285
|
+
return 'build_error'
|
286
|
+
elif 'hmr:update' in message_str:
|
287
|
+
return 'hmr_update'
|
288
|
+
|
289
|
+
elif framework == 'laravel_mix':
|
290
|
+
if 'file:changed' in message_str:
|
291
|
+
return 'file_change'
|
292
|
+
elif 'browser:reload' in message_str:
|
293
|
+
return 'browser_reload'
|
294
|
+
|
295
|
+
# Check against general patterns
|
296
|
+
for pattern in event_patterns:
|
297
|
+
if pattern.lower() in message_str:
|
298
|
+
return pattern
|
299
|
+
|
300
|
+
return 'unknown'
|
301
|
+
|
302
|
+
def _is_css_update(self, data: Any, raw_message: str) -> bool:
|
303
|
+
"""Determine if this HMR event represents a CSS update"""
|
304
|
+
if not self.hmr_config:
|
305
|
+
return False
|
306
|
+
|
307
|
+
css_indicators = self.hmr_config['css_indicators']
|
308
|
+
message_str = str(data).lower() if data else raw_message.lower()
|
309
|
+
|
310
|
+
# Check for CSS-specific indicators
|
311
|
+
for indicator in css_indicators:
|
312
|
+
if indicator in message_str:
|
313
|
+
return True
|
314
|
+
|
315
|
+
# Additional CSS detection patterns
|
316
|
+
css_patterns = ['.css', '.scss', '.sass', '.less', 'style', 'stylesheet']
|
317
|
+
for pattern in css_patterns:
|
318
|
+
if pattern in message_str:
|
319
|
+
return True
|
320
|
+
|
321
|
+
return False
|
322
|
+
|
323
|
+
async def wait_for_css_update(self, timeout: float = 10.0) -> Optional[Dict]:
|
324
|
+
"""
|
325
|
+
Wait for the next CSS update event with precision timing
|
326
|
+
|
327
|
+
This is the key method that replaces arbitrary waits in CSS iteration:
|
328
|
+
|
329
|
+
OLD WAY:
|
330
|
+
await page.screenshot("before.png")
|
331
|
+
# ... developer makes CSS changes ...
|
332
|
+
await page.wait_for_timeout(2000) # Arbitrary wait
|
333
|
+
await page.screenshot("after.png")
|
334
|
+
|
335
|
+
NEW WAY:
|
336
|
+
await page.screenshot("before.png")
|
337
|
+
# ... developer makes CSS changes ...
|
338
|
+
css_event = await hmr_detector.wait_for_css_update() # Precise timing
|
339
|
+
await page.screenshot("after.png")
|
340
|
+
|
341
|
+
Args:
|
342
|
+
timeout: Maximum time to wait for CSS update (seconds)
|
343
|
+
|
344
|
+
Returns:
|
345
|
+
HMR event dict if CSS update detected, None if timeout
|
346
|
+
"""
|
347
|
+
if not self.is_monitoring:
|
348
|
+
self.logger.warning("HMR monitoring not active - cannot wait for CSS updates")
|
349
|
+
return None
|
350
|
+
|
351
|
+
start_time = time.time()
|
352
|
+
initial_event_count = len(self.hmr_events)
|
353
|
+
|
354
|
+
self.logger.info(f"⏱️ Waiting for CSS update (timeout: {timeout}s)")
|
355
|
+
|
356
|
+
while time.time() - start_time < timeout:
|
357
|
+
# Check for new CSS update events
|
358
|
+
for event in self.hmr_events[initial_event_count:]:
|
359
|
+
if event['is_css_update']:
|
360
|
+
self.logger.info(f"✅ CSS update detected after {time.time() - start_time:.2f}s")
|
361
|
+
return event
|
362
|
+
|
363
|
+
# Short sleep to prevent busy waiting
|
364
|
+
await asyncio.sleep(0.1)
|
365
|
+
|
366
|
+
self.logger.warning(f"⏰ CSS update wait timeout after {timeout}s")
|
367
|
+
return None
|
368
|
+
|
369
|
+
async def wait_for_build_complete(self, timeout: float = 30.0) -> Optional[Dict]:
|
370
|
+
"""
|
371
|
+
Wait for build completion (useful for more complex changes)
|
372
|
+
|
373
|
+
Args:
|
374
|
+
timeout: Maximum time to wait for build completion (seconds)
|
375
|
+
|
376
|
+
Returns:
|
377
|
+
HMR event dict if build completed, None if timeout
|
378
|
+
"""
|
379
|
+
if not self.is_monitoring:
|
380
|
+
self.logger.warning("HMR monitoring not active - cannot wait for build completion")
|
381
|
+
return None
|
382
|
+
|
383
|
+
start_time = time.time()
|
384
|
+
initial_event_count = len(self.hmr_events)
|
385
|
+
|
386
|
+
self.logger.info(f"⏱️ Waiting for build completion (timeout: {timeout}s)")
|
387
|
+
|
388
|
+
build_complete_indicators = ['build_complete', 'build_success', 'css_update', 'hot_update']
|
389
|
+
|
390
|
+
while time.time() - start_time < timeout:
|
391
|
+
# Check for build completion events
|
392
|
+
for event in self.hmr_events[initial_event_count:]:
|
393
|
+
if event['event_type'] in build_complete_indicators:
|
394
|
+
self.logger.info(f"✅ Build completed after {time.time() - start_time:.2f}s")
|
395
|
+
return event
|
396
|
+
|
397
|
+
await asyncio.sleep(0.1)
|
398
|
+
|
399
|
+
self.logger.warning(f"⏰ Build completion wait timeout after {timeout}s")
|
400
|
+
return None
|
401
|
+
|
402
|
+
async def stop_monitoring(self):
|
403
|
+
"""Stop HMR monitoring and close WebSocket connection"""
|
404
|
+
self.is_monitoring = False
|
405
|
+
|
406
|
+
if self.websocket:
|
407
|
+
try:
|
408
|
+
await self.websocket.close()
|
409
|
+
self.logger.info("HMR monitoring stopped")
|
410
|
+
except Exception as e:
|
411
|
+
self.logger.error(f"Error stopping HMR monitoring: {e}")
|
412
|
+
|
413
|
+
self.websocket = None
|
414
|
+
|
415
|
+
def get_hmr_status(self) -> Dict[str, Any]:
|
416
|
+
"""Get current HMR detection and monitoring status"""
|
417
|
+
return {
|
418
|
+
'framework_detected': self.detected_framework,
|
419
|
+
'framework_name': self.hmr_config['name'] if self.hmr_config else None,
|
420
|
+
'is_monitoring': self.is_monitoring,
|
421
|
+
'websocket_url': self.hmr_config['ws_url'] if self.hmr_config else None,
|
422
|
+
'total_events': len(self.hmr_events),
|
423
|
+
'css_events': len([e for e in self.hmr_events if e['is_css_update']]),
|
424
|
+
'recent_events': self.hmr_events[-5:] if self.hmr_events else []
|
425
|
+
}
|
426
|
+
|
427
|
+
def get_framework_info(self) -> Dict[str, Any]:
|
428
|
+
"""Get information about the detected framework"""
|
429
|
+
if not self.hmr_config:
|
430
|
+
return {'detected': False, 'supported_frameworks': list(self.framework_configs.keys())}
|
431
|
+
|
432
|
+
return {
|
433
|
+
'detected': True,
|
434
|
+
'framework': self.hmr_config['framework'],
|
435
|
+
'name': self.hmr_config['name'],
|
436
|
+
'websocket_path': self.hmr_config['ws_path'],
|
437
|
+
'css_indicators': self.hmr_config['css_indicators'],
|
438
|
+
'event_patterns': self.hmr_config['event_patterns']
|
439
|
+
}
|
@@ -0,0 +1,209 @@
|
|
1
|
+
"""
|
2
|
+
Trace Manager for CursorFlow v2.0
|
3
|
+
|
4
|
+
Manages Playwright trace file recording for complete interaction history.
|
5
|
+
Pure observation - records everything without modifying application behavior.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import asyncio
|
9
|
+
import time
|
10
|
+
import logging
|
11
|
+
from typing import Optional, Dict, Any
|
12
|
+
from pathlib import Path
|
13
|
+
from playwright.async_api import BrowserContext
|
14
|
+
|
15
|
+
|
16
|
+
class TraceManager:
|
17
|
+
"""
|
18
|
+
Manages Playwright trace recording for comprehensive debugging data
|
19
|
+
|
20
|
+
Provides complete interaction history, screenshots, and network activity
|
21
|
+
in Playwright's native trace format for maximum debugging capability.
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __init__(self, artifacts_base: Path):
|
25
|
+
"""
|
26
|
+
Initialize trace manager
|
27
|
+
|
28
|
+
Args:
|
29
|
+
artifacts_base: Base directory for storing trace files
|
30
|
+
"""
|
31
|
+
self.artifacts_base = artifacts_base
|
32
|
+
self.traces_dir = artifacts_base / "traces"
|
33
|
+
self.traces_dir.mkdir(parents=True, exist_ok=True)
|
34
|
+
|
35
|
+
self.current_trace_path: Optional[Path] = None
|
36
|
+
self.is_recording = False
|
37
|
+
self.session_id: Optional[str] = None
|
38
|
+
|
39
|
+
self.logger = logging.getLogger(__name__)
|
40
|
+
|
41
|
+
async def start_trace(self, context: BrowserContext, session_id: str) -> str:
|
42
|
+
"""
|
43
|
+
Start Playwright trace recording
|
44
|
+
|
45
|
+
Args:
|
46
|
+
context: Browser context to record
|
47
|
+
session_id: Unique session identifier
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
Path where trace will be saved
|
51
|
+
"""
|
52
|
+
try:
|
53
|
+
self.session_id = session_id
|
54
|
+
self.current_trace_path = self.traces_dir / f"{session_id}.zip"
|
55
|
+
|
56
|
+
# Start comprehensive trace recording
|
57
|
+
await context.tracing.start(
|
58
|
+
screenshots=True, # Capture screenshots at each action
|
59
|
+
snapshots=True, # Capture DOM snapshots
|
60
|
+
sources=True, # Include source code context
|
61
|
+
title=f"CursorFlow Session {session_id}"
|
62
|
+
)
|
63
|
+
|
64
|
+
self.is_recording = True
|
65
|
+
self.logger.info(f"Started trace recording: {self.current_trace_path}")
|
66
|
+
|
67
|
+
return str(self.current_trace_path)
|
68
|
+
|
69
|
+
except Exception as e:
|
70
|
+
self.logger.error(f"Failed to start trace recording: {e}")
|
71
|
+
self.is_recording = False
|
72
|
+
raise
|
73
|
+
|
74
|
+
async def stop_trace(self, context: BrowserContext) -> Optional[str]:
|
75
|
+
"""
|
76
|
+
Stop trace recording and save file
|
77
|
+
|
78
|
+
Args:
|
79
|
+
context: Browser context being recorded
|
80
|
+
|
81
|
+
Returns:
|
82
|
+
Path to saved trace file, or None if recording wasn't active
|
83
|
+
"""
|
84
|
+
if not self.is_recording or not self.current_trace_path:
|
85
|
+
return None
|
86
|
+
|
87
|
+
try:
|
88
|
+
await context.tracing.stop(path=str(self.current_trace_path))
|
89
|
+
|
90
|
+
trace_path = str(self.current_trace_path)
|
91
|
+
file_size = self.current_trace_path.stat().st_size if self.current_trace_path.exists() else 0
|
92
|
+
|
93
|
+
self.logger.info(f"Trace recording saved: {trace_path} ({file_size:,} bytes)")
|
94
|
+
|
95
|
+
# Reset state
|
96
|
+
self.is_recording = False
|
97
|
+
self.current_trace_path = None
|
98
|
+
self.session_id = None
|
99
|
+
|
100
|
+
return trace_path
|
101
|
+
|
102
|
+
except Exception as e:
|
103
|
+
self.logger.error(f"Failed to stop trace recording: {e}")
|
104
|
+
self.is_recording = False
|
105
|
+
return None
|
106
|
+
|
107
|
+
async def stop_trace_on_error(self, context: BrowserContext, error: Exception) -> Optional[str]:
|
108
|
+
"""
|
109
|
+
Stop trace recording when an error occurs, with error context
|
110
|
+
|
111
|
+
Args:
|
112
|
+
context: Browser context being recorded
|
113
|
+
error: The error that occurred
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
Path to saved trace file with error context
|
117
|
+
"""
|
118
|
+
if not self.is_recording or not self.current_trace_path:
|
119
|
+
return None
|
120
|
+
|
121
|
+
try:
|
122
|
+
# Add error context to trace filename
|
123
|
+
error_trace_path = self.traces_dir / f"{self.session_id}_ERROR_{type(error).__name__}.zip"
|
124
|
+
|
125
|
+
await context.tracing.stop(path=str(error_trace_path))
|
126
|
+
|
127
|
+
file_size = error_trace_path.stat().st_size if error_trace_path.exists() else 0
|
128
|
+
|
129
|
+
self.logger.info(f"Error trace saved: {error_trace_path} ({file_size:,} bytes)")
|
130
|
+
self.logger.info(f"View trace: playwright show-trace {error_trace_path}")
|
131
|
+
|
132
|
+
# Reset state
|
133
|
+
self.is_recording = False
|
134
|
+
self.current_trace_path = None
|
135
|
+
self.session_id = None
|
136
|
+
|
137
|
+
return str(error_trace_path)
|
138
|
+
|
139
|
+
except Exception as trace_error:
|
140
|
+
self.logger.error(f"Failed to save error trace: {trace_error}")
|
141
|
+
self.is_recording = False
|
142
|
+
return None
|
143
|
+
|
144
|
+
def get_trace_info(self) -> Dict[str, Any]:
|
145
|
+
"""
|
146
|
+
Get current trace recording information
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
Dictionary with trace status and metadata
|
150
|
+
"""
|
151
|
+
return {
|
152
|
+
"is_recording": self.is_recording,
|
153
|
+
"session_id": self.session_id,
|
154
|
+
"trace_path": str(self.current_trace_path) if self.current_trace_path else None,
|
155
|
+
"traces_directory": str(self.traces_dir),
|
156
|
+
"total_traces": len(list(self.traces_dir.glob("*.zip")))
|
157
|
+
}
|
158
|
+
|
159
|
+
def cleanup_old_traces(self, max_traces: int = 50) -> int:
|
160
|
+
"""
|
161
|
+
Clean up old trace files to prevent disk space issues
|
162
|
+
|
163
|
+
Args:
|
164
|
+
max_traces: Maximum number of trace files to keep
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
Number of traces deleted
|
168
|
+
"""
|
169
|
+
try:
|
170
|
+
trace_files = sorted(
|
171
|
+
self.traces_dir.glob("*.zip"),
|
172
|
+
key=lambda p: p.stat().st_mtime,
|
173
|
+
reverse=True # Newest first
|
174
|
+
)
|
175
|
+
|
176
|
+
if len(trace_files) <= max_traces:
|
177
|
+
return 0
|
178
|
+
|
179
|
+
# Delete oldest traces
|
180
|
+
traces_to_delete = trace_files[max_traces:]
|
181
|
+
deleted_count = 0
|
182
|
+
|
183
|
+
for trace_file in traces_to_delete:
|
184
|
+
try:
|
185
|
+
trace_file.unlink()
|
186
|
+
deleted_count += 1
|
187
|
+
except Exception as e:
|
188
|
+
self.logger.warning(f"Failed to delete old trace {trace_file}: {e}")
|
189
|
+
|
190
|
+
if deleted_count > 0:
|
191
|
+
self.logger.info(f"Cleaned up {deleted_count} old trace files")
|
192
|
+
|
193
|
+
return deleted_count
|
194
|
+
|
195
|
+
except Exception as e:
|
196
|
+
self.logger.error(f"Trace cleanup failed: {e}")
|
197
|
+
return 0
|
198
|
+
|
199
|
+
def get_viewing_instructions(self, trace_path: str) -> str:
|
200
|
+
"""
|
201
|
+
Get instructions for viewing a trace file
|
202
|
+
|
203
|
+
Args:
|
204
|
+
trace_path: Path to the trace file
|
205
|
+
|
206
|
+
Returns:
|
207
|
+
Command to view the trace
|
208
|
+
"""
|
209
|
+
return f"playwright show-trace {trace_path}"
|