Compare commits

..

No commits in common. "63550a42b464dfdc4ff29db5764dace344de340f" and "ec91cfbafd971dfb27268fa0c2ba3491e8d1d06d" have entirely different histories.

11 changed files with 91 additions and 1048 deletions

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect } from 'react'
import './App.css' import './App.css'
import TraceViewer from './components/TraceViewer' import TraceViewer from './components/TraceViewer'
import PhaseViewer from './components/PhaseViewer' import PhaseViewer from './components/PhaseViewer'
@ -11,83 +11,20 @@ import { useDatabaseTraceData } from './hooks/useDatabaseTraceData'
type AppView = 'trace' | 'phases' | 'http' | 'debug' type AppView = 'trace' | 'phases' | 'http' | 'debug'
type AppMode = 'selector' | 'upload' | 'analysis' 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() { function App() {
const [mode, setMode] = useState<AppMode>('selector') const [mode, setMode] = useState<AppMode>('selector')
const [currentView, setCurrentView] = useState<AppView>('http') const [currentView, setCurrentView] = useState<AppView>('http')
const [selectedTraceId, setSelectedTraceId] = useState<string | null>(null) const [selectedTraceId, setSelectedTraceId] = useState<string | null>(null)
const [, setHasTraces] = useState<boolean>(false) const [hasTraces, setHasTraces] = useState<boolean>(false)
const [dbInitialized, setDbInitialized] = useState(false) const [dbInitialized, setDbInitialized] = useState(false)
// Always call hooks at the top level // Always call hooks at the top level
const { traceData } = useDatabaseTraceData(selectedTraceId) 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(() => { useEffect(() => {
initializeApp() initializeApp()
}, [])
// Listen for browser navigation (back/forward)
window.addEventListener('popstate', handlePopState)
return () => window.removeEventListener('popstate', handlePopState)
}, [handlePopState])
const initializeApp = async () => { const initializeApp = async () => {
try { try {
@ -99,23 +36,10 @@ function App() {
const traces = await traceDatabase.getAllTraces() const traces = await traceDatabase.getAllTraces()
setHasTraces(traces.length > 0) setHasTraces(traces.length > 0)
// Check URL for existing trace parameter // If no traces, show upload screen
const { traceId, view } = getUrlParams() if (traces.length === 0) {
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') setMode('upload')
} else { } else {
// Has traces, show selector
setMode('selector') setMode('selector')
} }
} catch (error) { } catch (error) {
@ -127,23 +51,18 @@ function App() {
const handleTraceSelect = (traceId: string) => { const handleTraceSelect = (traceId: string) => {
setSelectedTraceId(traceId) setSelectedTraceId(traceId)
setCurrentView('http') // Default to HTTP view
setMode('analysis') setMode('analysis')
updateUrlWithTraceId(traceId, 'http', null)
} }
const handleUploadSuccess = (traceId: string) => { const handleUploadSuccess = (traceId: string) => {
setSelectedTraceId(traceId) setSelectedTraceId(traceId)
setCurrentView('http')
setMode('analysis') setMode('analysis')
setHasTraces(true) setHasTraces(true)
updateUrlWithTraceId(traceId, 'http', null)
} }
const handleBackToSelector = () => { const handleBackToSelector = () => {
setSelectedTraceId(null) setSelectedTraceId(null)
setMode('selector') setMode('selector')
window.history.pushState({}, '', '/')
} }
const handleUploadNew = () => { const handleUploadNew = () => {
@ -209,10 +128,7 @@ function App() {
<nav style={{ display: 'flex', gap: '10px' }}> <nav style={{ display: 'flex', gap: '10px' }}>
<button <button
onClick={() => { onClick={() => setCurrentView('trace')}
setCurrentView('trace')
updateUrlWithTraceId(selectedTraceId, 'trace', null)
}}
style={{ style={{
background: currentView === 'trace' ? '#007bff' : '#6c757d', background: currentView === 'trace' ? '#007bff' : '#6c757d',
color: 'white', color: 'white',
@ -226,10 +142,7 @@ function App() {
Trace Stats Trace Stats
</button> </button>
<button <button
onClick={() => { onClick={() => setCurrentView('phases')}
setCurrentView('phases')
updateUrlWithTraceId(selectedTraceId, 'phases', null)
}}
style={{ style={{
background: currentView === 'phases' ? '#007bff' : '#6c757d', background: currentView === 'phases' ? '#007bff' : '#6c757d',
color: 'white', color: 'white',
@ -243,10 +156,7 @@ function App() {
Phase Events Phase Events
</button> </button>
<button <button
onClick={() => { onClick={() => setCurrentView('http')}
setCurrentView('http')
updateUrlWithTraceId(selectedTraceId, 'http', null)
}}
style={{ style={{
background: currentView === 'http' ? '#007bff' : '#6c757d', background: currentView === 'http' ? '#007bff' : '#6c757d',
color: 'white', color: 'white',
@ -260,10 +170,7 @@ function App() {
HTTP Requests HTTP Requests
</button> </button>
<button <button
onClick={() => { onClick={() => setCurrentView('debug')}
setCurrentView('debug')
updateUrlWithTraceId(selectedTraceId, 'debug', null)
}}
style={{ style={{
background: currentView === 'debug' ? '#007bff' : '#6c757d', background: currentView === 'debug' ? '#007bff' : '#6c757d',
color: 'white', color: 'white',

View File

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

View File

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

View File

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

View File

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

View File

@ -1,49 +1,11 @@
import { useState, useMemo, useEffect, lazy, Suspense } from 'react' import { useState, useMemo, useEffect } from 'react'
import { useDatabaseTraceData } from '../../hooks/useDatabaseTraceData' import { useDatabaseTraceData } from '../../hooks/useDatabaseTraceData'
import { getUrlParams, updateUrlWithTraceId } from '../../App' import BabylonViewer from '../../BabylonViewer'
import BabylonTimelineViewer from '../../BabylonTimelineViewer'
import RequestFilters from './RequestFilters' import RequestFilters from './RequestFilters'
import RequestsTable from './RequestsTable' import RequestsTable from './RequestsTable'
import styles from './HTTPRequestViewer.module.css' 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 // Imported utilities
import { ITEMS_PER_PAGE, SSIM_SIMILARITY_THRESHOLD } from './lib/httpRequestConstants' import { ITEMS_PER_PAGE, SSIM_SIMILARITY_THRESHOLD } from './lib/httpRequestConstants'
import { extractScreenshots, findUniqueScreenshots } from './lib/screenshotUtils' import { extractScreenshots, findUniqueScreenshots } from './lib/screenshotUtils'
@ -71,42 +33,8 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
const [priorityFilter, setPriorityFilter] = useState<string>('all') const [priorityFilter, setPriorityFilter] = useState<string>('all')
const [showQueueAnalysis, setShowQueueAnalysis] = useState(false) const [showQueueAnalysis, setShowQueueAnalysis] = useState(false)
const [showScreenshots, setShowScreenshots] = useState(false) const [showScreenshots, setShowScreenshots] = useState(false)
// 3D viewer state - initialized from URL
const [show3DViewer, setShow3DViewer] = useState(false) const [show3DViewer, setShow3DViewer] = useState(false)
const [showTimelineViewer, setShowTimelineViewer] = 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 [ssimThreshold, setSsimThreshold] = useState(SSIM_SIMILARITY_THRESHOLD)
const [pendingSSIMThreshold, setPendingSSIMThreshold] = useState(SSIM_SIMILARITY_THRESHOLD) const [pendingSSIMThreshold, setPendingSSIMThreshold] = useState(SSIM_SIMILARITY_THRESHOLD)
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set()) const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set())
@ -336,8 +264,8 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
setCurrentPage={setCurrentPage} setCurrentPage={setCurrentPage}
setShowQueueAnalysis={setShowQueueAnalysis} setShowQueueAnalysis={setShowQueueAnalysis}
setShowScreenshots={setShowScreenshots} setShowScreenshots={setShowScreenshots}
setShow3DViewer={handle3DViewerToggle} setShow3DViewer={setShow3DViewer}
setShowTimelineViewer={handleTimelineViewerToggle} setShowTimelineViewer={setShowTimelineViewer}
setPendingSSIMThreshold={setPendingSSIMThreshold} setPendingSSIMThreshold={setPendingSSIMThreshold}
handleSSIMRecalculate={handleSSIMRecalculate} handleSSIMRecalculate={handleSSIMRecalculate}
/> />
@ -376,16 +304,14 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
3D Network Visualization ({filteredRequests.length} requests) 3D Network Visualization ({filteredRequests.length} requests)
</h3> </h3>
<button <button
onClick={() => handle3DViewerToggle(false)} onClick={() => setShow3DViewer(false)}
className={styles.modalCloseButton} className={styles.modalCloseButton}
> >
Close Close
</button> </button>
</div> </div>
<div className={styles.modalContent}> <div className={styles.modalContent}>
<Suspense fallback={<ThreeDViewerLoading />}> <BabylonViewer httpRequests={filteredRequests} width={window.innerWidth * 0.9} height={window.innerHeight * 0.9 - 100} />
<BabylonViewer httpRequests={filteredRequests} width={window.innerWidth * 0.9} height={window.innerHeight * 0.9 - 100} />
</Suspense>
</div> </div>
<div className={styles.modalLegend}> <div className={styles.modalLegend}>
<div><strong>Legend:</strong></div> <div><strong>Legend:</strong></div>
@ -417,16 +343,14 @@ export default function HTTPRequestViewer({ traceId }: HTTPRequestViewerProps) {
3D Timeline Visualization ({filteredRequests.length} requests) 3D Timeline Visualization ({filteredRequests.length} requests)
</h3> </h3>
<button <button
onClick={() => handleTimelineViewerToggle(false)} onClick={() => setShowTimelineViewer(false)}
className={styles.modalCloseButton} className={styles.modalCloseButton}
> >
Close Close
</button> </button>
</div> </div>
<div className={styles.modalContent}> <div className={styles.modalContent}>
<Suspense fallback={<ThreeDViewerLoading />}> <BabylonTimelineViewer httpRequests={filteredRequests} screenshots={screenshots} width={window.innerWidth * 0.9} height={window.innerHeight * 0.9 - 100} />
<BabylonTimelineViewer httpRequests={filteredRequests} screenshots={screenshots} width={window.innerWidth * 0.9} height={window.innerHeight * 0.9 - 100} />
</Suspense>
</div> </div>
<div className={styles.modalLegend}> <div className={styles.modalLegend}>
<div><strong>Legend:</strong></div> <div><strong>Legend:</strong></div>

View File

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

View File

@ -1,119 +0,0 @@
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

@ -1,215 +0,0 @@
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

@ -1,345 +0,0 @@
// 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,7 +4,4 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: {
host: '0.0.0.0',
}
}) })