cursorflow 1.2.0__py3-none-any.whl → 1.3.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/cli.py +212 -0
- cursorflow/core/browser_controller.py +659 -7
- cursorflow/core/cursor_integration.py +788 -0
- cursorflow/core/cursorflow.py +151 -0
- cursorflow/core/mockup_comparator.py +1316 -0
- cursorflow-1.3.0.dist-info/METADATA +226 -0
- {cursorflow-1.2.0.dist-info → cursorflow-1.3.0.dist-info}/RECORD +11 -9
- cursorflow-1.3.0.dist-info/licenses/LICENSE +21 -0
- cursorflow-1.2.0.dist-info/METADATA +0 -444
- {cursorflow-1.2.0.dist-info → cursorflow-1.3.0.dist-info}/WHEEL +0 -0
- {cursorflow-1.2.0.dist-info → cursorflow-1.3.0.dist-info}/entry_points.txt +0 -0
- {cursorflow-1.2.0.dist-info → cursorflow-1.3.0.dist-info}/top_level.txt +0 -0
@@ -313,22 +313,55 @@ class BrowserController:
|
|
313
313
|
self.logger.error(f"Condition wait failed: {condition}, {e}")
|
314
314
|
raise
|
315
315
|
|
316
|
-
async def screenshot(self, name: str, full_page: bool = False) -> str:
|
317
|
-
"""Take screenshot - universal"""
|
316
|
+
async def screenshot(self, name: str, full_page: bool = False, capture_comprehensive_data: bool = True) -> Dict[str, Any]:
|
317
|
+
"""Take screenshot with comprehensive page analysis - universal"""
|
318
318
|
try:
|
319
319
|
timestamp = int(time.time())
|
320
|
-
|
320
|
+
screenshot_filename = f"artifacts/screenshots/{name}_{timestamp}.png"
|
321
321
|
|
322
|
+
# Take the visual screenshot
|
322
323
|
await self.page.screenshot(
|
323
|
-
path=
|
324
|
+
path=screenshot_filename,
|
324
325
|
full_page=full_page
|
325
326
|
)
|
326
327
|
|
327
|
-
|
328
|
-
|
328
|
+
# Always return structured data for consistency
|
329
|
+
screenshot_data = {
|
330
|
+
"screenshot_path": screenshot_filename,
|
331
|
+
"timestamp": timestamp,
|
332
|
+
"name": name,
|
333
|
+
"full_page": full_page
|
334
|
+
}
|
335
|
+
|
336
|
+
if capture_comprehensive_data:
|
337
|
+
# Capture comprehensive page analysis
|
338
|
+
comprehensive_data = await self._capture_comprehensive_page_analysis()
|
339
|
+
|
340
|
+
# Save comprehensive data alongside screenshot
|
341
|
+
data_filename = f"artifacts/screenshots/{name}_{timestamp}_comprehensive_data.json"
|
342
|
+
import json
|
343
|
+
with open(data_filename, 'w') as f:
|
344
|
+
json.dump(comprehensive_data, f, indent=2, default=str)
|
345
|
+
|
346
|
+
# Merge all data into structured response
|
347
|
+
screenshot_data.update({
|
348
|
+
"comprehensive_data_path": data_filename,
|
349
|
+
"dom_analysis": comprehensive_data.get("dom_analysis", {}),
|
350
|
+
"network_data": comprehensive_data.get("network_data", {}),
|
351
|
+
"console_data": comprehensive_data.get("console_data", {}),
|
352
|
+
"performance_data": comprehensive_data.get("performance_data", {}),
|
353
|
+
"page_state": comprehensive_data.get("page_state", {}),
|
354
|
+
"analysis_summary": comprehensive_data.get("analysis_summary", {})
|
355
|
+
})
|
356
|
+
|
357
|
+
self.logger.debug(f"Screenshot with comprehensive data saved: {screenshot_filename}, {data_filename}")
|
358
|
+
else:
|
359
|
+
self.logger.debug(f"Screenshot saved: {screenshot_filename}")
|
360
|
+
|
361
|
+
return screenshot_data
|
329
362
|
|
330
363
|
except Exception as e:
|
331
|
-
self.logger.error(f"Screenshot failed: {e}")
|
364
|
+
self.logger.error(f"Screenshot with comprehensive analysis failed: {e}")
|
332
365
|
raise
|
333
366
|
|
334
367
|
async def evaluate_javascript(self, script: str) -> Any:
|
@@ -517,6 +550,625 @@ class BrowserController:
|
|
517
550
|
failed.append(req)
|
518
551
|
return failed
|
519
552
|
|
553
|
+
async def _capture_comprehensive_page_analysis(self) -> Dict[str, Any]:
|
554
|
+
"""Capture comprehensive page analysis including DOM, network, console, and performance data"""
|
555
|
+
try:
|
556
|
+
# Capture DOM analysis
|
557
|
+
dom_analysis = await self._capture_dom_analysis()
|
558
|
+
|
559
|
+
# Capture current network state
|
560
|
+
network_data = self._capture_network_data()
|
561
|
+
|
562
|
+
# Capture current console state
|
563
|
+
console_data = self._capture_console_data()
|
564
|
+
|
565
|
+
# Capture performance metrics
|
566
|
+
performance_data = await self._capture_performance_data()
|
567
|
+
|
568
|
+
# Capture page state information
|
569
|
+
page_state = await self._capture_page_state()
|
570
|
+
|
571
|
+
# Create analysis summary
|
572
|
+
analysis_summary = self._create_analysis_summary(dom_analysis, network_data, console_data, performance_data)
|
573
|
+
|
574
|
+
return {
|
575
|
+
"dom_analysis": dom_analysis,
|
576
|
+
"network_data": network_data,
|
577
|
+
"console_data": console_data,
|
578
|
+
"performance_data": performance_data,
|
579
|
+
"page_state": page_state,
|
580
|
+
"analysis_summary": analysis_summary,
|
581
|
+
"capture_timestamp": time.time(),
|
582
|
+
"analysis_version": "2.0"
|
583
|
+
}
|
584
|
+
|
585
|
+
except Exception as e:
|
586
|
+
self.logger.error(f"Comprehensive page analysis failed: {e}")
|
587
|
+
return {"error": str(e), "capture_timestamp": time.time()}
|
588
|
+
|
589
|
+
async def _capture_dom_analysis(self) -> Dict[str, Any]:
|
590
|
+
"""Capture comprehensive DOM structure and CSS data for every screenshot"""
|
591
|
+
try:
|
592
|
+
dom_analysis = await self.page.evaluate("""
|
593
|
+
() => {
|
594
|
+
// Helper function to get element path
|
595
|
+
function getElementPath(element) {
|
596
|
+
const path = [];
|
597
|
+
while (element && element.nodeType === Node.ELEMENT_NODE) {
|
598
|
+
let selector = element.nodeName.toLowerCase();
|
599
|
+
if (element.id) {
|
600
|
+
selector += '#' + element.id;
|
601
|
+
} else if (element.className && typeof element.className === 'string') {
|
602
|
+
selector += '.' + element.className.split(' ').filter(c => c).join('.');
|
603
|
+
}
|
604
|
+
path.unshift(selector);
|
605
|
+
element = element.parentNode;
|
606
|
+
}
|
607
|
+
return path.join(' > ');
|
608
|
+
}
|
609
|
+
|
610
|
+
// Helper function to get comprehensive computed styles
|
611
|
+
function getComputedStylesDetailed(element) {
|
612
|
+
const computed = window.getComputedStyle(element);
|
613
|
+
return {
|
614
|
+
// Layout properties
|
615
|
+
display: computed.display,
|
616
|
+
position: computed.position,
|
617
|
+
top: computed.top,
|
618
|
+
left: computed.left,
|
619
|
+
right: computed.right,
|
620
|
+
bottom: computed.bottom,
|
621
|
+
width: computed.width,
|
622
|
+
height: computed.height,
|
623
|
+
minWidth: computed.minWidth,
|
624
|
+
maxWidth: computed.maxWidth,
|
625
|
+
minHeight: computed.minHeight,
|
626
|
+
maxHeight: computed.maxHeight,
|
627
|
+
|
628
|
+
// Flexbox properties
|
629
|
+
flexDirection: computed.flexDirection,
|
630
|
+
flexWrap: computed.flexWrap,
|
631
|
+
justifyContent: computed.justifyContent,
|
632
|
+
alignItems: computed.alignItems,
|
633
|
+
alignContent: computed.alignContent,
|
634
|
+
flex: computed.flex,
|
635
|
+
flexGrow: computed.flexGrow,
|
636
|
+
flexShrink: computed.flexShrink,
|
637
|
+
flexBasis: computed.flexBasis,
|
638
|
+
|
639
|
+
// Grid properties
|
640
|
+
gridTemplateColumns: computed.gridTemplateColumns,
|
641
|
+
gridTemplateRows: computed.gridTemplateRows,
|
642
|
+
gridGap: computed.gridGap,
|
643
|
+
gridArea: computed.gridArea,
|
644
|
+
|
645
|
+
// Spacing
|
646
|
+
margin: computed.margin,
|
647
|
+
marginTop: computed.marginTop,
|
648
|
+
marginRight: computed.marginRight,
|
649
|
+
marginBottom: computed.marginBottom,
|
650
|
+
marginLeft: computed.marginLeft,
|
651
|
+
padding: computed.padding,
|
652
|
+
paddingTop: computed.paddingTop,
|
653
|
+
paddingRight: computed.paddingRight,
|
654
|
+
paddingBottom: computed.paddingBottom,
|
655
|
+
paddingLeft: computed.paddingLeft,
|
656
|
+
|
657
|
+
// Typography
|
658
|
+
fontFamily: computed.fontFamily,
|
659
|
+
fontSize: computed.fontSize,
|
660
|
+
fontWeight: computed.fontWeight,
|
661
|
+
fontStyle: computed.fontStyle,
|
662
|
+
lineHeight: computed.lineHeight,
|
663
|
+
letterSpacing: computed.letterSpacing,
|
664
|
+
textAlign: computed.textAlign,
|
665
|
+
textDecoration: computed.textDecoration,
|
666
|
+
textTransform: computed.textTransform,
|
667
|
+
|
668
|
+
// Colors and backgrounds
|
669
|
+
color: computed.color,
|
670
|
+
backgroundColor: computed.backgroundColor,
|
671
|
+
backgroundImage: computed.backgroundImage,
|
672
|
+
backgroundSize: computed.backgroundSize,
|
673
|
+
backgroundPosition: computed.backgroundPosition,
|
674
|
+
backgroundRepeat: computed.backgroundRepeat,
|
675
|
+
|
676
|
+
// Borders
|
677
|
+
border: computed.border,
|
678
|
+
borderTop: computed.borderTop,
|
679
|
+
borderRight: computed.borderRight,
|
680
|
+
borderBottom: computed.borderBottom,
|
681
|
+
borderLeft: computed.borderLeft,
|
682
|
+
borderRadius: computed.borderRadius,
|
683
|
+
borderWidth: computed.borderWidth,
|
684
|
+
borderStyle: computed.borderStyle,
|
685
|
+
borderColor: computed.borderColor,
|
686
|
+
|
687
|
+
// Visual effects
|
688
|
+
boxShadow: computed.boxShadow,
|
689
|
+
opacity: computed.opacity,
|
690
|
+
transform: computed.transform,
|
691
|
+
transition: computed.transition,
|
692
|
+
animation: computed.animation,
|
693
|
+
|
694
|
+
// Z-index and overflow
|
695
|
+
zIndex: computed.zIndex,
|
696
|
+
overflow: computed.overflow,
|
697
|
+
overflowX: computed.overflowX,
|
698
|
+
overflowY: computed.overflowY
|
699
|
+
};
|
700
|
+
}
|
701
|
+
|
702
|
+
// Get all significant elements
|
703
|
+
const elements = [];
|
704
|
+
const selectors = [
|
705
|
+
// Structural elements
|
706
|
+
'body', 'main', 'header', 'nav', 'aside', 'footer', 'section', 'article',
|
707
|
+
// Common containers
|
708
|
+
'.container', '.wrapper', '.content', '.sidebar', '.header', '.footer',
|
709
|
+
'.navbar', '.nav', '.menu', '.main', '.page', '.app', '.layout',
|
710
|
+
// Interactive elements
|
711
|
+
'button', '.btn', '.button', 'a', '.link', 'input', 'form', '.form',
|
712
|
+
'select', 'textarea', '.input', '.field',
|
713
|
+
// Content elements
|
714
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', '.title', '.heading',
|
715
|
+
'.text', '.content', '.description',
|
716
|
+
// Layout elements
|
717
|
+
'.row', '.col', '.column', '.grid', '.flex', '.card', '.panel',
|
718
|
+
'.box', '.item', '.component',
|
719
|
+
// Common UI components
|
720
|
+
'.modal', '.dropdown', '.tooltip', '.alert', '.badge', '.tab',
|
721
|
+
'.table', '.list', '.menu-item'
|
722
|
+
];
|
723
|
+
|
724
|
+
selectors.forEach(selector => {
|
725
|
+
try {
|
726
|
+
document.querySelectorAll(selector).forEach((element, index) => {
|
727
|
+
const rect = element.getBoundingClientRect();
|
728
|
+
const computedStyles = getComputedStylesDetailed(element);
|
729
|
+
|
730
|
+
// Only include visible elements with meaningful size
|
731
|
+
if (rect.width > 0 && rect.height > 0) {
|
732
|
+
elements.push({
|
733
|
+
selector: selector,
|
734
|
+
index: index,
|
735
|
+
uniqueSelector: selector + (index > 0 ? `:nth-of-type(${index + 1})` : ''),
|
736
|
+
elementPath: getElementPath(element),
|
737
|
+
tagName: element.tagName.toLowerCase(),
|
738
|
+
id: element.id || null,
|
739
|
+
className: element.className || null,
|
740
|
+
textContent: element.textContent ? element.textContent.trim().substring(0, 200) : null,
|
741
|
+
|
742
|
+
// Bounding box
|
743
|
+
boundingBox: {
|
744
|
+
x: Math.round(rect.x),
|
745
|
+
y: Math.round(rect.y),
|
746
|
+
width: Math.round(rect.width),
|
747
|
+
height: Math.round(rect.height),
|
748
|
+
top: Math.round(rect.top),
|
749
|
+
left: Math.round(rect.left),
|
750
|
+
right: Math.round(rect.right),
|
751
|
+
bottom: Math.round(rect.bottom)
|
752
|
+
},
|
753
|
+
|
754
|
+
// All computed styles
|
755
|
+
computedStyles: computedStyles,
|
756
|
+
|
757
|
+
// Element attributes
|
758
|
+
attributes: Array.from(element.attributes).reduce((attrs, attr) => {
|
759
|
+
attrs[attr.name] = attr.value;
|
760
|
+
return attrs;
|
761
|
+
}, {}),
|
762
|
+
|
763
|
+
// Element hierarchy info
|
764
|
+
childrenCount: element.children.length,
|
765
|
+
parentTagName: element.parentElement ? element.parentElement.tagName.toLowerCase() : null,
|
766
|
+
|
767
|
+
// Visibility and interaction
|
768
|
+
isVisible: rect.width > 0 && rect.height > 0 &&
|
769
|
+
computedStyles.display !== 'none' &&
|
770
|
+
computedStyles.visibility !== 'hidden' &&
|
771
|
+
parseFloat(computedStyles.opacity) > 0,
|
772
|
+
isInteractive: ['button', 'a', 'input', 'select', 'textarea'].includes(element.tagName.toLowerCase()) ||
|
773
|
+
element.hasAttribute('onclick') ||
|
774
|
+
element.style.cursor === 'pointer'
|
775
|
+
});
|
776
|
+
}
|
777
|
+
});
|
778
|
+
} catch (e) {
|
779
|
+
console.warn(`Failed to analyze selector ${selector}:`, e);
|
780
|
+
}
|
781
|
+
});
|
782
|
+
|
783
|
+
// Get page-level information
|
784
|
+
const pageInfo = {
|
785
|
+
title: document.title,
|
786
|
+
url: window.location.href,
|
787
|
+
viewport: {
|
788
|
+
width: window.innerWidth,
|
789
|
+
height: window.innerHeight
|
790
|
+
},
|
791
|
+
documentSize: {
|
792
|
+
width: Math.max(
|
793
|
+
document.body.scrollWidth || 0,
|
794
|
+
document.body.offsetWidth || 0,
|
795
|
+
document.documentElement.clientWidth || 0,
|
796
|
+
document.documentElement.scrollWidth || 0,
|
797
|
+
document.documentElement.offsetWidth || 0
|
798
|
+
),
|
799
|
+
height: Math.max(
|
800
|
+
document.body.scrollHeight || 0,
|
801
|
+
document.body.offsetHeight || 0,
|
802
|
+
document.documentElement.clientHeight || 0,
|
803
|
+
document.documentElement.scrollHeight || 0,
|
804
|
+
document.documentElement.offsetHeight || 0
|
805
|
+
)
|
806
|
+
},
|
807
|
+
scrollPosition: {
|
808
|
+
x: window.pageXOffset || document.documentElement.scrollLeft || 0,
|
809
|
+
y: window.pageYOffset || document.documentElement.scrollTop || 0
|
810
|
+
}
|
811
|
+
};
|
812
|
+
|
813
|
+
// Analyze page structure
|
814
|
+
const pageStructure = {
|
815
|
+
hasHeader: elements.some(el => ['header', 'nav', '.header', '.navbar'].includes(el.selector)),
|
816
|
+
hasFooter: elements.some(el => ['footer', '.footer'].includes(el.selector)),
|
817
|
+
hasNavigation: elements.some(el => ['nav', '.nav', '.navbar', '.menu'].includes(el.selector)),
|
818
|
+
hasSidebar: elements.some(el => ['.sidebar', '.aside', 'aside'].includes(el.selector)),
|
819
|
+
hasMainContent: elements.some(el => ['main', '.main', '.content'].includes(el.selector)),
|
820
|
+
interactiveElements: elements.filter(el => el.isInteractive).length,
|
821
|
+
totalVisibleElements: elements.length
|
822
|
+
};
|
823
|
+
|
824
|
+
return {
|
825
|
+
pageInfo: pageInfo,
|
826
|
+
pageStructure: pageStructure,
|
827
|
+
elements: elements,
|
828
|
+
totalElements: elements.length,
|
829
|
+
captureTimestamp: Date.now(),
|
830
|
+
analysisVersion: "1.0"
|
831
|
+
};
|
832
|
+
}
|
833
|
+
""")
|
834
|
+
|
835
|
+
return dom_analysis
|
836
|
+
|
837
|
+
except Exception as e:
|
838
|
+
self.logger.error(f"DOM analysis failed: {e}")
|
839
|
+
return {"error": str(e), "captureTimestamp": time.time()}
|
840
|
+
|
841
|
+
def _capture_network_data(self) -> Dict[str, Any]:
|
842
|
+
"""Capture comprehensive network request and response data"""
|
843
|
+
try:
|
844
|
+
# Organize network requests by type
|
845
|
+
requests = [req for req in self.network_requests if req.get("type") == "request"]
|
846
|
+
responses = [req for req in self.network_requests if req.get("type") == "response"]
|
847
|
+
|
848
|
+
# Categorize requests
|
849
|
+
api_requests = [req for req in requests if any(api_path in req.get("url", "") for api_path in ["/api/", "/ajax", ".json", "/graphql"])]
|
850
|
+
static_requests = [req for req in requests if req.get("resource_type") in ["stylesheet", "script", "image", "font"]]
|
851
|
+
navigation_requests = [req for req in requests if req.get("is_navigation_request", False)]
|
852
|
+
|
853
|
+
# Analyze failed requests
|
854
|
+
failed_requests = [req for req in responses if req.get("status", 0) >= 400]
|
855
|
+
|
856
|
+
# Calculate timing statistics
|
857
|
+
request_timings = []
|
858
|
+
for req in requests:
|
859
|
+
# Find matching response
|
860
|
+
matching_response = next((resp for resp in responses if resp.get("url") == req.get("url")), None)
|
861
|
+
if matching_response:
|
862
|
+
timing = matching_response.get("timestamp", 0) - req.get("timestamp", 0)
|
863
|
+
request_timings.append({
|
864
|
+
"url": req.get("url"),
|
865
|
+
"method": req.get("method"),
|
866
|
+
"timing_ms": timing * 1000,
|
867
|
+
"status": matching_response.get("status"),
|
868
|
+
"size": matching_response.get("size", 0)
|
869
|
+
})
|
870
|
+
|
871
|
+
return {
|
872
|
+
"total_requests": len(requests),
|
873
|
+
"total_responses": len(responses),
|
874
|
+
"api_requests": {
|
875
|
+
"count": len(api_requests),
|
876
|
+
"requests": api_requests
|
877
|
+
},
|
878
|
+
"static_requests": {
|
879
|
+
"count": len(static_requests),
|
880
|
+
"requests": static_requests
|
881
|
+
},
|
882
|
+
"navigation_requests": {
|
883
|
+
"count": len(navigation_requests),
|
884
|
+
"requests": navigation_requests
|
885
|
+
},
|
886
|
+
"failed_requests": {
|
887
|
+
"count": len(failed_requests),
|
888
|
+
"requests": failed_requests
|
889
|
+
},
|
890
|
+
"request_timings": request_timings,
|
891
|
+
"network_summary": {
|
892
|
+
"total_requests": len(requests),
|
893
|
+
"successful_requests": len([r for r in responses if 200 <= r.get("status", 0) < 400]),
|
894
|
+
"failed_requests": len(failed_requests),
|
895
|
+
"average_response_time": sum(t["timing_ms"] for t in request_timings) / len(request_timings) if request_timings else 0,
|
896
|
+
"total_data_transferred": sum(r.get("size", 0) for r in responses)
|
897
|
+
},
|
898
|
+
"all_network_events": self.network_requests # Complete raw data
|
899
|
+
}
|
900
|
+
|
901
|
+
except Exception as e:
|
902
|
+
self.logger.error(f"Network data capture failed: {e}")
|
903
|
+
return {"error": str(e)}
|
904
|
+
|
905
|
+
def _capture_console_data(self) -> Dict[str, Any]:
|
906
|
+
"""Capture comprehensive console log data"""
|
907
|
+
try:
|
908
|
+
# Categorize console logs
|
909
|
+
errors = [log for log in self.console_logs if log.get("type") == "error"]
|
910
|
+
warnings = [log for log in self.console_logs if log.get("type") == "warning"]
|
911
|
+
info_logs = [log for log in self.console_logs if log.get("type") in ["log", "info"]]
|
912
|
+
debug_logs = [log for log in self.console_logs if log.get("type") == "debug"]
|
913
|
+
|
914
|
+
# Analyze error patterns
|
915
|
+
error_patterns = {}
|
916
|
+
for error in errors:
|
917
|
+
error_text = error.get("text", "")
|
918
|
+
# Group similar errors
|
919
|
+
error_key = error_text[:100] # First 100 chars as key
|
920
|
+
if error_key not in error_patterns:
|
921
|
+
error_patterns[error_key] = {
|
922
|
+
"count": 0,
|
923
|
+
"first_occurrence": error.get("timestamp"),
|
924
|
+
"last_occurrence": error.get("timestamp"),
|
925
|
+
"sample_error": error
|
926
|
+
}
|
927
|
+
error_patterns[error_key]["count"] += 1
|
928
|
+
error_patterns[error_key]["last_occurrence"] = error.get("timestamp")
|
929
|
+
|
930
|
+
# Recent activity (last 30 seconds)
|
931
|
+
current_time = time.time()
|
932
|
+
recent_logs = [log for log in self.console_logs if current_time - log.get("timestamp", 0) <= 30]
|
933
|
+
|
934
|
+
return {
|
935
|
+
"total_console_logs": len(self.console_logs),
|
936
|
+
"errors": {
|
937
|
+
"count": len(errors),
|
938
|
+
"logs": errors,
|
939
|
+
"patterns": error_patterns
|
940
|
+
},
|
941
|
+
"warnings": {
|
942
|
+
"count": len(warnings),
|
943
|
+
"logs": warnings
|
944
|
+
},
|
945
|
+
"info_logs": {
|
946
|
+
"count": len(info_logs),
|
947
|
+
"logs": info_logs
|
948
|
+
},
|
949
|
+
"debug_logs": {
|
950
|
+
"count": len(debug_logs),
|
951
|
+
"logs": debug_logs
|
952
|
+
},
|
953
|
+
"recent_activity": {
|
954
|
+
"count": len(recent_logs),
|
955
|
+
"logs": recent_logs
|
956
|
+
},
|
957
|
+
"console_summary": {
|
958
|
+
"total_logs": len(self.console_logs),
|
959
|
+
"error_count": len(errors),
|
960
|
+
"warning_count": len(warnings),
|
961
|
+
"unique_error_patterns": len(error_patterns),
|
962
|
+
"has_recent_errors": any(log.get("type") == "error" for log in recent_logs)
|
963
|
+
},
|
964
|
+
"all_console_logs": self.console_logs # Complete raw data
|
965
|
+
}
|
966
|
+
|
967
|
+
except Exception as e:
|
968
|
+
self.logger.error(f"Console data capture failed: {e}")
|
969
|
+
return {"error": str(e)}
|
970
|
+
|
971
|
+
async def _capture_performance_data(self) -> Dict[str, Any]:
|
972
|
+
"""Capture comprehensive performance metrics"""
|
973
|
+
try:
|
974
|
+
# Get browser performance metrics
|
975
|
+
browser_metrics = await self.get_performance_metrics()
|
976
|
+
|
977
|
+
# Get additional performance data from the page
|
978
|
+
additional_metrics = await self.page.evaluate("""
|
979
|
+
() => {
|
980
|
+
const perf = performance;
|
981
|
+
const navigation = perf.getEntriesByType('navigation')[0];
|
982
|
+
const paint = perf.getEntriesByType('paint');
|
983
|
+
const resources = perf.getEntriesByType('resource');
|
984
|
+
|
985
|
+
// Memory usage (if available)
|
986
|
+
const memory = performance.memory ? {
|
987
|
+
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
988
|
+
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
989
|
+
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
|
990
|
+
} : null;
|
991
|
+
|
992
|
+
// Resource timing summary
|
993
|
+
const resourceSummary = {
|
994
|
+
totalResources: resources.length,
|
995
|
+
slowestResource: resources.reduce((slowest, resource) =>
|
996
|
+
resource.duration > (slowest?.duration || 0) ? resource : slowest, null),
|
997
|
+
averageLoadTime: resources.length > 0 ?
|
998
|
+
resources.reduce((sum, r) => sum + r.duration, 0) / resources.length : 0
|
999
|
+
};
|
1000
|
+
|
1001
|
+
return {
|
1002
|
+
navigation: navigation ? {
|
1003
|
+
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
|
1004
|
+
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
|
1005
|
+
domInteractive: navigation.domInteractive - navigation.navigationStart,
|
1006
|
+
domComplete: navigation.domComplete - navigation.navigationStart,
|
1007
|
+
redirectTime: navigation.redirectEnd - navigation.redirectStart,
|
1008
|
+
dnsTime: navigation.domainLookupEnd - navigation.domainLookupStart,
|
1009
|
+
connectTime: navigation.connectEnd - navigation.connectStart,
|
1010
|
+
requestTime: navigation.responseStart - navigation.requestStart,
|
1011
|
+
responseTime: navigation.responseEnd - navigation.responseStart
|
1012
|
+
} : null,
|
1013
|
+
paint: {
|
1014
|
+
firstPaint: paint.find(p => p.name === 'first-paint')?.startTime || 0,
|
1015
|
+
firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0
|
1016
|
+
},
|
1017
|
+
memory: memory,
|
1018
|
+
resources: {
|
1019
|
+
summary: resourceSummary,
|
1020
|
+
details: resources.map(r => ({
|
1021
|
+
name: r.name,
|
1022
|
+
duration: r.duration,
|
1023
|
+
size: r.transferSize,
|
1024
|
+
type: r.initiatorType
|
1025
|
+
}))
|
1026
|
+
},
|
1027
|
+
timing: {
|
1028
|
+
now: performance.now(),
|
1029
|
+
timeOrigin: performance.timeOrigin
|
1030
|
+
}
|
1031
|
+
};
|
1032
|
+
}
|
1033
|
+
""")
|
1034
|
+
|
1035
|
+
return {
|
1036
|
+
"browser_metrics": browser_metrics,
|
1037
|
+
"detailed_metrics": additional_metrics,
|
1038
|
+
"performance_summary": {
|
1039
|
+
"page_load_time": additional_metrics.get("navigation", {}).get("loadComplete", 0),
|
1040
|
+
"dom_content_loaded": additional_metrics.get("navigation", {}).get("domContentLoaded", 0),
|
1041
|
+
"first_paint": additional_metrics.get("paint", {}).get("firstPaint", 0),
|
1042
|
+
"first_contentful_paint": additional_metrics.get("paint", {}).get("firstContentfulPaint", 0),
|
1043
|
+
"total_resources": additional_metrics.get("resources", {}).get("summary", {}).get("totalResources", 0),
|
1044
|
+
"memory_usage_mb": additional_metrics.get("memory", {}).get("usedJSHeapSize", 0) / (1024 * 1024) if additional_metrics.get("memory") else None
|
1045
|
+
}
|
1046
|
+
}
|
1047
|
+
|
1048
|
+
except Exception as e:
|
1049
|
+
self.logger.error(f"Performance data capture failed: {e}")
|
1050
|
+
return {"error": str(e)}
|
1051
|
+
|
1052
|
+
async def _capture_page_state(self) -> Dict[str, Any]:
|
1053
|
+
"""Capture current page state information"""
|
1054
|
+
try:
|
1055
|
+
page_state = await self.page.evaluate("""
|
1056
|
+
() => {
|
1057
|
+
return {
|
1058
|
+
url: window.location.href,
|
1059
|
+
title: document.title,
|
1060
|
+
readyState: document.readyState,
|
1061
|
+
visibilityState: document.visibilityState,
|
1062
|
+
activeElement: document.activeElement ? {
|
1063
|
+
tagName: document.activeElement.tagName,
|
1064
|
+
id: document.activeElement.id,
|
1065
|
+
className: document.activeElement.className
|
1066
|
+
} : null,
|
1067
|
+
viewport: {
|
1068
|
+
width: window.innerWidth,
|
1069
|
+
height: window.innerHeight,
|
1070
|
+
scrollX: window.scrollX,
|
1071
|
+
scrollY: window.scrollY
|
1072
|
+
},
|
1073
|
+
documentSize: {
|
1074
|
+
width: Math.max(
|
1075
|
+
document.body.scrollWidth || 0,
|
1076
|
+
document.body.offsetWidth || 0,
|
1077
|
+
document.documentElement.clientWidth || 0,
|
1078
|
+
document.documentElement.scrollWidth || 0,
|
1079
|
+
document.documentElement.offsetWidth || 0
|
1080
|
+
),
|
1081
|
+
height: Math.max(
|
1082
|
+
document.body.scrollHeight || 0,
|
1083
|
+
document.body.offsetHeight || 0,
|
1084
|
+
document.documentElement.clientHeight || 0,
|
1085
|
+
document.documentElement.scrollHeight || 0,
|
1086
|
+
document.documentElement.offsetHeight || 0
|
1087
|
+
)
|
1088
|
+
},
|
1089
|
+
userAgent: navigator.userAgent,
|
1090
|
+
timestamp: Date.now()
|
1091
|
+
};
|
1092
|
+
}
|
1093
|
+
""")
|
1094
|
+
|
1095
|
+
return page_state
|
1096
|
+
|
1097
|
+
except Exception as e:
|
1098
|
+
self.logger.error(f"Page state capture failed: {e}")
|
1099
|
+
return {"error": str(e)}
|
1100
|
+
|
1101
|
+
def _create_analysis_summary(self, dom_analysis: Dict, network_data: Dict, console_data: Dict, performance_data: Dict) -> Dict[str, Any]:
|
1102
|
+
"""Create high-level analysis summary"""
|
1103
|
+
try:
|
1104
|
+
return {
|
1105
|
+
"page_health": {
|
1106
|
+
"dom_elements_count": dom_analysis.get("totalElements", 0),
|
1107
|
+
"has_errors": console_data.get("console_summary", {}).get("error_count", 0) > 0,
|
1108
|
+
"error_count": console_data.get("console_summary", {}).get("error_count", 0),
|
1109
|
+
"warning_count": console_data.get("console_summary", {}).get("warning_count", 0),
|
1110
|
+
"failed_requests": network_data.get("network_summary", {}).get("failed_requests", 0),
|
1111
|
+
"page_load_time_ms": performance_data.get("performance_summary", {}).get("page_load_time", 0)
|
1112
|
+
},
|
1113
|
+
"interaction_readiness": {
|
1114
|
+
"interactive_elements": dom_analysis.get("pageStructure", {}).get("interactiveElements", 0),
|
1115
|
+
"has_navigation": dom_analysis.get("pageStructure", {}).get("hasNavigation", False),
|
1116
|
+
"has_main_content": dom_analysis.get("pageStructure", {}).get("hasMainContent", False),
|
1117
|
+
"page_ready": dom_analysis.get("pageInfo", {}).get("title", "") != ""
|
1118
|
+
},
|
1119
|
+
"technical_metrics": {
|
1120
|
+
"total_network_requests": network_data.get("network_summary", {}).get("total_requests", 0),
|
1121
|
+
"average_response_time_ms": network_data.get("network_summary", {}).get("average_response_time", 0),
|
1122
|
+
"memory_usage_mb": performance_data.get("performance_summary", {}).get("memory_usage_mb"),
|
1123
|
+
"first_contentful_paint_ms": performance_data.get("performance_summary", {}).get("first_contentful_paint", 0)
|
1124
|
+
},
|
1125
|
+
"quality_indicators": {
|
1126
|
+
"has_console_errors": console_data.get("console_summary", {}).get("has_recent_errors", False),
|
1127
|
+
"has_failed_requests": network_data.get("failed_requests", {}).get("count", 0) > 0,
|
1128
|
+
"performance_score": self._calculate_performance_score(performance_data),
|
1129
|
+
"overall_health": "good" if (
|
1130
|
+
console_data.get("console_summary", {}).get("error_count", 0) == 0 and
|
1131
|
+
network_data.get("failed_requests", {}).get("count", 0) == 0 and
|
1132
|
+
performance_data.get("performance_summary", {}).get("page_load_time", 0) < 3000
|
1133
|
+
) else "needs_attention"
|
1134
|
+
}
|
1135
|
+
}
|
1136
|
+
|
1137
|
+
except Exception as e:
|
1138
|
+
self.logger.error(f"Analysis summary creation failed: {e}")
|
1139
|
+
return {"error": str(e)}
|
1140
|
+
|
1141
|
+
def _calculate_performance_score(self, performance_data: Dict) -> int:
|
1142
|
+
"""Calculate a simple performance score (0-100)"""
|
1143
|
+
try:
|
1144
|
+
score = 100
|
1145
|
+
|
1146
|
+
# Deduct points for slow loading
|
1147
|
+
load_time = performance_data.get("performance_summary", {}).get("page_load_time", 0)
|
1148
|
+
if load_time > 3000:
|
1149
|
+
score -= 30
|
1150
|
+
elif load_time > 1000:
|
1151
|
+
score -= 15
|
1152
|
+
|
1153
|
+
# Deduct points for slow first contentful paint
|
1154
|
+
fcp = performance_data.get("performance_summary", {}).get("first_contentful_paint", 0)
|
1155
|
+
if fcp > 2000:
|
1156
|
+
score -= 20
|
1157
|
+
elif fcp > 1000:
|
1158
|
+
score -= 10
|
1159
|
+
|
1160
|
+
# Deduct points for high memory usage
|
1161
|
+
memory_mb = performance_data.get("performance_summary", {}).get("memory_usage_mb")
|
1162
|
+
if memory_mb and memory_mb > 100:
|
1163
|
+
score -= 20
|
1164
|
+
elif memory_mb and memory_mb > 50:
|
1165
|
+
score -= 10
|
1166
|
+
|
1167
|
+
return max(0, score)
|
1168
|
+
|
1169
|
+
except Exception as e:
|
1170
|
+
return 50 # Default middle score if calculation fails
|
1171
|
+
|
520
1172
|
def get_collected_data(self) -> Dict:
|
521
1173
|
"""Get all collected browser data"""
|
522
1174
|
return {
|