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>
This commit is contained in:
parent
327ef29d55
commit
33fb2b1674
20
src/App.tsx
20
src/App.tsx
@ -4,13 +4,14 @@ import TraceViewer from './components/TraceViewer'
|
|||||||
import PhaseViewer from './components/PhaseViewer'
|
import PhaseViewer from './components/PhaseViewer'
|
||||||
import HTTPRequestViewer from './components/httprequestviewer/HTTPRequestViewer'
|
import HTTPRequestViewer from './components/httprequestviewer/HTTPRequestViewer'
|
||||||
import JavaScriptViewer from './components/javascriptviewer/JavaScriptViewer'
|
import JavaScriptViewer from './components/javascriptviewer/JavaScriptViewer'
|
||||||
|
import RequestBreakdown from './components/RequestBreakdown'
|
||||||
import RequestDebugger from './components/RequestDebugger'
|
import RequestDebugger from './components/RequestDebugger'
|
||||||
import TraceUpload from './components/TraceUpload'
|
import TraceUpload from './components/TraceUpload'
|
||||||
import TraceSelector from './components/TraceSelector'
|
import TraceSelector from './components/TraceSelector'
|
||||||
import { traceDatabase } from './utils/traceDatabase'
|
import { traceDatabase } from './utils/traceDatabase'
|
||||||
import { useDatabaseTraceData } from './hooks/useDatabaseTraceData'
|
import { useDatabaseTraceData } from './hooks/useDatabaseTraceData'
|
||||||
|
|
||||||
type AppView = 'trace' | 'phases' | 'http' | 'js' | 'debug'
|
type AppView = 'trace' | 'phases' | 'http' | 'js' | 'breakdown' | 'debug'
|
||||||
type AppMode = 'selector' | 'upload' | 'analysis'
|
type AppMode = 'selector' | 'upload' | 'analysis'
|
||||||
type ThreeDView = 'network' | 'timeline' | null
|
type ThreeDView = 'network' | 'timeline' | null
|
||||||
|
|
||||||
@ -34,7 +35,7 @@ const getUrlParams = () => {
|
|||||||
const view = segments[1] as AppView
|
const view = segments[1] as AppView
|
||||||
const threeDView = segments[2] as ThreeDView
|
const threeDView = segments[2] as ThreeDView
|
||||||
// Validate view and 3D view values
|
// Validate view and 3D view values
|
||||||
const validViews: AppView[] = ['trace', 'phases', 'http', 'js', 'debug']
|
const validViews: AppView[] = ['trace', 'phases', 'http', 'js', 'breakdown', 'debug']
|
||||||
const validThreeDViews: (ThreeDView)[] = ['network', 'timeline']
|
const validThreeDViews: (ThreeDView)[] = ['network', 'timeline']
|
||||||
const validatedView = validViews.includes(view) ? view : 'http'
|
const validatedView = validViews.includes(view) ? view : 'http'
|
||||||
const validatedThreeDView = validThreeDViews.includes(threeDView) ? threeDView : null
|
const validatedThreeDView = validThreeDViews.includes(threeDView) ? threeDView : null
|
||||||
@ -43,7 +44,7 @@ const getUrlParams = () => {
|
|||||||
const traceId = segments[0]
|
const traceId = segments[0]
|
||||||
const view = segments[1] as AppView
|
const view = segments[1] as AppView
|
||||||
// Validate view is one of the allowed values
|
// Validate view is one of the allowed values
|
||||||
const validViews: AppView[] = ['trace', 'phases', 'http', 'js', 'debug']
|
const validViews: AppView[] = ['trace', 'phases', 'http', 'js', 'breakdown', 'debug']
|
||||||
const validatedView = validViews.includes(view) ? view : 'http'
|
const validatedView = validViews.includes(view) ? view : 'http'
|
||||||
return { traceId, view: validatedView, threeDView: null }
|
return { traceId, view: validatedView, threeDView: null }
|
||||||
} else if (segments.length === 1) {
|
} else if (segments.length === 1) {
|
||||||
@ -229,6 +230,15 @@ function App() {
|
|||||||
>
|
>
|
||||||
JavaScript
|
JavaScript
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.navButton} ${currentView === 'breakdown' ? styles.active : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentView('breakdown')
|
||||||
|
updateUrlWithTraceId(selectedTraceId, 'breakdown', null)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Request Breakdown
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`${styles.navButton} ${currentView === 'debug' ? styles.active : ''}`}
|
className={`${styles.navButton} ${currentView === 'debug' ? styles.active : ''}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -257,6 +267,10 @@ function App() {
|
|||||||
<JavaScriptViewer traceId={selectedTraceId} />
|
<JavaScriptViewer traceId={selectedTraceId} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{currentView === 'breakdown' && (
|
||||||
|
<RequestBreakdown traceId={selectedTraceId} />
|
||||||
|
)}
|
||||||
|
|
||||||
{currentView === 'debug' && traceData && (
|
{currentView === 'debug' && traceData && (
|
||||||
<RequestDebugger traceEvents={traceData.traceEvents} />
|
<RequestDebugger traceEvents={traceData.traceEvents} />
|
||||||
)}
|
)}
|
||||||
|
234
src/components/RequestBreakdown.module.css
Normal file
234
src/components/RequestBreakdown.module.css
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
.container {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container h2 {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading, .error {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 400px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fee;
|
||||||
|
border: 1px solid #fcc;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error h3 {
|
||||||
|
color: #c33;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overall Stats Grid */
|
||||||
|
.statsGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statCard {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statCard:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statCard h3 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.9;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statValue {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Breakdown Sections */
|
||||||
|
.breakdownSection {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breakdownSection h3 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 2px solid #e0e0e0;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breakdownTable {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableHeader {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr 1fr 1.5fr 1.5fr 1.5fr;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #555;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableRow {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr 1fr 1.5fr 1.5fr 1.5fr;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableRow:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableRow:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categoryName {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.percentageCell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBar {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressFill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #4CAF50 0%, #45a049 100%);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moreInfo {
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statsGrid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statCard {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statValue {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableHeader, .tableRow {
|
||||||
|
grid-template-columns: 1.5fr 0.8fr 0.8fr 1fr 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breakdownSection h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.statsGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableHeader, .tableRow {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableHeader span, .tableRow span, .tableRow .percentageCell {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableHeader span:before {
|
||||||
|
content: attr(data-label) ": ";
|
||||||
|
font-weight: 700;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableRow span:before {
|
||||||
|
content: attr(data-label) ": ";
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categoryName:before {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.percentageCell {
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBar {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
}
|
344
src/components/RequestBreakdown.tsx
Normal file
344
src/components/RequestBreakdown.tsx
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import { useDatabaseTraceData } from '../hooks/useDatabaseTraceData'
|
||||||
|
import { processHTTPRequests } from './httprequestviewer/lib/httpRequestProcessor'
|
||||||
|
import { addRequestPostProcessing } from './httprequestviewer/lib/requestPostProcessor'
|
||||||
|
import { analyzeCDN, analyzeQueueReason } from './httprequestviewer/lib/analysisUtils'
|
||||||
|
import { assignConnectionNumbers } from './httprequestviewer/lib/connectionUtils'
|
||||||
|
import sortRequests from './httprequestviewer/lib/sortRequests'
|
||||||
|
import type { HTTPRequest } from './httprequestviewer/types/httpRequest'
|
||||||
|
import styles from './RequestBreakdown.module.css'
|
||||||
|
|
||||||
|
interface RequestBreakdownProps {
|
||||||
|
traceId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BreakdownStats {
|
||||||
|
totalRequests: number
|
||||||
|
totalSize: number
|
||||||
|
totalDuration: number
|
||||||
|
averageResponseTime: number
|
||||||
|
successRate: number
|
||||||
|
cacheHitRate: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategoryBreakdown {
|
||||||
|
name: string
|
||||||
|
count: number
|
||||||
|
percentage: number
|
||||||
|
totalSize: number
|
||||||
|
averageResponseTime: number
|
||||||
|
totalResponseTime: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const RequestBreakdown: React.FC<RequestBreakdownProps> = ({ traceId }) => {
|
||||||
|
const { traceData, loading, error } = useDatabaseTraceData(traceId)
|
||||||
|
|
||||||
|
const httpRequests = useMemo(() => {
|
||||||
|
if (!traceData) return []
|
||||||
|
const requests = processHTTPRequests(traceData.traceEvents)
|
||||||
|
const sortedRequests = sortRequests(requests)
|
||||||
|
const processedRequests = addRequestPostProcessing(sortedRequests, analyzeQueueReason, analyzeCDN)
|
||||||
|
return assignConnectionNumbers(processedRequests)
|
||||||
|
}, [traceData])
|
||||||
|
|
||||||
|
const overallStats: BreakdownStats = useMemo(() => {
|
||||||
|
if (httpRequests.length === 0) {
|
||||||
|
return {
|
||||||
|
totalRequests: 0,
|
||||||
|
totalSize: 0,
|
||||||
|
totalDuration: 0,
|
||||||
|
averageResponseTime: 0,
|
||||||
|
successRate: 0,
|
||||||
|
cacheHitRate: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSize = httpRequests.reduce((sum, req) => {
|
||||||
|
const size = req.contentLength || req.encodedDataLength || 0
|
||||||
|
return sum + size
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const totalDuration = httpRequests.reduce((sum, req) => {
|
||||||
|
return sum + (req.timing.duration || 0)
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const totalResponseTime = httpRequests.reduce((sum, req) => {
|
||||||
|
return sum + (req.timing.totalResponseTime || 0)
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const successfulRequests = httpRequests.filter(req =>
|
||||||
|
req.statusCode && req.statusCode >= 200 && req.statusCode < 300
|
||||||
|
).length
|
||||||
|
|
||||||
|
const cachedRequests = httpRequests.filter(req => req.fromCache).length
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalRequests: httpRequests.length,
|
||||||
|
totalSize,
|
||||||
|
totalDuration,
|
||||||
|
averageResponseTime: totalResponseTime / httpRequests.length,
|
||||||
|
successRate: (successfulRequests / httpRequests.length) * 100,
|
||||||
|
cacheHitRate: (cachedRequests / httpRequests.length) * 100
|
||||||
|
}
|
||||||
|
}, [httpRequests])
|
||||||
|
|
||||||
|
const resourceTypeBreakdown: CategoryBreakdown[] = useMemo(() => {
|
||||||
|
const typeMap = new Map<string, HTTPRequest[]>()
|
||||||
|
|
||||||
|
httpRequests.forEach(req => {
|
||||||
|
const type = req.resourceType || 'unknown'
|
||||||
|
if (!typeMap.has(type)) {
|
||||||
|
typeMap.set(type, [])
|
||||||
|
}
|
||||||
|
typeMap.get(type)!.push(req)
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(typeMap.entries()).map(([type, requests]) => {
|
||||||
|
const totalSize = requests.reduce((sum, req) => {
|
||||||
|
return sum + (req.contentLength || req.encodedDataLength || 0)
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const totalResponseTime = requests.reduce((sum, req) => {
|
||||||
|
return sum + (req.timing.totalResponseTime || 0)
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: type,
|
||||||
|
count: requests.length,
|
||||||
|
percentage: (requests.length / httpRequests.length) * 100,
|
||||||
|
totalSize,
|
||||||
|
averageResponseTime: totalResponseTime / requests.length,
|
||||||
|
totalResponseTime
|
||||||
|
}
|
||||||
|
}).sort((a, b) => b.count - a.count)
|
||||||
|
}, [httpRequests])
|
||||||
|
|
||||||
|
const statusCodeBreakdown: CategoryBreakdown[] = useMemo(() => {
|
||||||
|
const statusMap = new Map<string, HTTPRequest[]>()
|
||||||
|
|
||||||
|
httpRequests.forEach(req => {
|
||||||
|
const status = req.statusCode ? Math.floor(req.statusCode / 100) + 'xx' : 'Unknown'
|
||||||
|
if (!statusMap.has(status)) {
|
||||||
|
statusMap.set(status, [])
|
||||||
|
}
|
||||||
|
statusMap.get(status)!.push(req)
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(statusMap.entries()).map(([status, requests]) => {
|
||||||
|
const totalSize = requests.reduce((sum, req) => {
|
||||||
|
return sum + (req.contentLength || req.encodedDataLength || 0)
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const totalResponseTime = requests.reduce((sum, req) => {
|
||||||
|
return sum + (req.timing.totalResponseTime || 0)
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: status,
|
||||||
|
count: requests.length,
|
||||||
|
percentage: (requests.length / httpRequests.length) * 100,
|
||||||
|
totalSize,
|
||||||
|
averageResponseTime: totalResponseTime / requests.length,
|
||||||
|
totalResponseTime
|
||||||
|
}
|
||||||
|
}).sort((a, b) => b.count - a.count)
|
||||||
|
}, [httpRequests])
|
||||||
|
|
||||||
|
const hostnameBreakdown: CategoryBreakdown[] = useMemo(() => {
|
||||||
|
const hostMap = new Map<string, HTTPRequest[]>()
|
||||||
|
|
||||||
|
httpRequests.forEach(req => {
|
||||||
|
const hostname = req.hostname || 'unknown'
|
||||||
|
if (!hostMap.has(hostname)) {
|
||||||
|
hostMap.set(hostname, [])
|
||||||
|
}
|
||||||
|
hostMap.get(hostname)!.push(req)
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(hostMap.entries()).map(([hostname, requests]) => {
|
||||||
|
const totalSize = requests.reduce((sum, req) => {
|
||||||
|
return sum + (req.contentLength || req.encodedDataLength || 0)
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const totalResponseTime = requests.reduce((sum, req) => {
|
||||||
|
return sum + (req.timing.totalResponseTime || 0)
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: hostname,
|
||||||
|
count: requests.length,
|
||||||
|
percentage: (requests.length / httpRequests.length) * 100,
|
||||||
|
totalSize,
|
||||||
|
averageResponseTime: totalResponseTime / requests.length,
|
||||||
|
totalResponseTime
|
||||||
|
}
|
||||||
|
}).sort((a, b) => b.count - a.count)
|
||||||
|
}, [httpRequests])
|
||||||
|
|
||||||
|
const formatSize = (bytes: number): string => {
|
||||||
|
if (bytes < 1024) return `${bytes} B`
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||||
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDuration = (microseconds: number): string => {
|
||||||
|
if (microseconds < 1000) return `${microseconds.toFixed(0)} μs`
|
||||||
|
if (microseconds < 1000000) return `${(microseconds / 1000).toFixed(1)} ms`
|
||||||
|
return `${(microseconds / 1000000).toFixed(2)} s`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.loading}>Loading request breakdown...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.error}>
|
||||||
|
<h3>Error Loading Data</h3>
|
||||||
|
<p>{error}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<h2>Request Data Breakdown</h2>
|
||||||
|
|
||||||
|
{/* Overall Stats */}
|
||||||
|
<div className={styles.statsGrid}>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<h3>Total Requests</h3>
|
||||||
|
<div className={styles.statValue}>{overallStats.totalRequests}</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<h3>Total Size</h3>
|
||||||
|
<div className={styles.statValue}>{formatSize(overallStats.totalSize)}</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<h3>Average Response Time</h3>
|
||||||
|
<div className={styles.statValue}>{formatDuration(overallStats.averageResponseTime)}</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<h3>Success Rate</h3>
|
||||||
|
<div className={styles.statValue}>{overallStats.successRate.toFixed(1)}%</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statCard}>
|
||||||
|
<h3>Cache Hit Rate</h3>
|
||||||
|
<div className={styles.statValue}>{overallStats.cacheHitRate.toFixed(1)}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Resource Type Breakdown */}
|
||||||
|
<div className={styles.breakdownSection}>
|
||||||
|
<h3>By Resource Type</h3>
|
||||||
|
<div className={styles.breakdownTable}>
|
||||||
|
<div className={styles.tableHeader}>
|
||||||
|
<span>Type</span>
|
||||||
|
<span>Count</span>
|
||||||
|
<span>Percentage</span>
|
||||||
|
<span>Total Size</span>
|
||||||
|
<span>Total Response Time</span>
|
||||||
|
<span>Avg Response Time</span>
|
||||||
|
</div>
|
||||||
|
{resourceTypeBreakdown.map(item => (
|
||||||
|
<div key={item.name} className={styles.tableRow}>
|
||||||
|
<span className={styles.categoryName}>{item.name}</span>
|
||||||
|
<span>{item.count}</span>
|
||||||
|
<div className={styles.percentageCell}>
|
||||||
|
<span>{item.percentage.toFixed(1)}%</span>
|
||||||
|
<div className={styles.progressBar}>
|
||||||
|
<div
|
||||||
|
className={styles.progressFill}
|
||||||
|
style={{ width: `${item.percentage}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>{formatSize(item.totalSize)}</span>
|
||||||
|
<span>{formatDuration(item.totalResponseTime)}</span>
|
||||||
|
<span>{formatDuration(item.averageResponseTime)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Code Breakdown */}
|
||||||
|
<div className={styles.breakdownSection}>
|
||||||
|
<h3>By Status Code</h3>
|
||||||
|
<div className={styles.breakdownTable}>
|
||||||
|
<div className={styles.tableHeader}>
|
||||||
|
<span>Status</span>
|
||||||
|
<span>Count</span>
|
||||||
|
<span>Percentage</span>
|
||||||
|
<span>Total Size</span>
|
||||||
|
<span>Total Response Time</span>
|
||||||
|
<span>Avg Response Time</span>
|
||||||
|
</div>
|
||||||
|
{statusCodeBreakdown.map(item => (
|
||||||
|
<div key={item.name} className={styles.tableRow}>
|
||||||
|
<span className={styles.categoryName}>{item.name}</span>
|
||||||
|
<span>{item.count}</span>
|
||||||
|
<div className={styles.percentageCell}>
|
||||||
|
<span>{item.percentage.toFixed(1)}%</span>
|
||||||
|
<div className={styles.progressBar}>
|
||||||
|
<div
|
||||||
|
className={styles.progressFill}
|
||||||
|
style={{ width: `${item.percentage}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>{formatSize(item.totalSize)}</span>
|
||||||
|
<span>{formatDuration(item.totalResponseTime)}</span>
|
||||||
|
<span>{formatDuration(item.averageResponseTime)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hostname Breakdown */}
|
||||||
|
<div className={styles.breakdownSection}>
|
||||||
|
<h3>By Hostname</h3>
|
||||||
|
<div className={styles.breakdownTable}>
|
||||||
|
<div className={styles.tableHeader}>
|
||||||
|
<span>Hostname</span>
|
||||||
|
<span>Count</span>
|
||||||
|
<span>Percentage</span>
|
||||||
|
<span>Total Size</span>
|
||||||
|
<span>Total Response Time</span>
|
||||||
|
<span>Avg Response Time</span>
|
||||||
|
</div>
|
||||||
|
{hostnameBreakdown.slice(0, 10).map(item => (
|
||||||
|
<div key={item.name} className={styles.tableRow}>
|
||||||
|
<span className={styles.categoryName}>{item.name}</span>
|
||||||
|
<span>{item.count}</span>
|
||||||
|
<div className={styles.percentageCell}>
|
||||||
|
<span>{item.percentage.toFixed(1)}%</span>
|
||||||
|
<div className={styles.progressBar}>
|
||||||
|
<div
|
||||||
|
className={styles.progressFill}
|
||||||
|
style={{ width: `${item.percentage}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>{formatSize(item.totalSize)}</span>
|
||||||
|
<span>{formatDuration(item.totalResponseTime)}</span>
|
||||||
|
<span>{formatDuration(item.averageResponseTime)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{hostnameBreakdown.length > 10 && (
|
||||||
|
<div className={styles.moreInfo}>
|
||||||
|
Showing top 10 of {hostnameBreakdown.length} hostnames
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RequestBreakdown
|
Loading…
Reference in New Issue
Block a user