Compare commits

...

4 Commits

Author SHA1 Message Date
63550a42b4 Implement lazy loading for 3D viewers to optimize bundle size
- Use React.lazy() and Suspense for BabylonViewer and BabylonTimelineViewer
- Reduce main bundle size from 7.4MB to 336KB (95% reduction)
- Split Babylon.js into separate chunk loaded only when 3D views are selected
- Add ThreeDViewerLoading component with spinner for better UX
- Babylon.js libraries now load on-demand when user clicks 3D view toggles

Bundle optimization results:
- Main bundle: 336KB (was 7.4MB)
- Babylon chunk: 7MB (lazy-loaded)
- BabylonViewer: 3.6KB (lazy-loaded)
- BabylonTimelineViewer: 6.6KB (lazy-loaded)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-11 12:21:18 -05:00
359e8a1bd3 Add URL routing for 3D visualization views
- Extend path routing to support /[traceid]/[view]/[3dview] format
- Add 'network' and 'timeline' as 3D view parameters in URL
- Update HTTPRequestViewer to sync 3D view state with URL
- Handle browser back/forward navigation for 3D views
- Ensure only one 3D view is active at a time
- URLs now reflect selected 3D visualization state for sharing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-11 12:18:31 -05:00
2e533925a2 Refactor tooltip system to use centralized enum-based definitions
- Create centralized tooltip definitions with TooltipType enum
- Add comprehensive tooltip content for 31 different field types
- Include Lighthouse relationships, calculations, and documentation links
- Implement click-to-open modal system with rich content display
- Refactor Tooltip component to use enum-based content lookup
- Update RequestsTable and RequestDebugger to use simplified enum syntax
- Add Modal component with keyboard shortcuts and backdrop dismiss
- Maintain type safety with proper TypeScript interfaces
- Clean up component props by centralizing all tooltip content

