cursorflow 1.2.0__py3-none-any.whl → 1.3.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.
@@ -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
- filename = f"artifacts/screenshots/{name}_{timestamp}.png"
320
+ screenshot_filename = f"artifacts/screenshots/{name}_{timestamp}.png"
321
321
 
322
+ # Take the visual screenshot
322
323
  await self.page.screenshot(
323
- path=filename,
324
+ path=screenshot_filename,
324
325
  full_page=full_page
325
326
  )
326
327
 
327
- self.logger.debug(f"Screenshot saved: {filename}")
328
- return filename
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 {