perfViz/src/App.tsx
Michael Mainguy 33fb2b1674 Add Request Breakdown page with comprehensive data analysis
Introduces new breakdown page that provides detailed statistics and visualizations for HTTP request data analysis across multiple dimensions.

Features:
- Overall statistics dashboard (total requests, size, success rate, cache hit rate)
- Resource type breakdown with visual progress bars
- Status code analysis by HTTP status categories
- Hostname breakdown showing top 10 domains
- Total and average response time metrics
- Responsive design with mobile-friendly layouts
- Added to main navigation as "Request Breakdown" tab

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-19 07:52:22 -05:00

283 lines
9.4 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react'
import styles from './App.module.css'
import TraceViewer from './components/TraceViewer'
import PhaseViewer from './components/PhaseViewer'
import HTTPRequestViewer from './components/httprequestviewer/HTTPRequestViewer'
import JavaScriptViewer from './components/javascriptviewer/JavaScriptViewer'
import RequestBreakdown from './components/RequestBreakdown'
import RequestDebugger from './components/RequestDebugger'
import TraceUpload from './components/TraceUpload'
import TraceSelector from './components/TraceSelector'
import { traceDatabase } from './utils/traceDatabase'
import { useDatabaseTraceData } from './hooks/useDatabaseTraceData'
type AppView = 'trace' | 'phases' | 'http' | 'js' | 'breakdown' | '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', 'js', 'breakdown', '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', 'js', 'breakdown', '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 [, 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 {
// Initialize the database
await traceDatabase.init()
setDbInitialized(true)
// Check if we have any traces
const traces = await traceDatabase.getAllTraces()
setHasTraces(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) {
console.error('Failed to initialize database:', error)
setMode('upload') // Fallback to upload mode
setDbInitialized(true)
}
}
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 = () => {
setMode('upload')
}
if (!dbInitialized) {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
fontFamily: 'system-ui, sans-serif'
}}>
<div>Initializing database...</div>
</div>
)
}
if (mode === 'upload') {
return <TraceUpload onUploadSuccess={handleUploadSuccess} />
}
if (mode === 'selector') {
return (
<TraceSelector
onTraceSelect={handleTraceSelect}
onUploadNew={handleUploadNew}
/>
)
}
// Analysis mode - show the main interface
return (
<>
<div className={styles.mainApp}>
<div className={styles.appHeader}>
<div className={styles.headerLeft}>
<button
className={styles.backButton}
onClick={handleBackToSelector}
>
Back to Traces
</button>
<h1 className={styles.appTitle}>Performance Trace Analysis</h1>
</div>
<nav className={styles.nav}>
<button
className={`${styles.navButton} ${currentView === 'trace' ? styles.active : ''}`}
onClick={() => {
setCurrentView('trace')
updateUrlWithTraceId(selectedTraceId, 'trace', null)
}}
>
Trace Stats
</button>
<button
className={`${styles.navButton} ${currentView === 'phases' ? styles.active : ''}`}
onClick={() => {
setCurrentView('phases')
updateUrlWithTraceId(selectedTraceId, 'phases', null)
}}
>
Phase Events
</button>
<button
className={`${styles.navButton} ${currentView === 'http' ? styles.active : ''}`}
onClick={() => {
setCurrentView('http')
updateUrlWithTraceId(selectedTraceId, 'http', null)
}}
>
HTTP Requests
</button>
<button
className={`${styles.navButton} ${currentView === 'js' ? styles.active : ''}`}
onClick={() => {
setCurrentView('js')
updateUrlWithTraceId(selectedTraceId, 'js', null)
}}
>
JavaScript
</button>
<button
className={`${styles.navButton} ${currentView === 'breakdown' ? styles.active : ''}`}
onClick={() => {
setCurrentView('breakdown')
updateUrlWithTraceId(selectedTraceId, 'breakdown', null)
}}
>
Request Breakdown
</button>
<button
className={`${styles.navButton} ${currentView === 'debug' ? styles.active : ''}`}
onClick={() => {
setCurrentView('debug')
updateUrlWithTraceId(selectedTraceId, 'debug', null)
}}
>
Request Debug
</button>
</nav>
</div>
{currentView === 'trace' && (
<TraceViewer traceId={selectedTraceId} />
)}
{currentView === 'phases' && (
<PhaseViewer traceId={selectedTraceId} />
)}
{currentView === 'http' && (
<HTTPRequestViewer traceId={selectedTraceId} />
)}
{currentView === 'js' && (
<JavaScriptViewer traceId={selectedTraceId} />
)}
{currentView === 'breakdown' && (
<RequestBreakdown traceId={selectedTraceId} />
)}
{currentView === 'debug' && traceData && (
<RequestDebugger traceEvents={traceData.traceEvents} />
)}
</div>
</>
)
}
export default App