import { useState, useMemo, useEffect, lazy, Suspense } from 'react' import { useDatabaseTraceData } from '../../hooks/useDatabaseTraceData' import { getUrlParams, updateUrlWithTraceId } from '../../App' import RequestFilters from './RequestFilters' import RequestsTable from './RequestsTable' import ColumnSettings from './ColumnSettings' import styles from './HTTPRequestViewer.module.css' // Lazy load 3D viewers to reduce main bundle size const BabylonViewer = lazy(() => import('../../BabylonViewer')) const BabylonTimelineViewer = lazy(() => import('../../BabylonTimelineViewer')) // Loading component for 3D viewers const ThreeDViewerLoading = () => (
Loading 3D Viewer...
Initializing Babylon.js
) // Imported utilities import { ITEMS_PER_PAGE, SSIM_SIMILARITY_THRESHOLD } from './lib/httpRequestConstants' import { extractScreenshots, findUniqueScreenshots } from './lib/screenshotUtils' import { processHTTPRequests } from './lib/httpRequestProcessor' import { analyzeCDN, analyzeQueueReason } from './lib/analysisUtils' import { addRequestPostProcessing } from './lib/requestPostProcessor' import { assignConnectionNumbers } from './lib/connectionUtils' // Import types import type { HTTPRequest, ScreenshotEvent } from './types/httpRequest' import HTTPRequestLoading from "./HTTPRequestLoading.tsx"; import sortRequests from "./lib/sortRequests.ts"; import PaginationControls from "./PaginationControls.tsx"; import { exportRequestsToCSV, downloadCSV } from './lib/csvExport'; interface HTTPRequestViewerProps { traceId: string | null } export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) { const { traceData, loading, error } = useDatabaseTraceData(traceId) const [currentPage, setCurrentPage] = useState(1) const [searchTerm, setSearchTerm] = useState('') const [resourceTypeFilter, setResourceTypeFilter] = useState('all') const [protocolFilter, setProtocolFilter] = useState('all') const [hostnameFilter, setHostnameFilter] = useState('all') const [priorityFilter, setPriorityFilter] = useState('all') const [showQueueAnalysis, setShowQueueAnalysis] = useState(false) const [showScreenshots, setShowScreenshots] = useState(false) // 3D viewer state - initialized from URL const [show3DViewer, setShow3DViewer] = useState(false) const [showTimelineViewer, setShowTimelineViewer] = useState(false) // Column visibility state - default visible columns as requested const [visibleColumns, setVisibleColumns] = useState(() => { const saved = localStorage.getItem('httpRequestTableColumns') if (saved) { try { return JSON.parse(saved) } catch (e) { console.warn('Failed to parse saved column settings, using defaults') } } // Default visible columns return { expand: true, method: false, status: false, type: false, priority: false, url: true, connectionNumber: false, requestNumber: false, startTime: true, queueTime: false, dns: false, connection: false, serverLatency: false, duration: false, totalResponseTime: true, dataRate: true, size: false, contentLength: true, protocol: false, cdn: false, cache: false } }) const [columnSettingsOpen, setColumnSettingsOpen] = useState(false) // Initialize 3D view state from URL on component mount and handle URL changes useEffect(() => { const updateFrom3DUrl = () => { const { threeDView } = getUrlParams() setShow3DViewer(threeDView === 'network') setShowTimelineViewer(threeDView === 'timeline') } // Set initial state updateFrom3DUrl() // Listen for URL changes (back/forward navigation) const handlePopState = () => updateFrom3DUrl() window.addEventListener('popstate', handlePopState) return () => window.removeEventListener('popstate', handlePopState) }, []) // Handle 3D view changes and update URL const handle3DViewerToggle = (enabled: boolean) => { const { traceId, view } = getUrlParams() setShow3DViewer(enabled) setShowTimelineViewer(false) // Ensure only one 3D view is active updateUrlWithTraceId(traceId, view, enabled ? 'network' : null) } const handleTimelineViewerToggle = (enabled: boolean) => { const { traceId, view } = getUrlParams() setShowTimelineViewer(enabled) setShow3DViewer(false) // Ensure only one 3D view is active updateUrlWithTraceId(traceId, view, enabled ? 'timeline' : null) } const [ssimThreshold, setSsimThreshold] = useState(SSIM_SIMILARITY_THRESHOLD) const [pendingSSIMThreshold, setPendingSSIMThreshold] = useState(SSIM_SIMILARITY_THRESHOLD) const [expandedRows, setExpandedRows] = useState>(new Set()) const handleSSIMRecalculate = () => { setSsimThreshold(pendingSSIMThreshold) } // Column visibility handlers const handleColumnToggle = (column: string) => { const newVisibleColumns = { ...visibleColumns, [column]: !visibleColumns[column] } setVisibleColumns(newVisibleColumns) localStorage.setItem('httpRequestTableColumns', JSON.stringify(newVisibleColumns)) } const handleShowAllColumns = () => { const allVisible = Object.keys(visibleColumns).reduce((acc, key) => { acc[key] = true return acc }, {} as Record) setVisibleColumns(allVisible) localStorage.setItem('httpRequestTableColumns', JSON.stringify(allVisible)) } const handleHideAllColumns = () => { const allHidden = Object.keys(visibleColumns).reduce((acc, key) => { acc[key] = key === 'expand' // Always keep expand column visible return acc }, {} as Record) setVisibleColumns(allHidden) localStorage.setItem('httpRequestTableColumns', JSON.stringify(allHidden)) } const handleResetDefaults = () => { const defaultColumns = { expand: true, method: false, status: false, type: false, priority: false, url: true, connectionNumber: false, requestNumber: false, startTime: true, queueTime: false, dns: false, connection: false, serverLatency: false, duration: false, totalResponseTime: true, dataRate: true, size: false, contentLength: true, protocol: false, cdn: false, cache: false } setVisibleColumns(defaultColumns) localStorage.setItem('httpRequestTableColumns', JSON.stringify(defaultColumns)) } const httpRequests = useMemo(() => { if (!traceData) return [] const httpRequests = processHTTPRequests(traceData.traceEvents) const sortedRequests = sortRequests(httpRequests) const processedRequests = addRequestPostProcessing(sortedRequests, analyzeQueueReason, analyzeCDN) const requestsWithConnections = assignConnectionNumbers(processedRequests) return requestsWithConnections }, [traceData]) // Extract and process screenshots with SSIM analysis const [screenshots, setScreenshots] = useState([]) const [screenshotsLoading, setScreenshotsLoading] = useState(false) useEffect(() => { if (!traceData) { setScreenshots([]) return } const processScreenshots = async () => { setScreenshotsLoading(true) try { const allScreenshots = extractScreenshots(traceData.traceEvents) console.log('Debug: Found screenshots:', allScreenshots.length) console.log('Debug: Screenshot events sample:', allScreenshots.slice(0, 3)) const uniqueScreenshots = await findUniqueScreenshots(allScreenshots, ssimThreshold) console.log('Debug: Unique screenshots after SSIM analysis:', uniqueScreenshots.length) setScreenshots(uniqueScreenshots) } catch (error) { console.error('Error processing screenshots with SSIM:', error) // Fallback to extracting all screenshots without SSIM filtering const allScreenshots = extractScreenshots(traceData.traceEvents) setScreenshots(allScreenshots) } finally { setScreenshotsLoading(false) } } processScreenshots() }, [traceData, ssimThreshold]) const filteredRequests = useMemo(() => { let requests = httpRequests // Filter by resource type if (resourceTypeFilter !== 'all') { requests = requests.filter(req => req.resourceType === resourceTypeFilter) } // Filter by protocol if (protocolFilter !== 'all') { requests = requests.filter(req => req.protocol === protocolFilter) } // Filter by hostname if (hostnameFilter !== 'all') { requests = requests.filter(req => req.hostname === hostnameFilter) } // Filter by priority if (priorityFilter !== 'all') { requests = requests.filter(req => req.priority === priorityFilter) } // Filter by search term if (searchTerm) { const term = searchTerm.toLowerCase() requests = requests.filter(req => req.url.toLowerCase().includes(term) || req.method.toLowerCase().includes(term) || req.resourceType.toLowerCase().includes(term) || req.hostname.toLowerCase().includes(term) || req.statusCode?.toString().includes(term) ) } return requests }, [httpRequests, resourceTypeFilter, protocolFilter, hostnameFilter, priorityFilter, searchTerm]) const paginatedRequests = useMemo(() => { const startIndex = (currentPage - 1) * ITEMS_PER_PAGE return filteredRequests.slice(startIndex, startIndex + ITEMS_PER_PAGE) }, [filteredRequests, currentPage]) // Create timeline entries that include both HTTP requests and screenshot changes const timelineEntries = useMemo(() => { const entries: Array<{ type: 'request' | 'screenshot', timestamp: number, data: HTTPRequest | ScreenshotEvent }> = [] // Add HTTP requests filteredRequests.forEach(request => { entries.push({ type: 'request', timestamp: request.timing.start, data: request }) }) // Add screenshots if enabled if (showScreenshots && screenshots.length > 0) { screenshots.forEach(screenshot => { entries.push({ type: 'screenshot', timestamp: screenshot.timestamp, data: screenshot }) }) } // Sort by timestamp return entries.sort((a, b) => a.timestamp - b.timestamp) }, [filteredRequests, screenshots, showScreenshots]) const paginatedTimelineEntries = useMemo(() => { const startIndex = (currentPage - 1) * ITEMS_PER_PAGE return timelineEntries.slice(startIndex, startIndex + ITEMS_PER_PAGE) }, [timelineEntries, currentPage]) const resourceTypes = useMemo(() => { const types = new Set(httpRequests.map(req => req.resourceType)) return Array.from(types).sort() }, [httpRequests]) const protocols = useMemo(() => { const protos = new Set(httpRequests.map(req => req.protocol).filter((p): p is string => Boolean(p))) return Array.from(protos).sort() }, [httpRequests]) const hostnames = useMemo(() => { const hosts = new Set(httpRequests.map(req => req.hostname).filter(h => h !== 'unknown')) return Array.from(hosts).sort() }, [httpRequests]) const priorities = useMemo(() => { const prios = new Set(httpRequests.map(req => req.priority).filter(Boolean)) return Array.from(prios).sort((a, b) => { // Sort priorities by importance: VeryHigh, High, Medium, Low, VeryLow const order = ['VeryHigh', 'High', 'Medium', 'Low', 'VeryLow'] return order.indexOf(a) - order.indexOf(b) }) }, [httpRequests]) const totalPages = Math.ceil((showScreenshots ? timelineEntries.length : filteredRequests.length) / ITEMS_PER_PAGE) const toggleRowExpansion = (requestId: string) => { const newExpanded = new Set(expandedRows) if (newExpanded.has(requestId)) { newExpanded.delete(requestId) } else { newExpanded.add(requestId) } setExpandedRows(newExpanded) } const handleDownloadCSV = () => { const csvContent = exportRequestsToCSV(filteredRequests, { includeHeaders: true }) const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5) const filename = `http-requests-${timestamp}.csv` downloadCSV(csvContent, filename) } if (loading) { return ( ) } if (error) { return (

