Refactor Request Breakdown into modular components
Extract breakdown sections into separate components for better maintainability: - ResourceTypeBreakdown: handles resource type analysis and visualization - StatusCodeBreakdown: handles status code analysis and visualization - HostnameBreakdown: handles hostname analysis with toggle functionality - RequestDataSummary: statistics cards component (previously extracted) - BreakdownTableHeader: shared table header component (previously extracted) Main RequestBreakdown component reduced from ~420 to ~90 lines with improved separation of concerns. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a2e161bf2a
commit
52543a5d04
@ -1,11 +1,14 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
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 RequestDataSummary from './RequestBreakdown/RequestDataSummary'
|
||||
import ResourceTypeBreakdown from './RequestBreakdown/ResourceTypeBreakdown'
|
||||
import StatusCodeBreakdown from './RequestBreakdown/StatusCodeBreakdown'
|
||||
import HostnameBreakdown from './RequestBreakdown/HostnameBreakdown'
|
||||
import styles from './RequestBreakdown.module.css'
|
||||
|
||||
interface RequestBreakdownProps {
|
||||
@ -21,20 +24,9 @@ interface BreakdownStats {
|
||||
cacheHitRate: number
|
||||
}
|
||||
|
||||
interface CategoryBreakdown {
|
||||
name: string
|
||||
count: number
|
||||
percentage: number
|
||||
totalSize: number
|
||||
sizePercentage: number
|
||||
totalResponseTime: number
|
||||
responseTimePercentage: number
|
||||
averageResponseTime: number
|
||||
}
|
||||
|
||||
const RequestBreakdown: React.FC<RequestBreakdownProps> = ({ traceId }) => {
|
||||
const { traceData, loading, error } = useDatabaseTraceData(traceId)
|
||||
const [showAllHostnames, setShowAllHostnames] = useState(false)
|
||||
|
||||
const httpRequests = useMemo(() => {
|
||||
if (!traceData) return []
|
||||
@ -85,143 +77,6 @@ const RequestBreakdown: React.FC<RequestBreakdownProps> = ({ traceId }) => {
|
||||
}
|
||||
}, [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)
|
||||
})
|
||||
|
||||
// Calculate total size and response time across all requests for percentage calculations
|
||||
const totalAllSize = httpRequests.reduce((sum, req) => {
|
||||
return sum + (req.contentLength || req.encodedDataLength || 0)
|
||||
}, 0)
|
||||
|
||||
const totalAllResponseTime = httpRequests.reduce((sum, req) => {
|
||||
return sum + (req.timing.totalResponseTime || 0)
|
||||
}, 0)
|
||||
|
||||
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,
|
||||
sizePercentage: totalAllSize > 0 ? (totalSize / totalAllSize) * 100 : 0,
|
||||
totalResponseTime,
|
||||
responseTimePercentage: totalAllResponseTime > 0 ? (totalResponseTime / totalAllResponseTime) * 100 : 0,
|
||||
averageResponseTime: totalResponseTime / requests.length
|
||||
}
|
||||
}).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)
|
||||
})
|
||||
|
||||
// Calculate total size and response time across all requests for percentage calculations
|
||||
const totalAllSize = httpRequests.reduce((sum, req) => {
|
||||
return sum + (req.contentLength || req.encodedDataLength || 0)
|
||||
}, 0)
|
||||
|
||||
const totalAllResponseTime = httpRequests.reduce((sum, req) => {
|
||||
return sum + (req.timing.totalResponseTime || 0)
|
||||
}, 0)
|
||||
|
||||
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,
|
||||
sizePercentage: totalAllSize > 0 ? (totalSize / totalAllSize) * 100 : 0,
|
||||
totalResponseTime,
|
||||
responseTimePercentage: totalAllResponseTime > 0 ? (totalResponseTime / totalAllResponseTime) * 100 : 0,
|
||||
averageResponseTime: totalResponseTime / requests.length
|
||||
}
|
||||
}).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)
|
||||
})
|
||||
|
||||
// Calculate total size and response time across all requests for percentage calculations
|
||||
const totalAllSize = httpRequests.reduce((sum, req) => {
|
||||
return sum + (req.contentLength || req.encodedDataLength || 0)
|
||||
}, 0)
|
||||
|
||||
const totalAllResponseTime = httpRequests.reduce((sum, req) => {
|
||||
return sum + (req.timing.totalResponseTime || 0)
|
||||
}, 0)
|
||||
|
||||
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,
|
||||
sizePercentage: totalAllSize > 0 ? (totalSize / totalAllSize) * 100 : 0,
|
||||
totalResponseTime,
|
||||
responseTimePercentage: totalAllResponseTime > 0 ? (totalResponseTime / totalAllResponseTime) * 100 : 0,
|
||||
averageResponseTime: totalResponseTime / requests.length
|
||||
}
|
||||
}).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 (
|
||||
@ -247,212 +102,17 @@ const RequestBreakdown: React.FC<RequestBreakdownProps> = ({ traceId }) => {
|
||||
<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>
|
||||
<RequestDataSummary
|
||||
totalRequests={overallStats.totalRequests}
|
||||
totalSize={overallStats.totalSize}
|
||||
averageResponseTime={overallStats.averageResponseTime}
|
||||
successRate={overallStats.successRate}
|
||||
cacheHitRate={overallStats.cacheHitRate}
|
||||
/>
|
||||
|
||||
{/* 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>Count %</span>
|
||||
<span>Total Size</span>
|
||||
<span>Size %</span>
|
||||
<span>Total Response Time</span>
|
||||
<span>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>
|
||||
<div className={styles.percentageCell}>
|
||||
<span>{item.sizePercentage.toFixed(1)}%</span>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${item.sizePercentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span>{formatDuration(item.totalResponseTime)}</span>
|
||||
<div className={styles.percentageCell}>
|
||||
<span>{item.responseTimePercentage.toFixed(1)}%</span>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${item.responseTimePercentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<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>Count %</span>
|
||||
<span>Total Size</span>
|
||||
<span>Size %</span>
|
||||
<span>Total Response Time</span>
|
||||
<span>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>
|
||||
<div className={styles.percentageCell}>
|
||||
<span>{item.sizePercentage.toFixed(1)}%</span>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${item.sizePercentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span>{formatDuration(item.totalResponseTime)}</span>
|
||||
<div className={styles.percentageCell}>
|
||||
<span>{item.responseTimePercentage.toFixed(1)}%</span>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${item.responseTimePercentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<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>Count %</span>
|
||||
<span>Total Size</span>
|
||||
<span>Size %</span>
|
||||
<span>Total Response Time</span>
|
||||
<span>Response Time %</span>
|
||||
<span>Avg Response Time</span>
|
||||
</div>
|
||||
{(showAllHostnames ? hostnameBreakdown : 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>
|
||||
<div className={styles.percentageCell}>
|
||||
<span>{item.sizePercentage.toFixed(1)}%</span>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${item.sizePercentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span>{formatDuration(item.totalResponseTime)}</span>
|
||||
<div className={styles.percentageCell}>
|
||||
<span>{item.responseTimePercentage.toFixed(1)}%</span>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${item.responseTimePercentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span>{formatDuration(item.averageResponseTime)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{hostnameBreakdown.length > 10 && (
|
||||
<div className={styles.moreInfo}>
|
||||
{showAllHostnames ? (
|
||||
<>
|
||||
Showing all {hostnameBreakdown.length} hostnames
|
||||
<button
|
||||
onClick={() => setShowAllHostnames(false)}
|
||||
className={styles.toggleButton}
|
||||
>
|
||||
Show Top 10 Only
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Showing top 10 of {hostnameBreakdown.length} hostnames
|
||||
<button
|
||||
onClick={() => setShowAllHostnames(true)}
|
||||
className={styles.toggleButton}
|
||||
>
|
||||
Show All Hostnames
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ResourceTypeBreakdown httpRequests={httpRequests} />
|
||||
<StatusCodeBreakdown httpRequests={httpRequests} />
|
||||
<HostnameBreakdown httpRequests={httpRequests} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
23
src/components/RequestBreakdown/BreakdownTableHeader.tsx
Normal file
23
src/components/RequestBreakdown/BreakdownTableHeader.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import styles from '../RequestBreakdown.module.css'
|
||||
|
||||
interface BreakdownTableHeaderProps {
|
||||
categoryLabel: string
|
||||
}
|
||||
|
||||
const BreakdownTableHeader: React.FC<BreakdownTableHeaderProps> = ({ categoryLabel }) => {
|
||||
return (
|
||||
<div className={styles.tableHeader}>
|
||||
<span>{categoryLabel}</span>
|
||||
<span>Request Count</span>
|
||||
<span>Request Count %</span>
|
||||
<span>Total Size</span>
|
||||
<span>Total Size %</span>
|
||||
<span>Total Response Time</span>
|
||||
<span>Total Response Time %</span>
|
||||
<span>Avg Response Time</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BreakdownTableHeader
|
149
src/components/RequestBreakdown/HostnameBreakdown.tsx
Normal file
149
src/components/RequestBreakdown/HostnameBreakdown.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import type { HTTPRequest } from '../httprequestviewer/types/httpRequest'
|
||||
import BreakdownTableHeader from './BreakdownTableHeader'
|
||||
import styles from '../RequestBreakdown.module.css'
|
||||
|
||||
interface CategoryBreakdown {
|
||||
name: string
|
||||
count: number
|
||||
percentage: number
|
||||
totalSize: number
|
||||
sizePercentage: number
|
||||
totalResponseTime: number
|
||||
responseTimePercentage: number
|
||||
averageResponseTime: number
|
||||
}
|
||||
|
||||
interface HostnameBreakdownProps {
|
||||
httpRequests: HTTPRequest[]
|
||||
}
|
||||
|
||||
const HostnameBreakdown: React.FC<HostnameBreakdownProps> = ({ httpRequests }) => {
|
||||
const [showAllHostnames, setShowAllHostnames] = useState(false)
|
||||
|
||||
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`
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
// Calculate total size and response time across all requests for percentage calculations
|
||||
const totalAllSize = httpRequests.reduce((sum, req) => {
|
||||
return sum + (req.contentLength || req.encodedDataLength || 0)
|
||||
}, 0)
|
||||
|
||||
const totalAllResponseTime = httpRequests.reduce((sum, req) => {
|
||||
return sum + (req.timing.totalResponseTime || 0)
|
||||
}, 0)
|
||||
|
||||
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,
|
||||
sizePercentage: totalAllSize > 0 ? (totalSize / totalAllSize) * 100 : 0,
|
||||
totalResponseTime,
|
||||
responseTimePercentage: totalAllResponseTime > 0 ? (totalResponseTime / totalAllResponseTime) * 100 : 0,
|
||||
averageResponseTime: totalResponseTime / requests.length
|
||||
}
|
||||
}).sort((a, b) => b.count - a.count)
|
||||
}, [httpRequests])
|
||||
|
||||
return (
|
||||
<div className={styles.breakdownSection}>
|
||||
<h3>By Hostname</h3>
|
||||
<div className={styles.breakdownTable}>
|
||||
<BreakdownTableHeader categoryLabel="Hostname" />
|
||||
{(showAllHostnames ? hostnameBreakdown : 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>
|
||||
<div className={styles.percentageCell}>
|
||||
<span>{item.sizePercentage.toFixed(1)}%</span>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${item.sizePercentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span>{formatDuration(item.totalResponseTime)}</span>
|
||||
<div className={styles.percentageCell}>
|
||||
<span>{item.responseTimePercentage.toFixed(1)}%</span>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${item.responseTimePercentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span>{formatDuration(item.averageResponseTime)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{hostnameBreakdown.length > 10 && (
|
||||
<div className={styles.moreInfo}>
|
||||
{showAllHostnames ? (
|
||||
<>
|
||||
Showing all {hostnameBreakdown.length} hostnames
|
||||
<button
|
||||
onClick={() => setShowAllHostnames(false)}
|
||||
className={styles.toggleButton}
|
||||
>
|
||||
Show Top 10 Only
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Showing top 10 of {hostnameBreakdown.length} hostnames
|
||||
<button
|
||||
onClick={() => setShowAllHostnames(true)}
|
||||
className={styles.toggleButton}
|
||||
>
|
||||
Show All Hostnames
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HostnameBreakdown
|
57
src/components/RequestBreakdown/RequestDataSummary.tsx
Normal file
57
src/components/RequestBreakdown/RequestDataSummary.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React from 'react'
|
||||
import styles from '../RequestBreakdown.module.css'
|
||||
|
||||
interface RequestDataSummaryProps {
|
||||
totalRequests: number
|
||||
totalSize: number
|
||||
averageResponseTime: number
|
||||
successRate: number
|
||||
cacheHitRate: number
|
||||
}
|
||||
|
||||
const RequestDataSummary: React.FC<RequestDataSummaryProps> = ({
|
||||
totalRequests,
|
||||
totalSize,
|
||||
averageResponseTime,
|
||||
successRate,
|
||||
cacheHitRate
|
||||
}) => {
|
||||
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`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.statsGrid}>
|
||||
<div className={styles.statCard}>
|
||||
<h3>Total Requests</h3>
|
||||
<div className={styles.statValue}>{totalRequests}</div>
|
||||
</div>
|
||||
<div className={styles.statCard}>
|
||||
<h3>Total Size</h3>
|
||||
<div className={styles.statValue}>{formatSize(totalSize)}</div>
|
||||
</div>
|
||||
<div className={styles.statCard}>
|
||||
<h3>Average Response Time</h3>
|
||||
<div className={styles.statValue}>{formatDuration(averageResponseTime)}</div>
|
||||
</div>
|
||||
<div className={styles.statCard}>
|
||||
<h3>Success Rate</h3>
|
||||
<div className={styles.statValue}>{successRate.toFixed(1)}%</div>
|
||||
</div>
|
||||
<div className={styles.statCard}>
|
||||
<h3>Cache Hit Rate</h3>
|
||||
<div className={styles.statValue}>{cacheHitRate.toFixed(1)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RequestDataSummary
|
122
src/components/RequestBreakdown/ResourceTypeBreakdown.tsx
Normal file
122
src/components/RequestBreakdown/ResourceTypeBreakdown.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import type { HTTPRequest } from '../httprequestviewer/types/httpRequest'
|
||||
import BreakdownTableHeader from './BreakdownTableHeader'
|
||||
import styles from '../RequestBreakdown.module.css'
|
||||
|
||||
interface CategoryBreakdown {
|
||||
name: string
|
||||
count: number
|
||||
percentage: number
|
||||
totalSize: number
|
||||
sizePercentage: number
|
||||
totalResponseTime: number
|
||||
responseTimePercentage: number
|
||||
averageResponseTime: number
|
||||
}
|
||||
|
||||
interface ResourceTypeBreakdownProps {
|
||||
httpRequests: HTTPRequest[]
|
||||
}
|
||||
|
||||
const ResourceTypeBreakdown: React.FC<ResourceTypeBreakdownProps> = ({ 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`
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
// Calculate total size and response time across all requests for percentage calculations
|
||||
const totalAllSize = httpRequests.reduce((sum, req) => {
|
||||
return sum + (req.contentLength || req.encodedDataLength || 0)
|
||||
}, 0)
|
||||
|
||||
const totalAllResponseTime = httpRequests.reduce((sum, req) => {
|
||||
return sum + (req.timing.totalResponseTime || 0)
|
||||
}, 0)
|
||||
|
||||
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,
|
||||
sizePercentage: totalAllSize > 0 ? (totalSize / totalAllSize) * 100 : 0,
|
||||
totalResponseTime,
|
||||
responseTimePercentage: totalAllResponseTime > 0 ? (totalResponseTime / totalAllResponseTime) * 100 : 0,
|
||||
averageResponseTime: totalResponseTime / requests.length
|
||||
}
|
||||
}).sort((a, b) => b.count - a.count)
|
||||
}, [httpRequests])
|
||||
|
||||
return (
|
||||
<div className={styles.breakdownSection}>
|
||||
<h3>By Resource Type</h3>
|
||||
<div className={styles.breakdownTable}>
|
||||
<BreakdownTableHeader categoryLabel="Resource Type" />
|
||||
{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>
|
||||
<div className={styles.percentageCell}>
|
||||
<span>{item.sizePercentage.toFixed(1)}%</span>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${item.sizePercentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span>{formatDuration(item.totalResponseTime)}</span>
|
||||
<div className={styles.percentageCell}>
|
||||
<span>{item.responseTimePercentage.toFixed(1)}%</span>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${item.responseTimePercentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span>{formatDuration(item.averageResponseTime)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResourceTypeBreakdown
|
122
src/components/RequestBreakdown/StatusCodeBreakdown.tsx
Normal file
122
src/components/RequestBreakdown/StatusCodeBreakdown.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import type { HTTPRequest } from '../httprequestviewer/types/httpRequest'
|
||||
import BreakdownTableHeader from './BreakdownTableHeader'
|
||||
import styles from '../RequestBreakdown.module.css'
|
||||
|
||||
interface CategoryBreakdown {
|
||||
name: string
|
||||
count: number
|
||||
percentage: number
|
||||
totalSize: number
|
||||
sizePercentage: number
|
||||
totalResponseTime: number
|
||||
responseTimePercentage: number
|
||||
averageResponseTime: number
|
||||
}
|
||||
|
||||
interface StatusCodeBreakdownProps {
|
||||
httpRequests: HTTPRequest[]
|
||||
}
|
||||
|
||||
const StatusCodeBreakdown: React.FC<StatusCodeBreakdownProps> = ({ 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`
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
// Calculate total size and response time across all requests for percentage calculations
|
||||
const totalAllSize = httpRequests.reduce((sum, req) => {
|
||||
return sum + (req.contentLength || req.encodedDataLength || 0)
|
||||
}, 0)
|
||||
|
||||
const totalAllResponseTime = httpRequests.reduce((sum, req) => {
|
||||
return sum + (req.timing.totalResponseTime || 0)
|
||||
}, 0)
|
||||
|
||||
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,
|
||||
sizePercentage: totalAllSize > 0 ? (totalSize / totalAllSize) * 100 : 0,
|
||||
totalResponseTime,
|
||||
responseTimePercentage: totalAllResponseTime > 0 ? (totalResponseTime / totalAllResponseTime) * 100 : 0,
|
||||
averageResponseTime: totalResponseTime / requests.length
|
||||
}
|
||||
}).sort((a, b) => b.count - a.count)
|
||||
}, [httpRequests])
|
||||
|
||||
return (
|
||||
<div className={styles.breakdownSection}>
|
||||
<h3>By Status Code</h3>
|
||||
<div className={styles.breakdownTable}>
|
||||
<BreakdownTableHeader categoryLabel="Status Code" />
|
||||
{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>
|
||||
<div className={styles.percentageCell}>
|
||||
<span>{item.sizePercentage.toFixed(1)}%</span>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${item.sizePercentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span>{formatDuration(item.totalResponseTime)}</span>
|
||||
<div className={styles.percentageCell}>
|
||||
<span>{item.responseTimePercentage.toFixed(1)}%</span>
|
||||
<div className={styles.progressBar}>
|
||||
<div
|
||||
className={styles.progressFill}
|
||||
style={{ width: `${item.responseTimePercentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<span>{formatDuration(item.averageResponseTime)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusCodeBreakdown
|
Loading…
Reference in New Issue
Block a user