cursorflow 1.3.7__py3-none-any.whl → 2.0.1__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/rules/cursorflow-installation.mdc +96 -50
- cursorflow/rules/cursorflow-usage.mdc +227 -91
- cursorflow-2.0.1.dist-info/METADATA +293 -0
- {cursorflow-1.3.7.dist-info → cursorflow-2.0.1.dist-info}/RECORD +12 -9
- cursorflow-1.3.7.dist-info/METADATA +0 -249
- {cursorflow-1.3.7.dist-info → cursorflow-2.0.1.dist-info}/WHEEL +0 -0
- {cursorflow-1.3.7.dist-info → cursorflow-2.0.1.dist-info}/entry_points.txt +0 -0
- {cursorflow-1.3.7.dist-info → cursorflow-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {cursorflow-1.3.7.dist-info → cursorflow-2.0.1.dist-info}/top_level.txt +0 -0
@@ -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
|
+
}
|