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>
283 lines
9.4 KiB
TypeScript
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
|