import { useState, useMemo, useEffect } from 'react' import { useDatabaseTraceData } from '../hooks/useDatabaseTraceData' import type { TraceEvent } from '../../types/trace' import ssim from 'ssim.js' import BabylonViewer from '../BabylonViewer' import BabylonTimelineViewer from '../BabylonTimelineViewer' interface QueueAnalysis { reason: 'connection_limit' | 'priority_queue' | 'resource_contention' | 'unknown' description: string concurrentRequests: number connectionId?: number relatedRequests?: string[] } interface CDNAnalysis { provider: 'cloudflare' | 'akamai' | 'aws_cloudfront' | 'fastly' | 'azure_cdn' | 'google_cdn' | 'cdn77' | 'keycdn' | 'unknown' isEdge: boolean cacheStatus?: 'hit' | 'miss' | 'expired' | 'bypass' | 'unknown' edgeLocation?: string confidence: number // 0-1 confidence score detectionMethod: string // How we detected it } interface ScreenshotEvent { timestamp: number screenshot: string // base64 image data index: number } interface HTTPRequest { requestId: string url: string hostname: string method: string resourceType: string priority: string statusCode?: number mimeType?: string protocol?: string events: { willSendRequest?: TraceEvent sendRequest?: TraceEvent receiveResponse?: TraceEvent receivedData?: TraceEvent[] finishLoading?: TraceEvent } timing: { start: number startOffset?: number end?: number duration?: number queueTime?: number serverLatency?: number networkDuration?: number dnsStart?: number dnsEnd?: number connectStart?: number connectEnd?: number sslStart?: number sslEnd?: number sendStart?: number sendEnd?: number receiveHeadersStart?: number receiveHeadersEnd?: number } responseHeaders?: Array<{ name: string, value: string }> encodedDataLength?: number contentLength?: number fromCache: boolean connectionReused: boolean queueAnalysis?: QueueAnalysis cdnAnalysis?: CDNAnalysis } const ITEMS_PER_PAGE = 25 // SSIM threshold for screenshot similarity (0-1, where 1 is identical) // Values above this threshold are considered "similar enough" to filter out const SSIM_SIMILARITY_THRESHOLD = 0.95 // Global highlighting constants const HIGHLIGHTING_CONFIG = { // File size thresholds (in KB) SIZE_THRESHOLDS: { LARGE: 500, // Red highlighting for files >= 500KB MEDIUM: 100, // Yellow highlighting for files >= 100KB SMALL: 50 // Green highlighting for files <= 50KB }, // Duration thresholds (in milliseconds) DURATION_THRESHOLDS: { SLOW: 250, // Red highlighting for requests > 150ms MEDIUM: 100, // Yellow highlighting for requests 50-150ms FAST: 100 // Green highlighting for requests < 50ms }, // Server latency thresholds (in microseconds) SERVER_LATENCY_THRESHOLDS: { SLOW: 200000, // Red highlighting for server latency > 200ms (200000 microseconds) MEDIUM: 50000, // Yellow highlighting for server latency 50-200ms (50000 microseconds) FAST: 50000 // Green highlighting for server latency < 50ms (50000 microseconds) }, // Queue time thresholds (in microseconds) QUEUE_TIME: { HIGH_THRESHOLD: 10000, // 10ms - highlight in red and show analysis ANALYSIS_THRESHOLD: 10000 // 10ms - minimum queue time for analysis }, // Colors COLORS: { // Status colors STATUS: { SUCCESS: '#28a745', // 2xx REDIRECT: '#ffc107', // 3xx CLIENT_ERROR: '#fd7e14', // 4xx SERVER_ERROR: '#dc3545', // 5xx UNKNOWN: '#6c757d' }, // Protocol colors PROTOCOL: { HTTP1_1: '#dc3545', // Red H2: '#b8860b', // Dark yellow H3: '#006400', // Dark green UNKNOWN: '#6c757d' }, // Priority colors PRIORITY: { VERY_HIGH: '#dc3545', // Red HIGH: '#fd7e14', // Orange MEDIUM: '#ffc107', // Yellow LOW: '#28a745', // Green VERY_LOW: '#6c757d', // Gray UNKNOWN: '#6c757d' }, // File size colors SIZE: { LARGE: { background: '#dc3545', color: 'white' }, // Red for 500KB+ MEDIUM: { background: '#ffc107', color: 'black' }, // Yellow for 100-500KB SMALL: { background: '#28a745', color: 'white' }, // Green for under 50KB DEFAULT: { background: 'transparent', color: '#495057' } // Default for 50-100KB }, // Duration colors DURATION: { SLOW: { background: '#dc3545', color: 'white' }, // Red for > 150ms MEDIUM: { background: '#ffc107', color: 'black' }, // Yellow for 50-150ms FAST: { background: '#28a745', color: 'white' }, // Green for < 50ms DEFAULT: { background: 'transparent', color: '#495057' } // Default }, // Server latency colors SERVER_LATENCY: { SLOW: { background: '#dc3545', color: 'white' }, // Red for > 200ms MEDIUM: { background: '#ffc107', color: 'black' }, // Yellow for 50-200ms FAST: { background: '#28a745', color: 'white' }, // Green for < 50ms DEFAULT: { background: 'transparent', color: '#495057' } // Default } } } const getHostnameFromUrl = (url: string): string => { try { const urlObj = new URL(url) return urlObj.hostname } catch { return 'unknown' } } // Helper function to convert base64 image to ImageData for SSIM analysis const base64ToImageData = (base64: string): Promise => { return new Promise((resolve, reject) => { const img = new Image() img.onload = () => { const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') if (!ctx) { reject(new Error('Could not get canvas context')) return } canvas.width = img.width canvas.height = img.height ctx.drawImage(img, 0, 0) const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) resolve(imageData) } img.onerror = () => reject(new Error('Failed to load image')) img.src = base64.startsWith('data:') ? base64 : `data:image/png;base64,${base64}` }) } const extractScreenshots = (traceEvents: TraceEvent[]): ScreenshotEvent[] => { const screenshots: ScreenshotEvent[] = [] let index = 0 // Debug: Check for any screenshot-related events const screenshotRelated = traceEvents.filter(event => event.name.toLowerCase().includes('screenshot') || event.cat.toLowerCase().includes('screenshot') || event.name.toLowerCase().includes('snap') || event.name.toLowerCase().includes('frame') || event.cat.toLowerCase().includes('devtools') ) console.log('Debug: Screenshot-related events found:', screenshotRelated.length) if (screenshotRelated.length > 0) { console.log('Debug: Screenshot-related sample:', screenshotRelated.slice(0, 5).map(e => ({ name: e.name, cat: e.cat, hasArgs: !!e.args, argsKeys: e.args ? Object.keys(e.args) : [] }))) } for (const event of traceEvents) { // Look for screenshot events with more flexible matching if ((event.name === 'Screenshot' || event.name === 'screenshot') && (event.cat === 'disabled-by-default-devtools.screenshot' || event.cat.includes('screenshot') || event.cat.includes('devtools')) && event.args) { const args = event.args as any // Check multiple possible locations for screenshot data const snapshot = args.snapshot || args.data?.snapshot || args.image || args.data?.image if (snapshot && typeof snapshot === 'string') { // Accept both data URLs and base64 strings if (snapshot.startsWith('data:image/') || snapshot.startsWith('/9j/') || snapshot.length > 1000) { const screenshotData = snapshot.startsWith('data:image/') ? snapshot : `data:image/jpeg;base64,${snapshot}` screenshots.push({ timestamp: event.ts, screenshot: screenshotData, index: index++ }) } } } } console.log('Debug: Extracted screenshots:', screenshots.length) return screenshots.sort((a, b) => a.timestamp - b.timestamp) } const findUniqueScreenshots = async (screenshots: ScreenshotEvent[], threshold: number = SSIM_SIMILARITY_THRESHOLD): Promise => { if (screenshots.length === 0) return [] const uniqueScreenshots: ScreenshotEvent[] = [screenshots[0]] // Always include first screenshot console.log('SSIM Analysis: Processing', screenshots.length, 'screenshots') // Compare each screenshot with the previous unique one using SSIM for (let i = 1; i < screenshots.length; i++) { const current = screenshots[i] const lastUnique = uniqueScreenshots[uniqueScreenshots.length - 1] try { // Convert both images to ImageData for SSIM analysis const [currentImageData, lastImageData] = await Promise.all([ base64ToImageData(current.screenshot), base64ToImageData(lastUnique.screenshot) ]) // Calculate SSIM similarity (0-1, where 1 is identical) const similarity = ssim(currentImageData, lastImageData) console.log(`SSIM Analysis: Screenshot ${i} similarity: ${similarity.toFixed(4)}`) // If similarity is below threshold, it's different enough to keep if (similarity < threshold) { uniqueScreenshots.push(current) console.log(`SSIM Analysis: Screenshot ${i} added (significant change detected)`) } else { console.log(`SSIM Analysis: Screenshot ${i} filtered out (too similar: ${similarity.toFixed(4)})`) } } catch (error) { console.warn('SSIM Analysis: Error comparing screenshots, falling back to string comparison:', error) // Fallback to simple string comparison if SSIM fails if (current.screenshot !== lastUnique.screenshot) { uniqueScreenshots.push(current) } } } console.log('SSIM Analysis: Filtered from', screenshots.length, 'to', uniqueScreenshots.length, 'unique screenshots') return uniqueScreenshots } const analyzeCDN = (request: HTTPRequest): CDNAnalysis => { const hostname = request.hostname const headers = request.responseHeaders || [] const headerMap = new Map(headers.map(h => [h.name.toLowerCase(), h.value.toLowerCase()])) let provider: CDNAnalysis['provider'] = 'unknown' let isEdge = false let cacheStatus: CDNAnalysis['cacheStatus'] = 'unknown' let edgeLocation: string | undefined let confidence = 0 let detectionMethod = 'unknown' // Enhanced Akamai detection (check this first as it's often missed) if (headerMap.has('akamai-cache-status') || headerMap.has('x-cache-key') || headerMap.has('x-akamai-request-id') || headerMap.has('x-akamai-edgescape') || headerMap.get('server')?.includes('akamai') || headerMap.get('server')?.includes('akamainet') || headerMap.get('server')?.includes('akamainetworking') || headerMap.get('x-cache')?.includes('akamai') || hostname.includes('akamai') || hostname.includes('akamaized') || hostname.includes('akamaicdn') || hostname.includes('akamai-staging') || // Check for Akamai Edge hostnames patterns /e\d+\.a\.akamaiedge\.net/.test(hostname) || // Check Via header for Akamai headerMap.get('via')?.includes('akamai') || // Check for Akamai Image Manager server header (headerMap.get('server') === 'akamai image manager') || // Check for typical Akamai headers headerMap.has('x-serial') || // Additional Akamai detection patterns headerMap.has('x-akamai-session-info') || headerMap.has('x-akamai-ssl-client-sid') || headerMap.get('x-cache')?.includes('tcp_hit') || headerMap.get('x-cache')?.includes('tcp_miss') || // Akamai sometimes uses x-served-by with specific patterns (headerMap.has('x-served-by') && headerMap.get('x-served-by')?.includes('cache-'))) { provider = 'akamai' confidence = 0.95 detectionMethod = 'enhanced akamai detection (headers/hostname/server)' isEdge = true const akamaiStatus = headerMap.get('akamai-cache-status') if (akamaiStatus) { if (akamaiStatus.includes('hit')) cacheStatus = 'hit' else if (akamaiStatus.includes('miss')) cacheStatus = 'miss' } // Also check x-cache for Akamai cache status const xCache = headerMap.get('x-cache') if (xCache && !akamaiStatus) { if (xCache.includes('hit')) cacheStatus = 'hit' else if (xCache.includes('miss')) cacheStatus = 'miss' else if (xCache.includes('tcp_hit')) cacheStatus = 'hit' else if (xCache.includes('tcp_miss')) cacheStatus = 'miss' } } // Cloudflare detection else if (headerMap.has('cf-ray') || headerMap.has('cf-cache-status') || hostname.includes('cloudflare')) { provider = 'cloudflare' confidence = 0.95 detectionMethod = 'headers (cf-ray/cf-cache-status)' isEdge = true const cfCacheStatus = headerMap.get('cf-cache-status') if (cfCacheStatus) { if (cfCacheStatus.includes('hit')) cacheStatus = 'hit' else if (cfCacheStatus.includes('miss')) cacheStatus = 'miss' else if (cfCacheStatus.includes('expired')) cacheStatus = 'expired' else if (cfCacheStatus.includes('bypass')) cacheStatus = 'bypass' } edgeLocation = headerMap.get('cf-ray')?.split('-')[1] } // AWS CloudFront detection else if (headerMap.has('x-amz-cf-id') || headerMap.has('x-amz-cf-pop') || hostname.includes('cloudfront.net')) { provider = 'aws_cloudfront' confidence = 0.95 detectionMethod = 'headers (x-amz-cf-id/x-amz-cf-pop)' isEdge = true const cloudfrontCache = headerMap.get('x-cache') if (cloudfrontCache) { if (cloudfrontCache.includes('hit')) cacheStatus = 'hit' else if (cloudfrontCache.includes('miss')) cacheStatus = 'miss' } edgeLocation = headerMap.get('x-amz-cf-pop') } // Fastly detection (moved after Akamai to avoid false positives) else if (headerMap.has('fastly-debug-digest') || headerMap.has('fastly-debug-path') || hostname.includes('fastly.com') || hostname.includes('fastlylb.net') || headerMap.get('via')?.includes('fastly') || headerMap.get('x-cache')?.includes('fastly') || // Only use x-served-by for Fastly if it has specific Fastly patterns (headerMap.has('x-served-by') && ( headerMap.get('x-served-by')?.includes('fastly') || headerMap.get('x-served-by')?.includes('f1') || headerMap.get('x-served-by')?.includes('f2') ))) { provider = 'fastly' confidence = 0.85 detectionMethod = 'headers (fastly-debug-digest/via/x-cache)' isEdge = true const fastlyCache = headerMap.get('x-cache') if (fastlyCache) { if (fastlyCache.includes('hit')) cacheStatus = 'hit' else if (fastlyCache.includes('miss')) cacheStatus = 'miss' } } // Azure CDN detection else if (headerMap.has('x-azure-ref') || hostname.includes('azureedge.net')) { provider = 'azure_cdn' confidence = 0.9 detectionMethod = 'headers (x-azure-ref) or hostname' isEdge = true } // Google CDN detection else if (headerMap.has('x-goog-generation') || hostname.includes('googleapis.com') || headerMap.get('server')?.includes('gws')) { provider = 'google_cdn' confidence = 0.8 detectionMethod = 'headers (x-goog-generation) or server' isEdge = true } // Generic CDN patterns else if (hostname.includes('cdn') || hostname.includes('cache') || hostname.includes('edge') || hostname.includes('static')) { provider = 'unknown' confidence = 0.3 detectionMethod = 'hostname patterns (cdn/cache/edge/static)' isEdge = true } // Check for origin indicators (lower confidence for edge) if (hostname.includes('origin') || hostname.includes('api') || headerMap.get('x-cache')?.includes('miss from origin')) { isEdge = false confidence = Math.max(0.1, confidence - 0.2) detectionMethod += ' + origin indicators' } return { provider, isEdge, cacheStatus, edgeLocation, confidence, detectionMethod } } const analyzeQueueReason = (request: HTTPRequest, allRequests: HTTPRequest[]): QueueAnalysis => { const queueTime = request.timing.queueTime || 0 // If no significant queue time, no analysis needed if (queueTime < HIGHLIGHTING_CONFIG.QUEUE_TIME.ANALYSIS_THRESHOLD) { return { reason: 'unknown', description: 'No significant queueing detected', concurrentRequests: 0 } } const requestStart = request.timing.start const requestEnd = request.timing.end || requestStart + (request.timing.duration || 0) // Find concurrent requests to the same host const concurrentToSameHost = allRequests.filter(other => { if (other.requestId === request.requestId) return false if (other.hostname !== request.hostname) return false const otherStart = other.timing.start const otherEnd = other.timing.end || otherStart + (other.timing.duration || 0) // Check if requests overlap in time return (otherStart <= requestEnd && otherEnd >= requestStart) }) const concurrentSameProtocol = concurrentToSameHost.filter(r => r.protocol === request.protocol) // HTTP/1.1 connection limit analysis (typically 6 connections per host) if (request.protocol === 'http/1.1' && concurrentSameProtocol.length >= 6) { return { reason: 'connection_limit', description: `HTTP/1.1 connection limit reached (${concurrentSameProtocol.length} concurrent requests)`, concurrentRequests: concurrentSameProtocol.length, relatedRequests: concurrentSameProtocol.slice(0, 5).map(r => r.requestId) } } // H2 multiplexing but still queued - likely priority or server limits if (request.protocol === 'h2' && concurrentSameProtocol.length > 0) { // Check if lower priority requests are being processed first const lowerPriorityActive = concurrentSameProtocol.filter(r => { const priorityOrder = ['VeryLow', 'Low', 'Medium', 'High', 'VeryHigh'] const requestPriorityIndex = priorityOrder.indexOf(request.priority || 'Medium') const otherPriorityIndex = priorityOrder.indexOf(r.priority || 'Medium') return otherPriorityIndex < requestPriorityIndex }) if (lowerPriorityActive.length > 0) { return { reason: 'priority_queue', description: `Queued behind ${lowerPriorityActive.length} lower priority requests`, concurrentRequests: concurrentSameProtocol.length, relatedRequests: lowerPriorityActive.slice(0, 3).map(r => r.requestId) } } return { reason: 'resource_contention', description: `Server resource contention (${concurrentSameProtocol.length} concurrent H2 requests)`, concurrentRequests: concurrentSameProtocol.length, relatedRequests: concurrentSameProtocol.slice(0, 3).map(r => r.requestId) } } // General resource contention if (concurrentToSameHost.length > 0) { return { reason: 'resource_contention', description: `Resource contention with ${concurrentToSameHost.length} concurrent requests`, concurrentRequests: concurrentToSameHost.length, relatedRequests: concurrentToSameHost.slice(0, 3).map(r => r.requestId) } } return { reason: 'unknown', description: `Queued for ${(queueTime / 1000).toFixed(1)}ms - reason unclear`, concurrentRequests: 0 } } 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) const [show3DViewer, setShow3DViewer] = useState(false) const [showTimelineViewer, setShowTimelineViewer] = useState(false) const [ssimThreshold, setSsimThreshold] = useState(SSIM_SIMILARITY_THRESHOLD) const [expandedRows, setExpandedRows] = useState>(new Set()) const httpRequests = useMemo(() => { if (!traceData) return [] const requestsMap = new Map() // Process all events and group by requestId for (const event of traceData.traceEvents) { const args = event.args as any const requestId = args?.data?.requestId if (!requestId) continue if (!requestsMap.has(requestId)) { requestsMap.set(requestId, { requestId, url: '', hostname: '', method: '', resourceType: '', priority: '', events: {}, timing: { start: event.ts }, fromCache: false, connectionReused: false }) } const request = requestsMap.get(requestId)! switch (event.name) { case 'ResourceWillSendRequest': request.events.willSendRequest = event request.timing.start = Math.min(request.timing.start, event.ts) break case 'ResourceSendRequest': request.events.sendRequest = event request.url = args.data.url || '' request.hostname = getHostnameFromUrl(request.url) request.method = args.data.requestMethod || '' request.resourceType = args.data.resourceType || '' request.priority = args.data.priority || '' request.timing.start = Math.min(request.timing.start, event.ts) break case 'ResourceReceiveResponse': request.events.receiveResponse = event request.statusCode = args.data.statusCode request.mimeType = args.data.mimeType request.protocol = args.data.protocol request.responseHeaders = args.data.headers // Extract content-length from response headers if (request.responseHeaders) { const contentLengthHeader = request.responseHeaders.find( header => header.name.toLowerCase() === 'content-length' ) if (contentLengthHeader) { request.contentLength = parseInt(contentLengthHeader.value, 10) } } request.fromCache = args.data.fromCache || false request.connectionReused = args.data.connectionReused || false request.timing.end = event.ts // Extract network timing details const timing = args.data.timing if (timing) { request.timing.networkDuration = timing.receiveHeadersEnd request.timing.dnsStart = timing.dnsStart request.timing.dnsEnd = timing.dnsEnd request.timing.connectStart = timing.connectStart request.timing.connectEnd = timing.connectEnd request.timing.sslStart = timing.sslStart request.timing.sslEnd = timing.sslEnd request.timing.sendStart = timing.sendStart request.timing.sendEnd = timing.sendEnd request.timing.receiveHeadersStart = timing.receiveHeadersStart request.timing.receiveHeadersEnd = timing.receiveHeadersEnd } break case 'ResourceReceivedData': if (!request.events.receivedData) request.events.receivedData = [] request.events.receivedData.push(event) request.encodedDataLength = (request.encodedDataLength || 0) + (args.data.encodedDataLength || 0) request.timing.end = Math.max(request.timing.end || event.ts, event.ts) break case 'ResourceFinishLoading': request.events.finishLoading = event request.timing.end = event.ts break } // Calculate total duration if (request.timing.end) { request.timing.duration = request.timing.end - request.timing.start } } // Calculate queue times and filter valid requests const sortedRequests = Array.from(requestsMap.values()) .filter(req => req.url && req.method) // Only include valid requests .map(request => { // Calculate queue time using multiple approaches if (request.events.willSendRequest && request.events.sendRequest) { // Primary approach: ResourceSendRequest - ResourceWillSendRequest request.timing.queueTime = request.events.sendRequest.ts - request.events.willSendRequest.ts } else if (request.events.sendRequest && request.events.receiveResponse) { // Fallback approach: use timing data from ResourceReceiveResponse const responseArgs = request.events.receiveResponse.args as any const timingData = responseArgs?.data?.timing if (timingData?.requestTime) { // requestTime is in seconds, convert to microseconds const requestTimeUs = timingData.requestTime * 1000000 // Queue time = requestTime (when browser queued) - ResourceSendRequest timestamp (when DevTools recorded) // Note: requestTime is the actual browser queue time, ResourceSendRequest is the DevTools event request.timing.queueTime = requestTimeUs - request.events.sendRequest.ts } } // Calculate server latency (server processing time = total duration - queue time) if (request.timing.duration && request.timing.queueTime) { request.timing.serverLatency = request.timing.duration - request.timing.queueTime // Ensure server latency is not negative (can happen with timing irregularities) if (request.timing.serverLatency < 0) { request.timing.serverLatency = 0 } } return request }) .sort((a, b) => a.timing.start - b.timing.start) // Calculate start time offsets from the first request if (sortedRequests.length > 0) { const firstRequestTime = sortedRequests[0].timing.start sortedRequests.forEach(request => { request.timing.startOffset = request.timing.start - firstRequestTime }) } // Add queue analysis for requests with significant queue time sortedRequests.forEach(request => { if ((request.timing.queueTime || 0) >= HIGHLIGHTING_CONFIG.QUEUE_TIME.ANALYSIS_THRESHOLD) { request.queueAnalysis = analyzeQueueReason(request, sortedRequests) } }) // Add CDN analysis for all requests sortedRequests.forEach(request => { if (request.responseHeaders && request.responseHeaders.length > 0) { request.cdnAnalysis = analyzeCDN(request) } }) return sortedRequests }, [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(Boolean)) 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 formatDuration = (microseconds?: number) => { if (!microseconds) return '-' if (microseconds < 1000) return `${microseconds.toFixed(0)}Ξs` return `${(microseconds / 1000).toFixed(2)}ms` } const formatSize = (bytes?: number) => { if (!bytes) return '-' if (bytes < 1024) return `${bytes}B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB` return `${(bytes / (1024 * 1024)).toFixed(1)}MB` } const getStatusColor = (status?: number) => { if (!status) return HIGHLIGHTING_CONFIG.COLORS.STATUS.UNKNOWN if (status < 300) return HIGHLIGHTING_CONFIG.COLORS.STATUS.SUCCESS if (status < 400) return HIGHLIGHTING_CONFIG.COLORS.STATUS.REDIRECT if (status < 500) return HIGHLIGHTING_CONFIG.COLORS.STATUS.CLIENT_ERROR return HIGHLIGHTING_CONFIG.COLORS.STATUS.SERVER_ERROR } const getProtocolColor = (protocol?: string) => { if (!protocol) return HIGHLIGHTING_CONFIG.COLORS.PROTOCOL.UNKNOWN if (protocol === 'http/1.1') return HIGHLIGHTING_CONFIG.COLORS.PROTOCOL.HTTP1_1 if (protocol === 'h2') return HIGHLIGHTING_CONFIG.COLORS.PROTOCOL.H2 if (protocol === 'h3') return HIGHLIGHTING_CONFIG.COLORS.PROTOCOL.H3 return HIGHLIGHTING_CONFIG.COLORS.PROTOCOL.UNKNOWN } const getPriorityColor = (priority?: string) => { if (!priority) return HIGHLIGHTING_CONFIG.COLORS.PRIORITY.UNKNOWN if (priority === 'VeryHigh') return HIGHLIGHTING_CONFIG.COLORS.PRIORITY.VERY_HIGH if (priority === 'High') return HIGHLIGHTING_CONFIG.COLORS.PRIORITY.HIGH if (priority === 'Medium') return HIGHLIGHTING_CONFIG.COLORS.PRIORITY.MEDIUM if (priority === 'Low') return HIGHLIGHTING_CONFIG.COLORS.PRIORITY.LOW if (priority === 'VeryLow') return HIGHLIGHTING_CONFIG.COLORS.PRIORITY.VERY_LOW return HIGHLIGHTING_CONFIG.COLORS.PRIORITY.UNKNOWN } const getQueueAnalysisIcon = (analysis?: QueueAnalysis) => { if (!analysis) return '' switch (analysis.reason) { case 'connection_limit': return 'ðŸšŦ' case 'priority_queue': return 'âģ' case 'resource_contention': return '⚠ïļ' default: return '❓' } } const getSizeColor = (bytes?: number) => { if (!bytes) return HIGHLIGHTING_CONFIG.COLORS.SIZE.DEFAULT const sizeKB = bytes / 1024 if (sizeKB >= HIGHLIGHTING_CONFIG.SIZE_THRESHOLDS.LARGE) { return HIGHLIGHTING_CONFIG.COLORS.SIZE.LARGE } else if (sizeKB >= HIGHLIGHTING_CONFIG.SIZE_THRESHOLDS.MEDIUM) { return HIGHLIGHTING_CONFIG.COLORS.SIZE.MEDIUM } else if (sizeKB <= HIGHLIGHTING_CONFIG.SIZE_THRESHOLDS.SMALL) { return HIGHLIGHTING_CONFIG.COLORS.SIZE.SMALL } return HIGHLIGHTING_CONFIG.COLORS.SIZE.DEFAULT } const getDurationColor = (microseconds?: number) => { if (!microseconds) return HIGHLIGHTING_CONFIG.COLORS.DURATION.DEFAULT const durationMs = microseconds / 1000 if (durationMs > HIGHLIGHTING_CONFIG.DURATION_THRESHOLDS.SLOW) { return HIGHLIGHTING_CONFIG.COLORS.DURATION.SLOW } else if (durationMs >= HIGHLIGHTING_CONFIG.DURATION_THRESHOLDS.MEDIUM) { return HIGHLIGHTING_CONFIG.COLORS.DURATION.MEDIUM } else if (durationMs < HIGHLIGHTING_CONFIG.DURATION_THRESHOLDS.FAST) { return HIGHLIGHTING_CONFIG.COLORS.DURATION.FAST } return HIGHLIGHTING_CONFIG.COLORS.DURATION.DEFAULT } const getServerLatencyColor = (microseconds?: number) => { if (!microseconds) return HIGHLIGHTING_CONFIG.COLORS.SERVER_LATENCY.DEFAULT if (microseconds > HIGHLIGHTING_CONFIG.SERVER_LATENCY_THRESHOLDS.SLOW) { return HIGHLIGHTING_CONFIG.COLORS.SERVER_LATENCY.SLOW } else if (microseconds >= HIGHLIGHTING_CONFIG.SERVER_LATENCY_THRESHOLDS.MEDIUM) { return HIGHLIGHTING_CONFIG.COLORS.SERVER_LATENCY.MEDIUM } else if (microseconds < HIGHLIGHTING_CONFIG.SERVER_LATENCY_THRESHOLDS.FAST) { return HIGHLIGHTING_CONFIG.COLORS.SERVER_LATENCY.FAST } return HIGHLIGHTING_CONFIG.COLORS.SERVER_LATENCY.DEFAULT } const getCDNIcon = (analysis?: CDNAnalysis) => { if (!analysis) return '' const providerIcons = { cloudflare: '🟠', akamai: 'ðŸ”ĩ', aws_cloudfront: 'ðŸŸĄ', fastly: '⚡', azure_cdn: 'ðŸŸĶ', google_cdn: 'ðŸŸĒ', cdn77: 'ðŸŸĢ', keycdn: 'ðŸ”ķ', unknown: 'ðŸ“Ą' } const edgeIcon = analysis.isEdge ? '🌐' : '🏠' const cacheIcon = analysis.cacheStatus === 'hit' ? '✅' : analysis.cacheStatus === 'miss' ? '❌' : analysis.cacheStatus === 'expired' ? '⏰' : '' return `${providerIcons[analysis.provider]}${edgeIcon}${cacheIcon}` } const getCDNDisplayName = (provider: CDNAnalysis['provider']) => { const names = { cloudflare: 'Cloudflare', akamai: 'Akamai', aws_cloudfront: 'CloudFront', fastly: 'Fastly', azure_cdn: 'Azure CDN', google_cdn: 'Google CDN', cdn77: 'CDN77', keycdn: 'KeyCDN', unknown: 'Unknown CDN' } return names[provider] } const toggleRowExpansion = (requestId: string) => { const newExpanded = new Set(expandedRows) if (newExpanded.has(requestId)) { newExpanded.delete(requestId) } else { newExpanded.add(requestId) } setExpandedRows(newExpanded) } const truncateUrl = (url: string, maxLength: number = 60) => { if (url.length <= maxLength) return url return url.substring(0, maxLength) + '...' } if (loading) { return (
Loading HTTP requests...
) } if (error) { return (

Error Loading Trace Data

{error}

) } return (

HTTP Requests & Responses

{/* Debug info - remove after investigation */} {traceData && (
Debug: Total events: {traceData.traceEvents.length.toLocaleString()}, Screenshots found: {screenshots.length}, HTTP requests: {httpRequests.length}
Categories sample: {Array.from(new Set(traceData.traceEvents.slice(0, 1000).map(e => e.cat))).slice(0, 10).join(', ')}
Event names sample: {Array.from(new Set(traceData.traceEvents.slice(0, 1000).map(e => e.name))).slice(0, 10).join(', ')}
)} {/* Controls */}
{/* Resource Type Filter */}
{/* Protocol Filter */}
{/* Hostname Filter */}
{/* Priority Filter */}
{/* Search */}
{ setSearchTerm(e.target.value) setCurrentPage(1) }} style={{ padding: '8px 12px', border: '1px solid #ced4da', borderRadius: '4px', fontSize: '14px', minWidth: '300px' }} />
{/* Queue Analysis Toggle */}
{/* 3D Viewer Toggle */} {httpRequests.length > 0 && (
)} {/* Screenshots Toggle */} {(screenshots.length > 0 || screenshotsLoading) && (
{screenshots.length > 0 && !screenshotsLoading && (
Lower values = more screenshots kept (more sensitive to changes)
)}
)} {/* Results count */}
{showScreenshots ? `${timelineEntries.length.toLocaleString()} timeline entries` : `${filteredRequests.length.toLocaleString()} requests found`}
{/* Pagination Controls */} {totalPages > 1 && (
Page {currentPage} of {totalPages}
)} {/* 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)
)} {/* Requests Table */}
{(showScreenshots ? paginatedTimelineEntries : paginatedRequests.map(req => ({ type: 'request' as const, timestamp: req.timing.start, data: req }))).map((entry, entryIndex) => { if (entry.type === 'screenshot') { const screenshot = entry.data as ScreenshotEvent const firstRequestTime = httpRequests.length > 0 ? httpRequests[0].timing.start : 0 const timeOffset = screenshot.timestamp - firstRequestTime return ( ) } const request = entry.data as HTTPRequest const isExpanded = expandedRows.has(request.requestId) return ( <> toggleRowExpansion(request.requestId)} > {/* Expanded Row Details */} {isExpanded && ( )} ) })}
Expand Method Status Type Priority Start Time Queue Time Server Latency URL Duration Size Content-Length Protocol CDN Cache
ðŸ“ļ Screenshot
Time: {formatDuration(timeOffset)}
{`Screenshot { // Open screenshot in new window for full size viewing const newWindow = window.open('', '_blank') if (newWindow) { newWindow.document.write(` Screenshot at ${formatDuration(timeOffset)} Full size screenshot `) } }} />
Click to view full size
{isExpanded ? '−' : '+'} {request.method} {request.statusCode || '-'} {request.resourceType} {request.priority || '-'} {formatDuration(request.timing.startOffset)} HIGHLIGHTING_CONFIG.QUEUE_TIME.HIGH_THRESHOLD ? HIGHLIGHTING_CONFIG.COLORS.STATUS.SERVER_ERROR : '#495057', fontWeight: request.timing.queueTime && request.timing.queueTime > HIGHLIGHTING_CONFIG.QUEUE_TIME.HIGH_THRESHOLD ? 'bold' : 'normal' }}>
{formatDuration(request.timing.queueTime)} {showQueueAnalysis && request.queueAnalysis && ( {getQueueAnalysisIcon(request.queueAnalysis)} )}
{formatDuration(request.timing.serverLatency)} {truncateUrl(request.url)} {formatDuration(request.timing.duration)} {formatSize(request.encodedDataLength)} {request.contentLength ? formatSize(request.contentLength) : '-'} {request.protocol || '-'} {request.cdnAnalysis ? getCDNIcon(request.cdnAnalysis) : '-'} {request.fromCache ? 'ðŸ’ū' : request.connectionReused ? '🔄' : '🌐'}
{/* Request Details */}

Request Details

Request ID: {request.requestId}
Method: {request.method}
Priority: {request.priority}
MIME Type: {request.mimeType || '-'}
Content-Length: {request.contentLength ? formatSize(request.contentLength) : '-'}
From Cache: {request.fromCache ? 'Yes' : 'No'}
Connection Reused: {request.connectionReused ? 'Yes' : 'No'}
{/* Network Timing */}

Network Timing

Start Time: {formatDuration(request.timing.startOffset)}
Queue Time: {formatDuration(request.timing.queueTime)} {request.queueAnalysis && ( {getQueueAnalysisIcon(request.queueAnalysis)} )}
Server Latency: {formatDuration(request.timing.serverLatency)}
Total Duration: {formatDuration(request.timing.duration)}
Network Duration: {formatDuration(request.timing.networkDuration)}
{request.timing.dnsStart !== -1 && request.timing.dnsEnd !== -1 && (
DNS: {formatDuration((request.timing.dnsEnd || 0) - (request.timing.dnsStart || 0))}
)} {request.timing.connectStart !== -1 && request.timing.connectEnd !== -1 && (
Connect: {formatDuration((request.timing.connectEnd || 0) - (request.timing.connectStart || 0))}
)} {request.timing.sslStart !== -1 && request.timing.sslEnd !== -1 && (
SSL: {formatDuration((request.timing.sslEnd || 0) - (request.timing.sslStart || 0))}
)} {request.timing.sendStart !== -1 && request.timing.sendEnd !== -1 && (
Send: {formatDuration((request.timing.sendEnd || 0) - (request.timing.sendStart || 0))}
)} {request.timing.receiveHeadersStart !== -1 && request.timing.receiveHeadersEnd !== -1 && (
Receive Headers: {formatDuration((request.timing.receiveHeadersEnd || 0) - (request.timing.receiveHeadersStart || 0))}
)}
{/* Queue Analysis */} {request.queueAnalysis && request.queueAnalysis.reason !== 'unknown' && (

Queue Analysis {getQueueAnalysisIcon(request.queueAnalysis)}

Reason: {request.queueAnalysis.description}
Concurrent Requests: {request.queueAnalysis.concurrentRequests}
{request.queueAnalysis.relatedRequests && request.queueAnalysis.relatedRequests.length > 0 && (
Related Request IDs:{' '} {request.queueAnalysis.relatedRequests.join(', ')} {request.queueAnalysis.concurrentRequests > request.queueAnalysis.relatedRequests.length && ` (+${request.queueAnalysis.concurrentRequests - request.queueAnalysis.relatedRequests.length} more)`}
)}
)} {/* CDN Analysis */} {request.cdnAnalysis && request.cdnAnalysis.provider !== 'unknown' && (

CDN Analysis {getCDNIcon(request.cdnAnalysis)}

Provider: {getCDNDisplayName(request.cdnAnalysis.provider)}
Source: {request.cdnAnalysis.isEdge ? 'Edge Server' : 'Origin Server'}
{request.cdnAnalysis.cacheStatus && request.cdnAnalysis.cacheStatus !== 'unknown' && (
Cache Status: {request.cdnAnalysis.cacheStatus.toUpperCase()}
)} {request.cdnAnalysis.edgeLocation && (
Edge Location: {request.cdnAnalysis.edgeLocation}
)}
Confidence: {(request.cdnAnalysis.confidence * 100).toFixed(0)}%
Detection Method: {request.cdnAnalysis.detectionMethod}
{/* Debug info for canadiantire.ca requests */} {request.hostname.includes('canadiantire.ca') && (
Debug - CDN Detection Analysis:
Current Detection: {request.cdnAnalysis?.provider} (confidence: {((request.cdnAnalysis?.confidence || 0) * 100).toFixed(0)}%)
All CDN-Related Headers:
{request.responseHeaders && request.responseHeaders.map((header, idx) => { const headerName = header.name.toLowerCase() // Show all potentially CDN-related headers if (headerName.includes('akamai') || headerName.includes('fastly') || headerName.includes('server') || headerName.includes('via') || headerName.includes('x-cache') || headerName.includes('x-serial') || headerName.includes('x-served-by') || headerName.includes('cf-') || headerName.includes('x-amz-cf') || headerName.includes('azure') || headerName.includes('x-goog') || headerName.includes('cdn') || headerName.includes('edge') || headerName.includes('cache')) { const isAkamaiIndicator = headerName.includes('akamai') || headerName.includes('x-serial') || (headerName === 'x-cache' && header.value.includes('tcp_')) || (headerName === 'x-served-by' && header.value.includes('cache-')) return (
{header.name}: {header.value} {isAkamaiIndicator && ( ← AKAMAI )}
) } return null })}
)}
)} {/* Response Headers */} {request.responseHeaders && request.responseHeaders.length > 0 && (

Response Headers ({request.responseHeaders.length})

{request.responseHeaders.map((header, index) => (
{header.name}:
{header.value}
))}
)}
{(showScreenshots ? paginatedTimelineEntries.length === 0 : paginatedRequests.length === 0) && (
{showScreenshots ? 'No timeline entries found matching the current filters' : 'No HTTP requests found matching the current filters'}
)}
) }