cursorflow 1.3.6__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.
@@ -0,0 +1,590 @@
1
+ """
2
+ CursorFlow v2.0 Enhanced Error Context Collection System
3
+
4
+ This module provides intelligent error context data collection with smart
5
+ screenshot deduplication, maintaining our core philosophy of pure data
6
+ collection without analysis or recommendations.
7
+
8
+ Core Philosophy: Collect more error context data, collect it better,
9
+ but never analyze what the errors mean - that's the AI's job.
10
+ """
11
+
12
+ import asyncio
13
+ import hashlib
14
+ import json
15
+ import logging
16
+ import time
17
+ from pathlib import Path
18
+ from typing import Dict, List, Optional, Any
19
+
20
+
21
+ class ErrorContextCollector:
22
+ """
23
+ v2.0 Enhancement: Intelligent error context data collection
24
+
25
+ Collects comprehensive error context data while avoiding duplicate
26
+ screenshots and maintaining efficient artifact management.
27
+ """
28
+
29
+ def __init__(self, page, logger: logging.Logger):
30
+ self.page = page
31
+ self.logger = logger
32
+
33
+ # Smart screenshot deduplication
34
+ self.recent_screenshots = {} # timestamp -> screenshot_info
35
+ self.content_hash_to_screenshot = {} # content_hash -> screenshot_path
36
+ self.screenshot_dedup_window = 5.0 # 5 seconds
37
+
38
+ # Error context tracking
39
+ self.error_contexts = []
40
+ self.recent_actions = []
41
+
42
+ # Ensure diagnostics directory exists
43
+ diagnostics_dir = Path(".cursorflow/artifacts/diagnostics")
44
+ diagnostics_dir.mkdir(parents=True, exist_ok=True)
45
+
46
+ async def capture_error_context(self, error_event: Dict[str, Any]) -> Dict[str, Any]:
47
+ """
48
+ Capture comprehensive error context data with smart deduplication
49
+
50
+ Args:
51
+ error_event: The error event that triggered context collection
52
+
53
+ Returns:
54
+ Complete error context data for AI analysis
55
+ """
56
+ try:
57
+ current_time = time.time()
58
+
59
+ # Capture screenshot with smart deduplication
60
+ screenshot_info = await self._capture_smart_screenshot(current_time, error_event)
61
+
62
+ # Capture comprehensive context data
63
+ context_data = {
64
+ 'error_timestamp': current_time,
65
+ 'error_details': error_event,
66
+ 'screenshot_info': screenshot_info,
67
+
68
+ # DOM state at error time
69
+ 'dom_snapshot': await self._capture_error_dom_snapshot(),
70
+
71
+ # Page state information
72
+ 'page_state': await self._capture_error_page_state(),
73
+
74
+ # Console context (last 10 messages)
75
+ 'console_context': await self._capture_console_context(),
76
+
77
+ # Network context (last 20 requests)
78
+ 'network_context': await self._capture_network_context(),
79
+
80
+ # Recent browser actions
81
+ 'action_context': self._capture_action_context(current_time),
82
+
83
+ # Element visibility and interaction state
84
+ 'element_context': await self._capture_element_context(error_event),
85
+
86
+ # Performance state at error time
87
+ 'performance_context': await self._capture_performance_context(),
88
+
89
+ # Browser environment context
90
+ 'environment_context': await self._capture_environment_context(),
91
+
92
+ # Error correlation data
93
+ 'correlation_context': self._capture_correlation_context(current_time, error_event)
94
+ }
95
+
96
+ # Store context for potential correlation with future errors
97
+ self.error_contexts.append(context_data)
98
+
99
+ # Keep only last 50 error contexts to prevent memory issues
100
+ if len(self.error_contexts) > 50:
101
+ self.error_contexts = self.error_contexts[-50:]
102
+
103
+ self.logger.info(f"📊 Error context captured: {error_event.get('type', 'unknown')} at {current_time}")
104
+
105
+ return context_data
106
+
107
+ except Exception as e:
108
+ self.logger.error(f"Error context capture failed: {e}")
109
+ return {
110
+ 'error_timestamp': time.time(),
111
+ 'error_details': error_event,
112
+ 'context_capture_error': str(e),
113
+ 'partial_data': True
114
+ }
115
+
116
+ async def _capture_smart_screenshot(self, current_time: float, error_event: Dict) -> Dict[str, Any]:
117
+ """Capture screenshot with intelligent deduplication"""
118
+ try:
119
+ # Check if we should capture a new screenshot based on error type
120
+ if not self._should_capture_screenshot_for_error(error_event):
121
+ return {
122
+ 'screenshot_captured': False,
123
+ 'reason': f"Error type '{error_event.get('type')}' typically doesn't require visual context",
124
+ 'screenshot_path': None
125
+ }
126
+
127
+ # Check for reusable recent screenshot
128
+ reusable_screenshot = self._find_reusable_screenshot(current_time)
129
+
130
+ if reusable_screenshot:
131
+ # Reuse existing screenshot
132
+ self.recent_screenshots[reusable_screenshot['timestamp']]['error_count'] += 1
133
+
134
+ return {
135
+ 'screenshot_path': reusable_screenshot['path'],
136
+ 'screenshot_timestamp': reusable_screenshot['timestamp'],
137
+ 'shared_with_errors': reusable_screenshot['error_count'] + 1,
138
+ 'is_reused': True,
139
+ 'reuse_reason': 'Recent screenshot within deduplication window'
140
+ }
141
+
142
+ # Check for content-based deduplication
143
+ content_hash = await self._generate_content_hash()
144
+ if content_hash in self.content_hash_to_screenshot:
145
+ return {
146
+ 'screenshot_path': self.content_hash_to_screenshot[content_hash],
147
+ 'is_content_duplicate': True,
148
+ 'content_hash': content_hash,
149
+ 'reuse_reason': 'Identical page content detected'
150
+ }
151
+
152
+ # Capture new screenshot
153
+ screenshot_filename = f"error_context_{int(current_time)}.png"
154
+ screenshot_path = f".cursorflow/artifacts/diagnostics/{screenshot_filename}"
155
+
156
+ await self.page.screenshot(path=screenshot_path, full_page=True)
157
+
158
+ screenshot_info = {
159
+ 'screenshot_path': screenshot_path,
160
+ 'screenshot_timestamp': current_time,
161
+ 'shared_with_errors': 1,
162
+ 'is_reused': False,
163
+ 'content_hash': content_hash,
164
+ 'full_page': True
165
+ }
166
+
167
+ # Store for potential reuse
168
+ self.recent_screenshots[current_time] = {
169
+ 'path': screenshot_path,
170
+ 'timestamp': current_time,
171
+ 'error_count': 1
172
+ }
173
+ self.content_hash_to_screenshot[content_hash] = screenshot_path
174
+
175
+ # Clean up old references
176
+ self._cleanup_old_screenshot_refs(current_time)
177
+
178
+ return screenshot_info
179
+
180
+ except Exception as e:
181
+ self.logger.error(f"Screenshot capture failed: {e}")
182
+ return {
183
+ 'screenshot_captured': False,
184
+ 'error': str(e),
185
+ 'screenshot_path': None
186
+ }
187
+
188
+ def _should_capture_screenshot_for_error(self, error_event: Dict) -> bool:
189
+ """Determine if this error type needs visual context"""
190
+ error_type = error_event.get('type', 'unknown')
191
+
192
+ # Visual/interaction errors always need screenshots
193
+ visual_error_types = [
194
+ 'selector_failed',
195
+ 'click_failed',
196
+ 'element_not_visible',
197
+ 'layout_shift',
198
+ 'css_error',
199
+ 'render_error'
200
+ ]
201
+
202
+ if error_type in visual_error_types:
203
+ return True
204
+
205
+ # Console errors might need screenshots if they're UI-related
206
+ if error_type == 'console_error':
207
+ error_message = error_event.get('message', '').lower()
208
+ ui_related_keywords = ['element', 'dom', 'css', 'style', 'render', 'layout']
209
+ return any(keyword in error_message for keyword in ui_related_keywords)
210
+
211
+ # Network errors typically don't need screenshots
212
+ if error_type == 'network_error':
213
+ return False
214
+
215
+ # Default: capture screenshot for unknown error types
216
+ return True
217
+
218
+ def _find_reusable_screenshot(self, current_time: float) -> Optional[Dict]:
219
+ """Find a recent screenshot that can be reused"""
220
+ for timestamp, screenshot_data in self.recent_screenshots.items():
221
+ if current_time - timestamp <= self.screenshot_dedup_window:
222
+ return screenshot_data
223
+ return None
224
+
225
+ async def _generate_content_hash(self) -> str:
226
+ """Generate a hash of current page content for deduplication"""
227
+ try:
228
+ content_fingerprint = await self.page.evaluate("""
229
+ () => {
230
+ const body = document.body;
231
+ if (!body) return 'no-body';
232
+
233
+ // Create content fingerprint from key page characteristics
234
+ const fingerprint = {
235
+ text_sample: body.innerText.substring(0, 1000),
236
+ element_count: body.querySelectorAll('*').length,
237
+ viewport: window.innerWidth + 'x' + window.innerHeight,
238
+ url: window.location.href,
239
+ title: document.title
240
+ };
241
+
242
+ return JSON.stringify(fingerprint);
243
+ }
244
+ """)
245
+
246
+ # Create hash from fingerprint
247
+ return hashlib.md5(content_fingerprint.encode()).hexdigest()[:16]
248
+
249
+ except Exception as e:
250
+ self.logger.error(f"Content hash generation failed: {e}")
251
+ return f"hash_error_{int(time.time())}"
252
+
253
+ def _cleanup_old_screenshot_refs(self, current_time: float):
254
+ """Remove old screenshot references outside the dedup window"""
255
+ expired_timestamps = [
256
+ ts for ts in self.recent_screenshots.keys()
257
+ if current_time - ts > self.screenshot_dedup_window
258
+ ]
259
+ for ts in expired_timestamps:
260
+ del self.recent_screenshots[ts]
261
+
262
+ # Also cleanup content hash references (keep last 20)
263
+ if len(self.content_hash_to_screenshot) > 20:
264
+ # Remove oldest entries
265
+ sorted_items = sorted(self.content_hash_to_screenshot.items())
266
+ items_to_keep = sorted_items[-20:]
267
+ self.content_hash_to_screenshot = dict(items_to_keep)
268
+
269
+ async def _capture_error_dom_snapshot(self) -> Dict[str, Any]:
270
+ """Capture DOM state at error time"""
271
+ try:
272
+ dom_snapshot = await self.page.evaluate("""
273
+ () => {
274
+ // Capture essential DOM state information
275
+ const snapshot = {
276
+ document_ready_state: document.readyState,
277
+ active_element: document.activeElement ? {
278
+ tag_name: document.activeElement.tagName.toLowerCase(),
279
+ id: document.activeElement.id,
280
+ class_name: document.activeElement.className
281
+ } : null,
282
+ visible_elements_count: document.querySelectorAll('*').length,
283
+ interactive_elements: Array.from(document.querySelectorAll('button, a, input, select, textarea')).map(el => ({
284
+ tag_name: el.tagName.toLowerCase(),
285
+ id: el.id,
286
+ class_name: el.className,
287
+ visible: el.offsetWidth > 0 && el.offsetHeight > 0,
288
+ disabled: el.disabled
289
+ })),
290
+ forms_data: Array.from(document.forms).map(form => ({
291
+ id: form.id,
292
+ name: form.name,
293
+ method: form.method,
294
+ action: form.action,
295
+ elements_count: form.elements.length
296
+ })),
297
+ scripts_count: document.scripts.length,
298
+ stylesheets_count: document.styleSheets.length
299
+ };
300
+
301
+ return snapshot;
302
+ }
303
+ """)
304
+
305
+ return dom_snapshot
306
+
307
+ except Exception as e:
308
+ self.logger.error(f"DOM snapshot capture failed: {e}")
309
+ return {'error': str(e)}
310
+
311
+ async def _capture_error_page_state(self) -> Dict[str, Any]:
312
+ """Capture page state at error time"""
313
+ try:
314
+ page_state = await self.page.evaluate("""
315
+ () => {
316
+ return {
317
+ url: window.location.href,
318
+ title: document.title,
319
+ ready_state: document.readyState,
320
+ visibility_state: document.visibilityState,
321
+ viewport: {
322
+ width: window.innerWidth,
323
+ height: window.innerHeight
324
+ },
325
+ scroll_position: {
326
+ x: window.pageXOffset,
327
+ y: window.pageYOffset
328
+ },
329
+ document_size: {
330
+ width: Math.max(
331
+ document.body.scrollWidth,
332
+ document.body.offsetWidth,
333
+ document.documentElement.clientWidth,
334
+ document.documentElement.scrollWidth,
335
+ document.documentElement.offsetWidth
336
+ ),
337
+ height: Math.max(
338
+ document.body.scrollHeight,
339
+ document.body.offsetHeight,
340
+ document.documentElement.clientHeight,
341
+ document.documentElement.scrollHeight,
342
+ document.documentElement.offsetHeight
343
+ )
344
+ },
345
+ user_agent: navigator.userAgent,
346
+ timestamp: Date.now()
347
+ };
348
+ }
349
+ """)
350
+
351
+ return page_state
352
+
353
+ except Exception as e:
354
+ self.logger.error(f"Page state capture failed: {e}")
355
+ return {'error': str(e)}
356
+
357
+ async def _capture_console_context(self) -> List[Dict]:
358
+ """Capture recent console messages for context"""
359
+ # Get console logs from BrowserController (last 10 messages)
360
+ try:
361
+ # This will be populated via integration - for now return empty
362
+ # The BrowserController will pass console_logs when initializing
363
+ return getattr(self, '_console_logs_ref', [])[-10:]
364
+ except Exception:
365
+ return []
366
+
367
+ async def _capture_network_context(self) -> List[Dict]:
368
+ """Capture recent network requests for context"""
369
+ # Get network requests from BrowserController (last 20 requests)
370
+ try:
371
+ # This will be populated via integration - for now return empty
372
+ # The BrowserController will pass network_requests when initializing
373
+ return getattr(self, '_network_requests_ref', [])[-20:]
374
+ except Exception:
375
+ return []
376
+
377
+ def set_browser_data_references(self, console_logs: List[Dict], network_requests: List[Dict]):
378
+ """Set references to browser data from BrowserController"""
379
+ self._console_logs_ref = console_logs
380
+ self._network_requests_ref = network_requests
381
+
382
+ def _capture_action_context(self, error_time: float) -> List[Dict]:
383
+ """Capture recent browser actions that might be related to the error"""
384
+ # Get actions from the last 30 seconds
385
+ recent_actions = [
386
+ action for action in self.recent_actions
387
+ if error_time - action.get('timestamp', 0) <= 30.0
388
+ ]
389
+
390
+ return recent_actions[-10:] # Last 10 actions
391
+
392
+ async def _capture_element_context(self, error_event: Dict) -> Dict[str, Any]:
393
+ """Capture element-specific context if the error is element-related"""
394
+ try:
395
+ # If error involves a specific element/selector
396
+ selector = error_event.get('selector') or error_event.get('element')
397
+
398
+ if not selector:
399
+ return {'no_element_context': True}
400
+
401
+ element_context = await self.page.evaluate(f"""
402
+ (selector) => {{
403
+ try {{
404
+ const element = document.querySelector(selector);
405
+
406
+ if (!element) {{
407
+ // Element not found - get similar elements
408
+ const allElements = Array.from(document.querySelectorAll('*'));
409
+ const similarElements = allElements.filter(el => {{
410
+ return el.id.includes(selector.replace('#', '').replace('.', '')) ||
411
+ el.className.includes(selector.replace('#', '').replace('.', ''));
412
+ }}).slice(0, 5);
413
+
414
+ return {{
415
+ element_found: false,
416
+ selector: selector,
417
+ similar_elements: similarElements.map(el => ({{
418
+ tag_name: el.tagName.toLowerCase(),
419
+ id: el.id,
420
+ class_name: el.className,
421
+ text_content: el.textContent ? el.textContent.trim().substring(0, 50) : null
422
+ }}))
423
+ }};
424
+ }}
425
+
426
+ // Element found - get detailed info
427
+ const rect = element.getBoundingClientRect();
428
+ const computedStyle = window.getComputedStyle(element);
429
+
430
+ return {{
431
+ element_found: true,
432
+ selector: selector,
433
+ element_info: {{
434
+ tag_name: element.tagName.toLowerCase(),
435
+ id: element.id,
436
+ class_name: element.className,
437
+ text_content: element.textContent ? element.textContent.trim().substring(0, 100) : null,
438
+ bounding_box: {{
439
+ x: Math.round(rect.x),
440
+ y: Math.round(rect.y),
441
+ width: Math.round(rect.width),
442
+ height: Math.round(rect.height)
443
+ }},
444
+ visibility: {{
445
+ is_visible: rect.width > 0 && rect.height > 0,
446
+ display: computedStyle.display,
447
+ visibility: computedStyle.visibility,
448
+ opacity: computedStyle.opacity
449
+ }},
450
+ interaction_state: {{
451
+ disabled: element.disabled,
452
+ readonly: element.readOnly,
453
+ focusable: element.tabIndex >= 0
454
+ }}
455
+ }}
456
+ }};
457
+ }} catch (e) {{
458
+ return {{
459
+ element_context_error: e.message,
460
+ selector: selector
461
+ }};
462
+ }}
463
+ }}
464
+ """, selector)
465
+
466
+ return element_context
467
+
468
+ except Exception as e:
469
+ self.logger.error(f"Element context capture failed: {e}")
470
+ return {'error': str(e)}
471
+
472
+ async def _capture_performance_context(self) -> Dict[str, Any]:
473
+ """Capture performance state at error time"""
474
+ try:
475
+ performance_context = await self.page.evaluate("""
476
+ () => {
477
+ const performance = window.performance;
478
+ const navigation = performance.getEntriesByType('navigation')[0];
479
+
480
+ return {
481
+ timing: {
482
+ dom_content_loaded: navigation ? navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart : null,
483
+ load_complete: navigation ? navigation.loadEventEnd - navigation.loadEventStart : null
484
+ },
485
+ memory: performance.memory ? {
486
+ used_js_heap_size: performance.memory.usedJSHeapSize,
487
+ total_js_heap_size: performance.memory.totalJSHeapSize,
488
+ js_heap_size_limit: performance.memory.jsHeapSizeLimit
489
+ } : null,
490
+ resource_count: performance.getEntriesByType('resource').length,
491
+ timestamp: performance.now()
492
+ };
493
+ }
494
+ """)
495
+
496
+ return performance_context
497
+
498
+ except Exception as e:
499
+ self.logger.error(f"Performance context capture failed: {e}")
500
+ return {'error': str(e)}
501
+
502
+ async def _capture_environment_context(self) -> Dict[str, Any]:
503
+ """Capture browser environment context"""
504
+ try:
505
+ environment_context = await self.page.evaluate("""
506
+ () => {
507
+ return {
508
+ user_agent: navigator.userAgent,
509
+ platform: navigator.platform,
510
+ language: navigator.language,
511
+ cookie_enabled: navigator.cookieEnabled,
512
+ online: navigator.onLine,
513
+ connection: navigator.connection ? {
514
+ effective_type: navigator.connection.effectiveType,
515
+ downlink: navigator.connection.downlink,
516
+ rtt: navigator.connection.rtt
517
+ } : null,
518
+ screen: {
519
+ width: screen.width,
520
+ height: screen.height,
521
+ color_depth: screen.colorDepth
522
+ },
523
+ timezone_offset: new Date().getTimezoneOffset()
524
+ };
525
+ }
526
+ """)
527
+
528
+ return environment_context
529
+
530
+ except Exception as e:
531
+ self.logger.error(f"Environment context capture failed: {e}")
532
+ return {'error': str(e)}
533
+
534
+ def _capture_correlation_context(self, error_time: float, error_event: Dict) -> Dict[str, Any]:
535
+ """Capture data for correlating this error with other events"""
536
+
537
+ # Find recent errors for pattern detection
538
+ recent_errors = [
539
+ ctx for ctx in self.error_contexts
540
+ if error_time - ctx.get('error_timestamp', 0) <= 60.0 # Last minute
541
+ ]
542
+
543
+ # Find errors of the same type
544
+ same_type_errors = [
545
+ ctx for ctx in recent_errors
546
+ if ctx.get('error_details', {}).get('type') == error_event.get('type')
547
+ ]
548
+
549
+ return {
550
+ 'recent_errors_count': len(recent_errors),
551
+ 'same_type_errors_count': len(same_type_errors),
552
+ 'error_frequency': len(recent_errors) / 60.0 if recent_errors else 0, # errors per second
553
+ 'time_since_last_error': min([
554
+ error_time - ctx.get('error_timestamp', 0)
555
+ for ctx in recent_errors
556
+ ]) if recent_errors else None,
557
+ 'error_pattern_detected': len(same_type_errors) >= 3 # 3+ similar errors indicate pattern
558
+ }
559
+
560
+ def record_action(self, action_type: str, details: Dict = None):
561
+ """Record a browser action for context correlation"""
562
+ action_record = {
563
+ 'timestamp': time.time(),
564
+ 'action_type': action_type,
565
+ 'details': details or {}
566
+ }
567
+
568
+ self.recent_actions.append(action_record)
569
+
570
+ # Keep only last 100 actions to prevent memory issues
571
+ if len(self.recent_actions) > 100:
572
+ self.recent_actions = self.recent_actions[-100:]
573
+
574
+ def get_error_context_summary(self) -> Dict[str, Any]:
575
+ """Get summary of collected error contexts"""
576
+ if not self.error_contexts:
577
+ return {'no_errors_recorded': True}
578
+
579
+ error_types = {}
580
+ for ctx in self.error_contexts:
581
+ error_type = ctx.get('error_details', {}).get('type', 'unknown')
582
+ error_types[error_type] = error_types.get(error_type, 0) + 1
583
+
584
+ return {
585
+ 'total_errors': len(self.error_contexts),
586
+ 'error_types': error_types,
587
+ 'screenshots_captured': len(self.recent_screenshots),
588
+ 'unique_content_hashes': len(self.content_hash_to_screenshot),
589
+ 'recent_actions': len(self.recent_actions)
590
+ }