Error Loading Trace Data

{error}

) } return (

HTTP Requests & Responses

{/* Controls */} {/* Export Controls */}
{/* 3D Network Visualization Modal */} {show3DViewer && (

3D Network Visualization ({filteredRequests.length} requests)

}>
Legend:
⬛→⬜ Gray gradient: 2xx Success (darker=lower, lighter=higher)
🟡 Yellow: 3xx Redirects
🟠 Orange: 4xx Client Errors
🔴 Red: 5xx Server Errors
Layout:
🔵 Central sphere: Origin
🏷️ Hostname labels: At 12m radius
📦 Request boxes: Start → end timeline
📏 Front face: Request start time
📐 Height: 0.1m-5m (content-length)
📊 Depth: Request duration
📚 Overlapping requests stack vertically
🔗 Connection lines to center
👁️ Labels always face camera
)} {/* 3D Timeline Visualization Modal */} {showTimelineViewer && (

3D Timeline Visualization ({filteredRequests.length} requests)

}>
Legend:
⬛→⬜ Gray gradient: 2xx Success (darker=lower, lighter=higher)
🟡 Yellow: 3xx Redirects
🟠 Orange: 4xx Client Errors
🔴 Red: 5xx Server Errors
Timeline Layout:
🔵 Central sphere: Timeline origin
🏷️ Hostname labels: At 12m radius
📦 Request boxes: Chronological timeline
📏 Distance from center: Start time
📐 Height: 0.1m-5m (content-length)
📊 Depth: Request duration
📚 Overlapping requests stack vertically
🔗 Connection lines to center
👁️ Labels face origin (180° rotated)
)} {/* Column Settings */} setColumnSettingsOpen(!columnSettingsOpen)} onShowAll={handleShowAllColumns} onHideAll={handleHideAllColumns} onResetDefaults={handleResetDefaults} /> {/* Requests Table */}
) }