Compare commits
4 Commits
ec91cfbafd
...
63550a42b4
Author | SHA1 | Date | |
---|---|---|---|
63550a42b4 | |||
359e8a1bd3 | |||
2e533925a2 | |||
aa6e29fb0c |
111
src/App.tsx
111
src/App.tsx
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
119
src/components/shared/Modal.tsx
Normal file
119
src/components/shared/Modal.tsx
Normal 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>
|
||||
)
|
||||
}
|
215
src/components/shared/Tooltip.tsx
Normal file
215
src/components/shared/Tooltip.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
345
src/components/shared/tooltipDefinitions.ts
Normal file
345
src/components/shared/tooltipDefinitions.ts
Normal 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" }
|
||||
]
|
||||
}
|
||||
}
|
@ -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',
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user