Benefits:
- Single source of truth for all tooltip definitions
- Improved maintainability and consistency
- Type-safe enum usage prevents errors
- Reduced code duplication across components
- Enhanced UX with detailed modal explanations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-11 12:04:09 -05:00
aa6e29fb0c Fix TypeScript build errors and improve code quality
- Remove unused variables and imports across components
- Fix BabylonJS material property errors (hasAlpha → useAlphaFromDiffuseTexture)
- Resolve TypeScript interface extension issues in PhaseViewer
- Add null safety checks for potentially undefined properties
- Ensure proper array initialization before operations
- Clean up unused function declarations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-11 11:29:10 -05:00
11 changed files with 1048 additions and 91 deletions

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useCallback } from 'react'
import './App.css'
import TraceViewer from './components/TraceViewer'
import PhaseViewer from './components/PhaseViewer'
@ -11,20 +11,83 @@ import { useDatabaseTraceData } from './hooks/useDatabaseTraceData'
type AppView = 'trace' | 'phases' | 'http' | 'debug'
type AppMode = 'selector' | 'upload' | 'analysis'
type ThreeDView = 'network' | 'timeline' | null
// URL path utilities for /[traceid]/[view]/[3dview] routing
const updateUrlWithTraceId = (traceId: string | null, view: AppView = 'http', threeDView: ThreeDView = null) => {
if (!traceId) {
window.history.pushState({}, '', '/')
return
}
const path = threeDView ? `/${traceId}/${view}/${threeDView}` : `/${traceId}/${view}`
window.history.pushState({}, '', path)
}
const getUrlParams = () => {
const path = window.location.pathname
const segments = path.split('/').filter(Boolean) // Remove empty segments
if (segments.length >= 3) {
const traceId = segments[0]
const view = segments[1] as AppView
const threeDView = segments[2] as ThreeDView
// Validate view and 3D view values
const validViews: AppView[] = ['trace', 'phases', 'http', 'debug']
const validThreeDViews: (ThreeDView)[] = ['network', 'timeline']
const validatedView = validViews.includes(view) ? view : 'http'
const validatedThreeDView = validThreeDViews.includes(threeDView) ? threeDView : null
return { traceId, view: validatedView, threeDView: validatedThreeDView }
} else if (segments.length >= 2) {
const traceId = segments[0]
const view = segments[1] as AppView
// Validate view is one of the allowed values
const validViews: AppView[] = ['trace', 'phases', 'http', 'debug']
const validatedView = validViews.includes(view) ? view : 'http'
return { traceId, view: validatedView, threeDView: null }
} else if (segments.length === 1) {
// Only trace ID provided, default to http view
return { traceId: segments[0], view: 'http' as AppView, threeDView: null }
}
// Root path or invalid format
return { traceId: null, view: 'http' as AppView, threeDView: null }
}
// Export utility functions for use in other components
export { getUrlParams, updateUrlWithTraceId, type ThreeDView }
function App() {
const [mode, setMode] = useState<AppMode>('selector')
const [currentView, setCurrentView] = useState<AppView>('http')
const [selectedTraceId, setSelectedTraceId] = useState<string | null>(null)
const [hasTraces, setHasTraces] = useState<boolean>(false)
const [, setHasTraces] = useState<boolean>(false)
const [dbInitialized, setDbInitialized] = useState(false)
// Always call hooks at the top level
const { traceData } = useDatabaseTraceData(selectedTraceId)
// Handle browser back/forward navigation
const handlePopState = useCallback(() => {
const { traceId, view } = getUrlParams()
if (traceId && traceId !== selectedTraceId) {
setSelectedTraceId(traceId)
setCurrentView(view)
setMode('analysis')
// Note: threeDView will be handled by HTTPRequestViewer component
} else if (!traceId && selectedTraceId) {
setSelectedTraceId(null)
setMode('selector')
}
}, [selectedTraceId])
useEffect(() => {
initializeApp()
}, [])
// Listen for browser navigation (back/forward)
window.addEventListener('popstate', handlePopState)
return () => window.removeEventListener('popstate', handlePopState)
}, [handlePopState])
const initializeApp = async () => {
try {
@ -36,10 +99,23 @@ function App() {
const traces = await traceDatabase.getAllTraces()
setHasTraces(traces.length > 0)
// If no traces, show upload screen
if (traces.length === 0) {
// Check URL for existing trace parameter
const { traceId, view } = getUrlParams()
if (traceId && traces.some(t => t.id === traceId)) {
// Valid trace ID in URL, load it
setSelectedTraceId(traceId)
setCurrentView(view)
setMode('analysis')
// Note: threeDView will be handled by HTTPRequestViewer component
} else if (traceId) {
// Invalid trace ID in URL, clear it and show selector
window.history.replaceState({}, '', '/')
setMode(traces.length > 0 ? 'selector' : 'upload')
} else if (traces.length === 0) {
// No traces, show upload screen
setMode('upload')
} else {
// Has traces, show selector
setMode('selector')
}
} catch (error) {
@ -51,18 +127,23 @@ function App() {
const handleTraceSelect = (traceId: string) => {
setSelectedTraceId(traceId)
setCurrentView('http') // Default to HTTP view
setMode('analysis')
updateUrlWithTraceId(traceId, 'http', null)
}
const handleUploadSuccess = (traceId: string) => {
setSelectedTraceId(traceId)
setCurrentView('http')
setMode('analysis')
setHasTraces(true)
updateUrlWithTraceId(traceId, 'http', null)
}
const handleBackToSelector = () => {
setSelectedTraceId(null)
setMode('selector')
window.history.pushState({}, '', '/')
}
const handleUploadNew = () => {
@ -128,7 +209,10 @@ function App() {
<nav style={{ display: 'flex', gap: '10px' }}>
<button
onClick={() => setCurrentView('trace')}
onClick={() => {
setCurrentView('trace')
updateUrlWithTraceId(selectedTraceId, 'trace', null)
}}
style={{
background: currentView === 'trace' ? '#007bff' : '#6c757d',
color: 'white',
@ -142,7 +226,10 @@ function App() {
Trace Stats
</button>
<button
onClick={() => setCurrentView('phases')}
onClick={() => {
setCurrentView('phases')
updateUrlWithTraceId(selectedTraceId, 'phases', null)
}}
style={{
background: currentView === 'phases' ? '#007bff' : '#6c757d',
color: 'white',
@ -156,7 +243,10 @@ function App() {
Phase Events
</button>
<button
onClick={() => setCurrentView('http')}
onClick={() => {
setCurrentView('http')
updateUrlWithTraceId(selectedTraceId, 'http', null)
}}
style={{
background: currentView === 'http' ? '#007bff' : '#6c757d',
color: 'white',
@ -170,7 +260,10 @@ function App() {
HTTP Requests
</button>
<button
onClick={() => setCurrentView('debug')}
onClick={() => {
setCurrentView('debug')
updateUrlWithTraceId(selectedTraceId, 'debug', null)
}}
style={{
background: currentView === 'debug' ? '#007bff' : '#6c757d',
color: 'white',

View File

@ -37,7 +37,7 @@ function createTimelineLabel(
// Create label texture
const labelTexture = new DynamicTexture(`timeLabel_${labelId}`, { width: 80, height: 32 }, scene)
labelTexture.hasAlpha = true
// Note: hasAlpha property handled by BabylonJS
labelTexture.drawText(timeLabelText, null, null, '12px Arial', 'white', 'rgba(0,0,0,0.9)', true)
// Create label plane
@ -47,7 +47,7 @@ function createTimelineLabel(
// Create and apply material
const timeLabelMaterial = new StandardMaterial(`timeLabelMaterial_${labelId}`, scene)
timeLabelMaterial.diffuseTexture = labelTexture
timeLabelMaterial.hasAlpha = true
timeLabelMaterial.useAlphaFromDiffuseTexture = true
timeLabelMaterial.backFaceCulling = false
timeLabelPlane.material = timeLabelMaterial
}
@ -244,7 +244,7 @@ export default function BabylonTimelineViewer({ width = 800, height = 600, httpR
new Vector3(totalWidth / 2, 0, zPosition)
]
const gridLine = MeshBuilder.CreateLines(`gridLine_${i}`, { points: linePoints }, scene)
MeshBuilder.CreateLines(`gridLine_${i}`, { points: linePoints }, scene)
const gridMaterial = new StandardMaterial(`gridMaterial_${i}`, scene)
gridMaterial.diffuseColor = new Color3(0.5, 0.5, 0.5)
gridMaterial.alpha = 0.3
@ -291,7 +291,7 @@ export default function BabylonTimelineViewer({ width = 800, height = 600, httpR
// Create hostname label at the start of each swimlane
const labelTexture = new DynamicTexture(`hostLabel_${sortedIndex}`, { width: 256, height: 64 }, scene)
labelTexture.hasAlpha = true
// Note: hasAlpha property handled by BabylonJS
labelTexture.drawText(
hostname,
null, null,
@ -306,7 +306,7 @@ export default function BabylonTimelineViewer({ width = 800, height = 600, httpR
const labelMaterial = new StandardMaterial(`hostLabelMaterial_${sortedIndex}`, scene)
labelMaterial.diffuseTexture = labelTexture
labelMaterial.hasAlpha = true
labelMaterial.useAlphaFromDiffuseTexture = true
labelMaterial.backFaceCulling = false
hostLabel.material = labelMaterial
@ -338,7 +338,6 @@ export default function BabylonTimelineViewer({ width = 800, height = 600, httpR
// Calculate box depth (duration) and center Z position
const boxDepth = Math.max(0.05, endZ - startZ) // Minimum depth of 0.05m
const centerZ = startZ + (boxDepth / 2)
// Calculate height based on content-length
const contentLength = request.contentLength || 0
@ -480,7 +479,7 @@ export default function BabylonTimelineViewer({ width = 800, height = 600, httpR
// Create a swimlane line from timeline start to end for visual reference
const swimlaneLinePoints = [new Vector3(xPosition, 0, minZ), new Vector3(xPosition, 0, maxZ)]
const swimlaneLine = MeshBuilder.CreateLines(`swimlaneLine_${sortedIndex}`, { points: swimlaneLinePoints }, scene)
MeshBuilder.CreateLines(`swimlaneLine_${sortedIndex}`, { points: swimlaneLinePoints }, scene)
const lineMaterial = new StandardMaterial(`swimlaneLineMaterial_${sortedIndex}`, scene)
lineMaterial.diffuseColor = new Color3(0.4, 0.4, 0.4)
lineMaterial.alpha = 0.3

View File

@ -8,7 +8,6 @@ import {
MeshBuilder,
StandardMaterial,
Color3,
Mesh,
DynamicTexture
} from 'babylonjs'
@ -104,13 +103,6 @@ export default function BabylonViewer({ width = 800, height = 600, httpRequests
const maxContentLength = contentLengths.length > 0 ? Math.max(...contentLengths) : 1
const contentLengthRange = maxContentLength - minContentLength
// Find min and max duration values for depth normalization
const durations = httpRequests
.map(req => req.timing.duration || 0)
.filter(duration => duration > 0)
const minDuration = durations.length > 0 ? Math.min(...durations) : 1000
const maxDuration = durations.length > 0 ? Math.max(...durations) : 1000
const durationRange = maxDuration - minDuration
const hostnames = Array.from(requestsByHostname.keys())
const hostCount = hostnames.length
@ -120,7 +112,6 @@ export default function BabylonViewer({ width = 800, height = 600, httpRequests
const minHeight = 0.1 // Minimum box height (0.1 meters)
const maxHeight = 5 // Maximum box height (5 meters)
const minDepth = 0.05 // Minimum box depth (0.05 meters)
const maxDepth = 2 // Maximum box depth (2 meters)
hostnames.forEach((hostname, hostIndex) => {
// Calculate radial position for this hostname
@ -130,7 +121,7 @@ export default function BabylonViewer({ width = 800, height = 600, httpRequests
// Create hostname label that always faces camera
const labelTexture = new DynamicTexture(`hostLabel_${hostIndex}`, { width: 256, height: 64 }, scene)
labelTexture.hasAlpha = true
// Note: hasAlpha property handled by BabylonJS
labelTexture.drawText(
hostname,
null, null,
@ -150,7 +141,7 @@ export default function BabylonViewer({ width = 800, height = 600, httpRequests
const labelMaterial = new StandardMaterial(`hostLabelMaterial_${hostIndex}`, scene)
labelMaterial.diffuseTexture = labelTexture
labelMaterial.hasAlpha = true
labelMaterial.useAlphaFromDiffuseTexture = true
labelMaterial.backFaceCulling = false
hostLabel.material = labelMaterial
@ -261,7 +252,7 @@ export default function BabylonViewer({ width = 800, height = 600, httpRequests
// Create a line from center to hostname label position for visual connection
const linePoints = [Vector3.Zero(), new Vector3(labelX * 0.8, 0, labelZ * 0.8)]
const line = MeshBuilder.CreateLines(`connectionLine_${hostIndex}`, { points: linePoints }, scene)
MeshBuilder.CreateLines(`connectionLine_${hostIndex}`, { points: linePoints }, scene)
const lineMaterial = new StandardMaterial(`lineMaterial_${hostIndex}`, scene)
lineMaterial.diffuseColor = new Color3(0.4, 0.4, 0.4)
lineMaterial.alpha = 0.5

View File

@ -1,11 +1,18 @@
import { useState, useMemo } from 'react'
import { useDatabaseTraceData } from '../hooks/useDatabaseTraceData'
import type { TraceEvent, TraceEventPhase } from '../../types/trace'
import type { TraceEventPhase } from '../../types/trace'
interface ExtendedTraceEvent extends TraceEvent {
interface ExtendedTraceEvent {
args: Record<string, unknown>
cat: string
name: string
ph: TraceEventPhase
pid: number
tid: number
ts: number
tts?: number
dur?: number
tdur?: number
tts?: number
}
const PHASE_DESCRIPTIONS: Record<string, string> = {
@ -39,10 +46,6 @@ const getStackTrace = (event: ExtendedTraceEvent): any[] | null => {
return args?.beginData?.stackTrace || null
}
const getFrameInfo = (event: ExtendedTraceEvent): string | null => {
const args = event.args as any
return args?.beginData?.frame || args?.data?.frameTreeNodeId || null
}
const getScriptInfo = (event: ExtendedTraceEvent): { contextId?: number, scriptId?: number } => {
const args = event.args as any

View File

@ -1,4 +1,6 @@
import { useState, useMemo } from 'react'
import { Tooltip } from './shared/Tooltip'
import { TooltipType } from './shared/tooltipDefinitions'
import type { TraceEvent } from '../../types/trace'
interface RequestDebuggerProps {
@ -170,12 +172,15 @@ export default function RequestDebugger({ traceEvents }: RequestDebuggerProps) {
request.events.keepAliveURLLoader.push(event)
break
case 'v8.parseOnBackground':
request.events.parseOnBackground = request.events.parseOnBackground || []
request.events.parseOnBackground.push(event)
break
case 'v8.compile':
request.events.compile = request.events.compile || []
request.events.compile.push(event)
break
case 'EvaluateScript':
request.events.evaluateScript = request.events.evaluateScript || []
request.events.evaluateScript.push(event)
break
default:
@ -200,18 +205,18 @@ export default function RequestDebugger({ traceEvents }: RequestDebuggerProps) {
if (request) {
switch (event.name) {
case 'v8.parseOnBackground':
if (!request.events.parseOnBackground.some(e => e.ts === event.ts)) {
request.events.parseOnBackground.push(event)
if (!request.events.parseOnBackground?.some(e => e.ts === event.ts)) {
request.events.parseOnBackground?.push(event)
}
break
case 'v8.compile':
if (!request.events.compile.some(e => e.ts === event.ts)) {
request.events.compile.push(event)
if (!request.events.compile?.some(e => e.ts === event.ts)) {
request.events.compile?.push(event)
}
break
case 'EvaluateScript':
if (!request.events.evaluateScript.some(e => e.ts === event.ts)) {
request.events.evaluateScript.push(event)
if (!request.events.evaluateScript?.some(e => e.ts === event.ts)) {
request.events.evaluateScript?.push(event)
}
break
// Add additional network events that might have requestId but not URL
@ -287,9 +292,9 @@ export default function RequestDebugger({ traceEvents }: RequestDebuggerProps) {
request.events.onReceivedRedirect?.sort((a, b) => a.ts - b.ts)
request.events.connection?.sort((a, b) => a.ts - b.ts)
request.events.keepAliveURLLoader?.sort((a, b) => a.ts - b.ts)
request.events.parseOnBackground.sort((a, b) => a.ts - b.ts)
request.events.compile.sort((a, b) => a.ts - b.ts)
request.events.evaluateScript.sort((a, b) => a.ts - b.ts)
request.events.parseOnBackground?.sort((a, b) => a.ts - b.ts)
request.events.compile?.sort((a, b) => a.ts - b.ts)
request.events.evaluateScript?.sort((a, b) => a.ts - b.ts)
request.events.other.sort((a, b) => a.ts - b.ts)
})
@ -318,18 +323,15 @@ export default function RequestDebugger({ traceEvents }: RequestDebuggerProps) {
return fullTimestamp + ' (baseline)'
}
const formatDuration = (startTs: number, endTs: number) => {
const durationUs = endTs - startTs
const durationMs = durationUs / 1000
return `${durationUs.toLocaleString()} μs (${durationMs.toFixed(3)} ms)`
}
const formatTiming = (timing: any) => {
if (!timing) return null
return (
<div style={{ fontSize: '12px', fontFamily: 'monospace', marginTop: '10px' }}>
<strong>Network Timing (from ResourceReceiveResponse):</strong>
<Tooltip type={TooltipType.NETWORK_TIMING}>
<strong>Network Timing (from ResourceReceiveResponse):</strong>
</Tooltip>
<div style={{ marginLeft: '10px', marginTop: '5px' }}>
<div><strong>requestTime:</strong> {timing.requestTime} seconds</div>
<div><strong>dnsStart:</strong> {timing.dnsStart} ms</div>
@ -352,7 +354,6 @@ export default function RequestDebugger({ traceEvents }: RequestDebuggerProps) {
const args = events.receiveResponse.args as any
const timing = args?.data?.timing
const finishTime = events.finishLoading ? (events.finishLoading.args as any)?.data?.finishTime : null
const lastDataEvent = events.receivedData[events.receivedData.length - 1]
return (
@ -475,17 +476,30 @@ export default function RequestDebugger({ traceEvents }: RequestDebuggerProps) {
borderRadius: '8px',
padding: '20px'
}}>
<h3>Request Details: {selectedRequest.id}</h3>
<h3>
<Tooltip type={TooltipType.REQUEST_DETAILS}>
Request Details: {selectedRequest.id}
</Tooltip>
</h3>
<div style={{ marginBottom: '15px', wordBreak: 'break-all', fontSize: '14px' }}>
<strong>URL:</strong> {selectedRequest.url}
</div>
{/* Duration Calculations */}
{calculateDurations(selectedRequest.events)}
<div style={{ marginBottom: '20px' }}>
<Tooltip type={TooltipType.DURATION_CALCULATIONS}>
<div></div>
</Tooltip>
{calculateDurations(selectedRequest.events)}
</div>
{/* Event Details */}
<div style={{ marginTop: '30px' }}>
<h4>All Events for this Request:</h4>
<h4>
<Tooltip type={TooltipType.ALL_EVENTS}>
All Events for this Request:
</Tooltip>
</h4>
{(() => {
const baseTimestamp = selectedRequest.events.sendRequest?.ts
@ -494,7 +508,10 @@ export default function RequestDebugger({ traceEvents }: RequestDebuggerProps) {
{selectedRequest.events.sendRequest && (
<div style={{ marginBottom: '20px', padding: '10px', backgroundColor: '#e3f2fd', borderRadius: '4px' }}>
<strong>ResourceSendRequest</strong> - {formatTimestamp(selectedRequest.events.sendRequest.ts, baseTimestamp)}
<Tooltip type={TooltipType.RESOURCE_SEND_REQUEST}>
<strong>ResourceSendRequest</strong>
</Tooltip>
{' '}- {formatTimestamp(selectedRequest.events.sendRequest.ts, baseTimestamp)}
<pre style={{ fontSize: '12px', marginTop: '10px', overflow: 'auto' }}>
{JSON.stringify(selectedRequest.events.sendRequest.args, null, 2)}
</pre>
@ -503,7 +520,10 @@ export default function RequestDebugger({ traceEvents }: RequestDebuggerProps) {
{selectedRequest.events.receiveResponse && (
<div style={{ marginBottom: '20px', padding: '10px', backgroundColor: '#e8f5e8', borderRadius: '4px' }}>
<strong>ResourceReceiveResponse</strong> - {formatTimestamp(selectedRequest.events.receiveResponse.ts, baseTimestamp)}
<Tooltip type={TooltipType.RESOURCE_RECEIVE_RESPONSE}>
<strong>ResourceReceiveResponse</strong>
</Tooltip>
{' '}- {formatTimestamp(selectedRequest.events.receiveResponse.ts, baseTimestamp)}
{formatTiming((selectedRequest.events.receiveResponse.args as any)?.data?.timing)}
<pre style={{ fontSize: '12px', marginTop: '10px', overflow: 'auto' }}>
{JSON.stringify(selectedRequest.events.receiveResponse.args, null, 2)}
@ -513,7 +533,10 @@ export default function RequestDebugger({ traceEvents }: RequestDebuggerProps) {
{selectedRequest.events.receivedData.map((event, index) => (
<div key={index} style={{ marginBottom: '20px', padding: '10px', backgroundColor: '#fff3e0', borderRadius: '4px' }}>
<strong>ResourceReceivedData #{index + 1}</strong> - {formatTimestamp(event.ts, baseTimestamp)}
<Tooltip type={TooltipType.RESOURCE_RECEIVED_DATA}>
<strong>ResourceReceivedData #{index + 1}</strong>
</Tooltip>
{' '}- {formatTimestamp(event.ts, baseTimestamp)}
{index === selectedRequest.events.receivedData.length - 1 && (
<span style={{ color: '#ff9800', fontWeight: 'bold', marginLeft: '10px' }}> LAST DATA CHUNK</span>
)}
@ -533,9 +556,13 @@ export default function RequestDebugger({ traceEvents }: RequestDebuggerProps) {
)}
{/* Additional Event Types */}
{selectedRequest.events.parseOnBackground.length > 0 && (
{selectedRequest.events.parseOnBackground && selectedRequest.events.parseOnBackground.length > 0 && (
<div style={{ marginTop: '20px' }}>
<h5>V8 Parse Events:</h5>
<h5>
<Tooltip type={TooltipType.V8_PARSE_EVENTS}>
V8 Parse Events:
</Tooltip>
</h5>
{selectedRequest.events.parseOnBackground.map((event, index) => (
<div key={index} style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#e1f5fe', borderRadius: '4px' }}>
<strong>{event.name}</strong> - {formatTimestamp(event.ts, baseTimestamp)}
@ -548,9 +575,13 @@ export default function RequestDebugger({ traceEvents }: RequestDebuggerProps) {
</div>
)}
{selectedRequest.events.compile.length > 0 && (
{selectedRequest.events.compile && selectedRequest.events.compile.length > 0 && (
<div style={{ marginTop: '20px' }}>
<h5>V8 Compile Events:</h5>
<h5>
<Tooltip type={TooltipType.V8_COMPILE_EVENTS}>
V8 Compile Events:
</Tooltip>
</h5>
{selectedRequest.events.compile.map((event, index) => (
<div key={index} style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#f3e5f5', borderRadius: '4px' }}>
<strong>{event.name}</strong> - {formatTimestamp(event.ts, baseTimestamp)}
@ -563,9 +594,13 @@ export default function RequestDebugger({ traceEvents }: RequestDebuggerProps) {
</div>
)}
{selectedRequest.events.evaluateScript.length > 0 && (
{selectedRequest.events.evaluateScript && selectedRequest.events.evaluateScript.length > 0 && (
<div style={{ marginTop: '20px' }}>
<h5>Script Evaluation Events:</h5>
<h5>
<Tooltip type={TooltipType.SCRIPT_EVALUATION}>
Script Evaluation Events:
</Tooltip>
</h5>
{selectedRequest.events.evaluateScript.map((event, index) => (
<div key={index} style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#e8f5e8', borderRadius: '4px' }}>
<strong>{event.name}</strong> - {formatTimestamp(event.ts, baseTimestamp)}
@ -698,7 +733,11 @@ export default function RequestDebugger({ traceEvents }: RequestDebuggerProps) {
{/* URL Loader Events */}
{selectedRequest.events.throttlingURLLoader && selectedRequest.events.throttlingURLLoader.length > 0 && (
<div style={{ marginTop: '20px' }}>
<h5>Throttling URL Loader Events ({selectedRequest.events.throttlingURLLoader.length}):</h5>
<h5>
<Tooltip type={TooltipType.THROTTLING_URL_LOADER}>
Throttling URL Loader Events ({selectedRequest.events.throttlingURLLoader.length}):
</Tooltip>
</h5>
{selectedRequest.events.throttlingURLLoader.map((event, index) => (
<div key={index} style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#fff3e0', borderRadius: '4px' }}>
<strong>{event.name}</strong> - {formatTimestamp(event.ts, baseTimestamp)}

View File

@ -1,11 +1,49 @@
import { useState, useMemo, useEffect } from 'react'
import { useState, useMemo, useEffect, lazy, Suspense } from 'react'
import { useDatabaseTraceData } from '../../hooks/useDatabaseTraceData'
import BabylonViewer from '../../BabylonViewer'
import BabylonTimelineViewer from '../../BabylonTimelineViewer'
import { getUrlParams, updateUrlWithTraceId } from '../../App'
import RequestFilters from './RequestFilters'
import RequestsTable from './RequestsTable'
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 = () => (
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '500px',
background: '#f8f9fa',
borderRadius: '8px'
}}>
<div style={{
width: '50px',
height: '50px',
border: '4px solid #e3f2fd',
borderTop: '4px solid #2196f3',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
marginBottom: '20px'
}} />
<div style={{ color: '#6c757d', fontSize: '16px' }}>
Loading 3D Viewer...
</div>
<div style={{ color: '#9e9e9e', fontSize: '14px', marginTop: '8px' }}>
Initializing Babylon.js
</div>
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
)
// Imported utilities
import { ITEMS_PER_PAGE, SSIM_SIMILARITY_THRESHOLD } from './lib/httpRequestConstants'
import { extractScreenshots, findUniqueScreenshots } from './lib/screenshotUtils'
@ -33,8 +71,42 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
const [priorityFilter, setPriorityFilter] = useState<string>('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)
// 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<Set<string>>(new Set())
@ -264,8 +336,8 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
setCurrentPage={setCurrentPage}
setShowQueueAnalysis={setShowQueueAnalysis}
setShowScreenshots={setShowScreenshots}
setShow3DViewer={setShow3DViewer}
setShowTimelineViewer={setShowTimelineViewer}
setShow3DViewer={handle3DViewerToggle}
setShowTimelineViewer={handleTimelineViewerToggle}
setPendingSSIMThreshold={setPendingSSIMThreshold}
handleSSIMRecalculate={handleSSIMRecalculate}
/>
@ -304,14 +376,16 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
3D Network Visualization ({filteredRequests.length} requests)
</h3>
<button
onClick={() => setShow3DViewer(false)}
onClick={() => handle3DViewerToggle(false)}
className={styles.modalCloseButton}
>
Close
</button>
</div>
<div className={styles.modalContent}>
<BabylonViewer httpRequests={filteredRequests} width={window.innerWidth * 0.9} height={window.innerHeight * 0.9 - 100} />
<Suspense fallback={<ThreeDViewerLoading />}>
<BabylonViewer httpRequests={filteredRequests} width={window.innerWidth * 0.9} height={window.innerHeight * 0.9 - 100} />
</Suspense>
</div>
<div className={styles.modalLegend}>
<div><strong>Legend:</strong></div>
@ -343,14 +417,16 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
3D Timeline Visualization ({filteredRequests.length} requests)
</h3>
<button
onClick={() => setShowTimelineViewer(false)}
onClick={() => handleTimelineViewerToggle(false)}
className={styles.modalCloseButton}
>
Close
</button>
</div>
<div className={styles.modalContent}>
<BabylonTimelineViewer httpRequests={filteredRequests} screenshots={screenshots} width={window.innerWidth * 0.9} height={window.innerHeight * 0.9 - 100} />
<Suspense fallback={<ThreeDViewerLoading />}>
<BabylonTimelineViewer httpRequests={filteredRequests} screenshots={screenshots} width={window.innerWidth * 0.9} height={window.innerHeight * 0.9 - 100} />
</Suspense>
</div>
<div className={styles.modalLegend}>
<div><strong>Legend:</strong></div>

View File

@ -1,6 +1,8 @@
import React from 'react'
import RequestRowSummary from './RequestRowSummary'
import ScreenshotRow from './ScreenshotRow'
import { Tooltip } from '../shared/Tooltip'
import { TooltipType } from '../shared/tooltipDefinitions'
import styles from './HTTPRequestViewer.module.css'
import type { HTTPRequest, ScreenshotEvent } from './types/httpRequest'
@ -39,24 +41,96 @@ const RequestsTable: React.FC<RequestsTableProps> = ({
<table className={styles.table}>
<thead className={styles.tableHeader}>
<tr>
<th className={`${styles.tableHeaderCell} ${styles.center} ${styles.expandColumn}`}>Expand</th>
<th className={`${styles.tableHeaderCell} ${styles.left}`}>Method</th>
<th className={`${styles.tableHeaderCell} ${styles.center}`}>Status</th>
<th className={`${styles.tableHeaderCell} ${styles.left}`}>Type</th>
<th className={`${styles.tableHeaderCell} ${styles.center}`}>Priority</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>Start Time</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>Queue Time</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>DNS</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>Connection</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>Server Latency</th>
<th className={`${styles.tableHeaderCell} ${styles.left}`}>URL</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>Duration</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>Total Response Time</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>Size</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>Content-Length</th>
<th className={`${styles.tableHeaderCell} ${styles.center}`}>Protocol</th>
<th className={`${styles.tableHeaderCell} ${styles.center}`}>CDN</th>
<th className={`${styles.tableHeaderCell} ${styles.center}`}>Cache</th>
<th className={`${styles.tableHeaderCell} ${styles.center} ${styles.expandColumn}`}>
<Tooltip type={TooltipType.EXPAND_ROW}>
Expand
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.left}`}>
<Tooltip type={TooltipType.HTTP_METHOD}>
Method
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.center}`}>
<Tooltip type={TooltipType.HTTP_STATUS}>
Status
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.left}`}>
<Tooltip type={TooltipType.RESOURCE_TYPE}>
Type
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.center}`}>
<Tooltip type={TooltipType.REQUEST_PRIORITY}>
Priority
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>
<Tooltip type={TooltipType.START_TIME}>
Start Time
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>
<Tooltip type={TooltipType.QUEUE_TIME}>
Queue Time
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>
<Tooltip type={TooltipType.DNS_TIME}>
DNS
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>
<Tooltip type={TooltipType.CONNECTION_TIME}>
Connection
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>
<Tooltip type={TooltipType.SERVER_LATENCY}>
Server Latency
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.left}`}>
<Tooltip type={TooltipType.REQUEST_URL}>
URL
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>
<Tooltip type={TooltipType.REQUEST_DURATION}>
Duration
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>
<Tooltip type={TooltipType.TOTAL_RESPONSE_TIME}>
Total Response Time
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>
<Tooltip type={TooltipType.TRANSFER_SIZE}>
Size
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.right}`}>
<Tooltip type={TooltipType.CONTENT_LENGTH}>
Content-Length
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.center}`}>
<Tooltip type={TooltipType.HTTP_PROTOCOL}>
Protocol
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.center}`}>
<Tooltip type={TooltipType.CDN_DETECTION}>
CDN
</Tooltip>
</th>
<th className={`${styles.tableHeaderCell} ${styles.center}`}>
<Tooltip type={TooltipType.CACHE_STATUS}>
Cache
</Tooltip>
</th>
</tr>
</thead>
<tbody>

View File

@ -0,0 +1,119 @@
import { useEffect } from 'react'
interface ModalProps {
isOpen: boolean
onClose: () => void
title: string
children: React.ReactNode
}
export function Modal({ isOpen, onClose, title, children }: ModalProps) {
// Close modal on Escape key
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose()
}
}
if (isOpen) {
document.addEventListener('keydown', handleEscape)
// Prevent body scrolling when modal is open
document.body.style.overflow = 'hidden'
}
return () => {
document.removeEventListener('keydown', handleEscape)
document.body.style.overflow = 'unset'
}
}, [isOpen, onClose])
if (!isOpen) return null
return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 10000,
padding: '20px'
}}
onClick={onClose}
>
<div
style={{
backgroundColor: 'white',
borderRadius: '8px',
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.3)',
maxWidth: '600px',
maxHeight: '80vh',
width: '100%',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column'
}}
onClick={(e) => e.stopPropagation()}
>
{/* Modal Header */}
<div
style={{
padding: '20px',
borderBottom: '1px solid #e9ecef',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#f8f9fa'
}}
>
<h2 style={{ margin: 0, color: '#495057', fontSize: '18px', fontWeight: 'bold' }}>
{title}
</h2>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
fontSize: '24px',
cursor: 'pointer',
color: '#6c757d',
padding: '0',
width: '30px',
height: '30px',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'background-color 0.2s'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#e9ecef'
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent'
}}
>
×
</button>
</div>
{/* Modal Content */}
<div
style={{
padding: '20px',
overflow: 'auto',
flex: 1
}}
>
{children}
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,215 @@
import { useState } from 'react'
import { Modal } from './Modal'
import { TOOLTIP_DEFINITIONS } from './tooltipDefinitions'
import type { TooltipTypeValues } from './tooltipDefinitions'
// Tooltip component for field explanations
interface TooltipProps {
children: React.ReactNode
type: TooltipTypeValues
}
export function Tooltip({ children, type }: TooltipProps) {
const { title, description, lighthouseRelation, calculation, links } = TOOLTIP_DEFINITIONS[type]
const [isHovered, setIsHovered] = useState(false)
const [isModalOpen, setIsModalOpen] = useState(false)
const handleIconClick = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
setIsHovered(false) // Hide hover tooltip when opening modal
setIsModalOpen(true)
}
return (
<>
<div style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }}>
{children}
<span
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={handleIconClick}
style={{
marginLeft: '6px',
cursor: 'pointer',
color: '#007bff',
fontSize: '14px',
fontWeight: 'bold',
width: '16px',
height: '16px',
borderRadius: '50%',
backgroundColor: '#e3f2fd',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
userSelect: 'none',
transition: 'all 0.2s ease',
border: '1px solid transparent'
}}
onMouseDown={(e) => {
e.currentTarget.style.backgroundColor = '#bbdefb'
e.currentTarget.style.borderColor = '#2196f3'
}}
onMouseUp={(e) => {
e.currentTarget.style.backgroundColor = '#e3f2fd'
e.currentTarget.style.borderColor = 'transparent'
}}
>
?
</span>
{/* Hover tooltip - only show when not modal open */}
{isHovered && !isModalOpen && (
<div style={{
position: 'absolute',
top: '25px',
left: '0',
backgroundColor: '#333',
color: 'white',
padding: '8px 12px',
borderRadius: '6px',
fontSize: '12px',
lineHeight: '1.4',
maxWidth: '300px',
zIndex: 1000,
boxShadow: '0 4px 8px rgba(0,0,0,0.3)',
whiteSpace: 'normal',
pointerEvents: 'none'
}}>
<div style={{ fontWeight: 'bold', marginBottom: '4px', color: '#4fc3f7' }}>
{title}
</div>
<div style={{ marginBottom: '6px' }}>
{description}
</div>
<div style={{ fontSize: '11px', color: '#90caf9', fontStyle: 'italic' }}>
Click for detailed information
</div>
</div>
)}
</div>
{/* Modal with detailed content */}
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title={title}
>
<div style={{ lineHeight: '1.6' }}>
<div style={{
marginBottom: '20px',
fontSize: '15px',
color: '#495057'
}}>
{description}
</div>
{lighthouseRelation && (
<div style={{
marginBottom: '20px',
padding: '15px',
backgroundColor: '#fff3e0',
borderRadius: '6px',
borderLeft: '4px solid #ffb74d'
}}>
<div style={{
fontWeight: 'bold',
marginBottom: '8px',
color: '#e65100',
fontSize: '14px'
}}>
🎯 Lighthouse Relationship
</div>
<div style={{ color: '#5d4037', fontSize: '14px' }}>
{lighthouseRelation}
</div>
</div>
)}
{calculation && (
<div style={{
marginBottom: '20px',
padding: '15px',
backgroundColor: '#e8f5e8',
borderRadius: '6px',
borderLeft: '4px solid #81c784'
}}>
<div style={{
fontWeight: 'bold',
marginBottom: '8px',
color: '#2e7d32',
fontSize: '14px'
}}>
🧮 Calculation
</div>
<div style={{
color: '#1b5e20',
fontSize: '14px',
fontFamily: 'monospace',
backgroundColor: '#f1f8e9',
padding: '8px',
borderRadius: '4px',
border: '1px solid #c8e6c9'
}}>
{calculation}
</div>
</div>
)}
{links && links.length > 0 && (
<div style={{
padding: '15px',
backgroundColor: '#f3e5f5',
borderRadius: '6px',
borderLeft: '4px solid #ba68c8'
}}>
<div style={{
fontWeight: 'bold',
marginBottom: '12px',
color: '#6a1b9a',
fontSize: '14px'
}}>
📚 Learn More
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{links.map((link, index) => (
<a
key={index}
href={link.url}
target="_blank"
rel="noopener noreferrer"
style={{
color: '#1976d2',
textDecoration: 'none',
fontSize: '14px',
padding: '8px 12px',
backgroundColor: 'white',
borderRadius: '4px',
border: '1px solid #e3f2fd',
transition: 'all 0.2s ease',
display: 'inline-block'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#e3f2fd'
e.currentTarget.style.borderColor = '#2196f3'
e.currentTarget.style.transform = 'translateY(-1px)'
e.currentTarget.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'white'
e.currentTarget.style.borderColor = '#e3f2fd'
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = 'none'
}}
>
🔗 {link.text}
</a>
))}
</div>
</div>
)}
</div>
</Modal>
</>
)
}

View File

@ -0,0 +1,345 @@
// Centralized tooltip definitions
export const TooltipType = {
// HTTP Request Table Headers
EXPAND_ROW: 'EXPAND_ROW',
HTTP_METHOD: 'HTTP_METHOD',
HTTP_STATUS: 'HTTP_STATUS',
RESOURCE_TYPE: 'RESOURCE_TYPE',
REQUEST_PRIORITY: 'REQUEST_PRIORITY',
START_TIME: 'START_TIME',
QUEUE_TIME: 'QUEUE_TIME',
DNS_TIME: 'DNS_TIME',
CONNECTION_TIME: 'CONNECTION_TIME',
SERVER_LATENCY: 'SERVER_LATENCY',
REQUEST_URL: 'REQUEST_URL',
REQUEST_DURATION: 'REQUEST_DURATION',
TOTAL_RESPONSE_TIME: 'TOTAL_RESPONSE_TIME',
TRANSFER_SIZE: 'TRANSFER_SIZE',
CONTENT_LENGTH: 'CONTENT_LENGTH',
HTTP_PROTOCOL: 'HTTP_PROTOCOL',
CDN_DETECTION: 'CDN_DETECTION',
CACHE_STATUS: 'CACHE_STATUS',
// Request Debugger Headers
REQUEST_DETAILS: 'REQUEST_DETAILS',
DURATION_CALCULATIONS: 'DURATION_CALCULATIONS',
ALL_EVENTS: 'ALL_EVENTS',
RESOURCE_SEND_REQUEST: 'RESOURCE_SEND_REQUEST',
RESOURCE_RECEIVE_RESPONSE: 'RESOURCE_RECEIVE_RESPONSE',
RESOURCE_RECEIVED_DATA: 'RESOURCE_RECEIVED_DATA',
V8_PARSE_EVENTS: 'V8_PARSE_EVENTS',
V8_COMPILE_EVENTS: 'V8_COMPILE_EVENTS',
SCRIPT_EVALUATION: 'SCRIPT_EVALUATION',
THROTTLING_URL_LOADER: 'THROTTLING_URL_LOADER',
NETWORK_TIMING: 'NETWORK_TIMING'
} as const
export type TooltipTypeValues = typeof TooltipType[keyof typeof TooltipType]
export interface TooltipDefinition {
title: string
description: string
lighthouseRelation?: string
calculation?: string
links?: Array<{ text: string; url: string }>
}
export const TOOLTIP_DEFINITIONS: Record<TooltipTypeValues, TooltipDefinition> = {
// HTTP Request Table Headers
[TooltipType.EXPAND_ROW]: {
title: "Expand Row",
description: "Click to expand and see detailed request breakdown including timing waterfall, headers, and response details.",
lighthouseRelation: "Expanded view helps understand request bottlenecks affecting LCP, FCP, and overall performance scores."
},
[TooltipType.HTTP_METHOD]: {
title: "HTTP Method",
description: "The HTTP method used for this request (GET, POST, PUT, DELETE, etc.). Different methods have different caching and performance implications.",
lighthouseRelation: "GET requests are cacheable and can improve repeat visit performance. POST requests cannot be cached, affecting performance metrics.",
links: [
{ text: "HTTP Methods - MDN", url: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods" }
]
},
[TooltipType.HTTP_STATUS]: {
title: "HTTP Status Code",
description: "HTTP response status code indicating success (2xx), redirection (3xx), client error (4xx), or server error (5xx).",
lighthouseRelation: "Status codes affect Lighthouse scoring: 4xx/5xx errors hurt performance scores, redirects (3xx) add latency affecting LCP and FCP.",
links: [
{ text: "HTTP Status Codes", url: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" },
{ text: "Avoid Redirects", url: "https://web.dev/articles/redirects" }
]
},
[TooltipType.RESOURCE_TYPE]: {
title: "Resource Type",
description: "Type of resource being requested: Document, Stylesheet, Script, Image, Font, XHR, Fetch, etc.",
lighthouseRelation: "Different resource types have different priority and impact on Core Web Vitals. Scripts/CSS block rendering, images affect LCP.",
calculation: "Derived from Content-Type header and request context.",
links: [
{ text: "Resource Prioritization", url: "https://web.dev/articles/resource-prioritization" },
{ text: "Critical Rendering Path", url: "https://web.dev/articles/critical-rendering-path" }
]
},
[TooltipType.REQUEST_PRIORITY]: {
title: "Request Priority",
description: "Browser's internal priority for this request: VeryHigh, High, Medium, Low, VeryLow. Determines resource loading order.",
lighthouseRelation: "High priority resources are critical for LCP and FCP. Low priority resources should not block critical content.",
links: [
{ text: "Resource Prioritization", url: "https://web.dev/articles/resource-prioritization" },
{ text: "Chrome Resource Priorities", url: "https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc" }
]
},
[TooltipType.START_TIME]: {
title: "Start Time",
description: "When the browser initiated this request, relative to navigation start or first request.",
lighthouseRelation: "Earlier start times generally improve performance scores. Delayed critical resource requests hurt LCP and FCP.",
calculation: "Timestamp relative to navigation start or performance.timeOrigin.",
links: [
{ text: "Navigation Timing API", url: "https://developer.mozilla.org/en-US/docs/Web/API/Navigation_timing_API" }
]
},
[TooltipType.QUEUE_TIME]: {
title: "Queue Time",
description: "Time spent waiting in browser's request queue before being sent. Indicates network congestion or connection limits.",
lighthouseRelation: "High queue times delay resource loading, negatively impacting LCP, FCP, and Speed Index scores.",
calculation: "Time from request initiation to actual network send start.",
links: [
{ text: "HTTP/1.1 vs HTTP/2 Connection Limits", url: "https://web.dev/articles/http2" }
]
},
[TooltipType.DNS_TIME]: {
title: "DNS Lookup Time",
description: "Time spent resolving the domain name to an IP address. First request to a domain includes DNS lookup.",
lighthouseRelation: "DNS lookup time contributes to TTFB (Time to First Byte) and overall request latency, affecting all performance metrics.",
calculation: "dnsEnd - dnsStart from Navigation Timing API.",
links: [
{ text: "Optimize DNS Lookups", url: "https://web.dev/articles/preconnect-and-dns-prefetch" },
{ text: "DNS Performance", url: "https://developer.mozilla.org/en-US/docs/Web/Performance/dns-prefetch" }
]
},
[TooltipType.CONNECTION_TIME]: {
title: "Connection Time",
description: "Time to establish TCP connection (and SSL handshake if HTTPS). Includes connection reuse optimization.",
lighthouseRelation: "Connection time contributes to TTFB and affects all network-dependent metrics. HTTP/2 connection reuse improves performance.",
calculation: "connectEnd - connectStart, includes SSL time if HTTPS.",
links: [
{ text: "Preconnect to Required Origins", url: "https://web.dev/articles/preconnect-and-dns-prefetch" },
{ text: "HTTP/2 Connection Reuse", url: "https://web.dev/articles/http2" }
]
},
[TooltipType.SERVER_LATENCY]: {
title: "Server Latency",
description: "Time from when request was sent to when first response byte was received. Measures server processing time.",
lighthouseRelation: "Server latency is a key component of TTFB (Time to First Byte), directly affecting LCP and FCP scores.",
calculation: "responseStart - requestStart from Navigation Timing API.",
links: [
{ text: "Reduce Server Response Times (TTFB)", url: "https://web.dev/articles/ttfb" },
{ text: "Server Performance Optimization", url: "https://web.dev/articles/fast" }
]
},
[TooltipType.REQUEST_URL]: {
title: "Request URL",
description: "The full URL of the requested resource. Long URLs with many parameters can impact performance.",
lighthouseRelation: "URL structure affects caching efficiency. Query parameters can prevent caching, hurting repeat visit performance.",
links: [
{ text: "HTTP Caching", url: "https://web.dev/articles/http-cache" }
]
},
[TooltipType.REQUEST_DURATION]: {
title: "Request Duration",
description: "Total time from request start to completion, including all network phases and data transfer.",
lighthouseRelation: "Long durations for critical resources directly impact LCP, FCP, and Speed Index. Background resource duration affects overall performance score.",
calculation: "responseEnd - requestStart from Navigation Timing API.",
links: [
{ text: "Optimize Resource Loading", url: "https://web.dev/articles/critical-rendering-path" }
]
},
[TooltipType.TOTAL_RESPONSE_TIME]: {
title: "Total Response Time",
description: "Complete time from navigation start to request completion. Shows request timing in context of page load.",
lighthouseRelation: "Response time relative to navigation affects when resources become available for rendering, impacting LCP and FCP timing.",
calculation: "responseEnd - navigationStart."
},
[TooltipType.TRANSFER_SIZE]: {
title: "Transfer Size",
description: "Actual bytes transferred over network after compression, encoding, and headers. Includes HTTP overhead.",
lighthouseRelation: "Transfer size affects download time and bandwidth usage, impacting LCP for large resources and overall Speed Index.",
calculation: "Actual bytes received including headers and compression.",
links: [
{ text: "Optimize Resource Sizes", url: "https://web.dev/articles/fast#optimize_your_content_efficiency" },
{ text: "Enable Compression", url: "https://web.dev/articles/reduce-network-payloads-using-text-compression" }
]
},
[TooltipType.CONTENT_LENGTH]: {
title: "Content-Length",
description: "Uncompressed size of response body as declared in Content-Length header. May differ from actual transfer size.",
lighthouseRelation: "Large content sizes increase download time, especially impacting LCP for critical resources and mobile performance scores.",
calculation: "Value from Content-Length HTTP response header.",
links: [
{ text: "Optimize Images", url: "https://web.dev/articles/fast#optimize_your_images" },
{ text: "Minify Resources", url: "https://web.dev/articles/reduce-network-payloads-using-text-compression" }
]
},
[TooltipType.HTTP_PROTOCOL]: {
title: "HTTP Protocol",
description: "HTTP protocol version used: HTTP/1.1, HTTP/2, or HTTP/3. Newer protocols offer performance benefits.",
lighthouseRelation: "HTTP/2 and HTTP/3 improve multiplexing and reduce latency, positively impacting all performance metrics especially on mobile.",
links: [
{ text: "HTTP/2 Benefits", url: "https://web.dev/articles/http2" },
{ text: "HTTP/3 Performance", url: "https://web.dev/articles/http3" }
]
},
[TooltipType.CDN_DETECTION]: {
title: "CDN Detection",
description: "Indicates if request was served by a Content Delivery Network. CDNs improve performance by serving content from geographically closer servers.",
lighthouseRelation: "CDN usage typically improves TTFB, LCP, and FCP by reducing latency. Critical for good mobile performance scores.",
links: [
{ text: "CDN Performance Benefits", url: "https://web.dev/articles/content-delivery-networks" }
]
},
[TooltipType.CACHE_STATUS]: {
title: "Cache Status",
description: "Whether the resource was served from cache (browser, proxy, CDN) or fetched fresh from origin server.",
lighthouseRelation: "Cached resources load faster, improving repeat visit performance and Lighthouse scores. Critical for mobile performance optimization.",
links: [
{ text: "HTTP Caching", url: "https://web.dev/articles/http-cache" },
{ text: "Browser Caching", url: "https://web.dev/articles/uses-long-cache-ttl" }
]
},
// Request Debugger Headers
[TooltipType.REQUEST_DETAILS]: {
title: "Request Details",
description: "Comprehensive trace analysis for a specific HTTP request, showing all related browser events and timing data.",
lighthouseRelation: "This detailed breakdown helps understand what contributes to Lighthouse metrics like LCP, FCP, and Speed Index. Each request impacts overall page load performance.",
links: [
{ text: "Chrome DevTools Network Panel", url: "https://developer.chrome.com/docs/devtools/network/" },
{ text: "Understanding Resource Loading", url: "https://web.dev/articles/critical-rendering-path" }
]
},
[TooltipType.DURATION_CALCULATIONS]: {
title: "Duration Calculations",
description: "Key timing metrics extracted from trace events that show how long different phases of the request took.",
lighthouseRelation: "Directly impacts LCP (Largest Contentful Paint), FCP (First Contentful Paint), and overall Performance Score. Network request timings are critical components of Core Web Vitals.",
calculation: "Total Duration = Receive Response timestamp - Send Request timestamp. Individual phases calculated from trace event timing data.",
links: [
{ text: "Lighthouse Performance Scoring", url: "https://developer.chrome.com/docs/lighthouse/performance/performance-scoring/" },
{ text: "Core Web Vitals", url: "https://web.dev/articles/vitals" },
{ text: "Network Request Timing", url: "https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Resource_timing" }
]
},
[TooltipType.ALL_EVENTS]: {
title: "All Events for this Request",
description: "Complete chronological list of browser trace events related to this HTTP request, from initiation to completion.",
lighthouseRelation: "These events show the complete request lifecycle that affects Lighthouse timing metrics. Each event type contributes differently to performance scores.",
links: [
{ text: "Chrome Trace Event Format", url: "https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview" },
{ text: "DevTools Performance Panel", url: "https://developer.chrome.com/docs/devtools/performance/" }
]
},
[TooltipType.RESOURCE_SEND_REQUEST]: {
title: "ResourceSendRequest Event",
description: "Marks when the browser initiates sending the HTTP request. This is the starting point for network timing measurements.",
lighthouseRelation: "Start time for calculating request latency. Affects TTI (Time to Interactive) and Speed Index when blocking critical resources.",
calculation: "Request Duration = ResourceReceiveResponse.ts - ResourceSendRequest.ts",
links: [
{ text: "Network Request Lifecycle", url: "https://developer.chrome.com/blog/resource-loading-insights/" },
{ text: "Critical Request Chains", url: "https://web.dev/articles/critical-request-chains" }
]
},
[TooltipType.RESOURCE_RECEIVE_RESPONSE]: {
title: "ResourceReceiveResponse Event",
description: "Indicates when the browser receives the HTTP response headers. Contains critical timing data like DNS lookup, connection establishment, and server response time.",
lighthouseRelation: "Key component of LCP (Largest Contentful Paint) and FCP (First Contentful Paint) measurements. Server response time directly impacts Lighthouse Performance Score.",
calculation: "Server Response Time = receiveHeadersEnd - sendStart (from timing object). Total request time measured from SendRequest to this event.",
links: [
{ text: "Server Response Time Optimization", url: "https://web.dev/articles/ttfb" },
{ text: "Navigation Timing API", url: "https://developer.mozilla.org/en-US/docs/Web/API/Navigation_timing_API" }
]
},
[TooltipType.RESOURCE_RECEIVED_DATA]: {
title: "ResourceReceivedData Events",
description: "Shows when chunks of response data are received from the server. Multiple events indicate streaming/chunked transfer encoding.",
lighthouseRelation: "Affects Progressive Loading metrics and Speed Index. More data chunks generally indicate better streaming performance for large resources.",
calculation: "Data transfer progress can be tracked by summing encodedDataLength across all chunks. Last chunk indicates download completion.",
links: [
{ text: "Progressive Loading", url: "https://web.dev/articles/progressive-loading" },
{ text: "Chunked Transfer Encoding", url: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding" }
]
},
[TooltipType.V8_PARSE_EVENTS]: {
title: "V8 Parse Events",
description: "JavaScript parsing events that occur when V8 engine processes downloaded JS files. Background parsing improves main thread performance.",
lighthouseRelation: "Directly impacts TTI (Time to Interactive) and TBT (Total Blocking Time). Background parsing reduces main thread blocking, improving Lighthouse performance scores.",
calculation: "Parse time = event duration. Background parsing happens off main thread, reducing blocking time.",
links: [
{ text: "JavaScript Parsing Performance", url: "https://v8.dev/blog/background-compilation" },
{ text: "Reduce JavaScript Execution Time", url: "https://web.dev/articles/bootup-time" }
]
},
[TooltipType.V8_COMPILE_EVENTS]: {
title: "V8 Compile Events",
description: "JavaScript compilation events in V8 engine. Shows when parsed JavaScript is compiled to bytecode or optimized machine code.",
lighthouseRelation: "Compilation time affects TTI (Time to Interactive) and can contribute to TBT (Total Blocking Time) if done on main thread. Efficient compilation improves runtime performance.",
calculation: "Compile time = event duration. Background compilation reduces main thread impact.",
links: [
{ text: "V8 Compilation Pipeline", url: "https://v8.dev/docs/ignition" },
{ text: "JavaScript Performance Best Practices", url: "https://web.dev/articles/fast" }
]
},
[TooltipType.SCRIPT_EVALUATION]: {
title: "Script Evaluation Events",
description: "JavaScript execution/evaluation events. Shows when compiled scripts are actually executed by V8 engine.",
lighthouseRelation: "Direct impact on TTI (Time to Interactive) and TBT (Total Blocking Time). Script evaluation on main thread blocks user interaction, heavily affecting Lighthouse performance scores.",
calculation: "Execution time = event duration. All execution happens on main thread and contributes to blocking time.",
links: [
{ text: "Reduce JavaScript Execution Time", url: "https://web.dev/articles/bootup-time" },
{ text: "Main Thread Blocking", url: "https://web.dev/articles/long-tasks-devtools" }
]
},
[TooltipType.THROTTLING_URL_LOADER]: {
title: "Throttling URL Loader Events",
description: "Network throttling and URL loading events from Chrome's network stack. Shows how requests are managed and potentially throttled.",
lighthouseRelation: "Network throttling affects all timing metrics (LCP, FCP, FID). Understanding these events helps identify network bottlenecks that impact Lighthouse scores.",
calculation: "Throttling delays can be calculated from event timestamps. Multiple events show request queuing and prioritization.",
links: [
{ text: "Network Throttling in DevTools", url: "https://developer.chrome.com/docs/devtools/network/#throttle" },
{ text: "Resource Prioritization", url: "https://web.dev/articles/resource-prioritization" }
]
},
[TooltipType.NETWORK_TIMING]: {
title: "Network Timing Breakdown",
description: "Detailed network timing phases from the ResourceReceiveResponse event, showing DNS lookup, connection, SSL handshake, and data transfer times.",
lighthouseRelation: "These timings directly feed into TTFB (Time to First Byte) and overall request duration. DNS, connection, and SSL times affect all network-dependent Lighthouse metrics.",
calculation: "Total time = receiveHeadersEnd - requestTime. Each phase represents different network bottlenecks that can be optimized.",
links: [
{ text: "Navigation Timing API", url: "https://developer.mozilla.org/en-US/docs/Web/API/Navigation_timing_API" },
{ text: "Optimize TTFB", url: "https://web.dev/articles/ttfb" }
]
}
}

View File

@ -4,4 +4,7 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
}
})