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 { useDatabaseTraceData } from '../hooks/useDatabaseTraceData'
|
||||||
import { processHTTPRequests } from './httprequestviewer/lib/httpRequestProcessor'
|
import { processHTTPRequests } from './httprequestviewer/lib/httpRequestProcessor'
|
||||||
import { addRequestPostProcessing } from './httprequestviewer/lib/requestPostProcessor'
|
import { addRequestPostProcessing } from './httprequestviewer/lib/requestPostProcessor'
|
||||||
import { analyzeCDN, analyzeQueueReason } from './httprequestviewer/lib/analysisUtils'
|
import { analyzeCDN, analyzeQueueReason } from './httprequestviewer/lib/analysisUtils'
|
||||||
import { assignConnectionNumbers } from './httprequestviewer/lib/connectionUtils'
|
import { assignConnectionNumbers } from './httprequestviewer/lib/connectionUtils'
|
||||||
import sortRequests from './httprequestviewer/lib/sortRequests'
|
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'
|
import styles from './RequestBreakdown.module.css'
|
||||||
|
|
||||||
interface RequestBreakdownProps {
|
interface RequestBreakdownProps {
|
||||||
@ -21,20 +24,9 @@ interface BreakdownStats {
|
|||||||
cacheHitRate: number
|
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 RequestBreakdown: React.FC<RequestBreakdownProps> = ({ traceId }) => {
|
||||||
const { traceData, loading, error } = useDatabaseTraceData(traceId)
|
const { traceData, loading, error } = useDatabaseTraceData(traceId)
|
||||||
const [showAllHostnames, setShowAllHostnames] = useState(false)
|
|
||||||
|
|
||||||
const httpRequests = useMemo(() => {
|
const httpRequests = useMemo(() => {
|
||||||
if (!traceData) return []
|
if (!traceData) return []
|
||||||
@ -85,143 +77,6 @@ const RequestBreakdown: React.FC<RequestBreakdownProps> = ({ traceId }) => {
|
|||||||
}
|
}
|
||||||
}, [httpRequests])
|
}, [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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@ -247,212 +102,17 @@ const RequestBreakdown: React.FC<RequestBreakdownProps> = ({ traceId }) => {
|
|||||||
<h2>Request Data Breakdown</h2>
|
<h2>Request Data Breakdown</h2>
|
||||||
|
|
||||||
{/* Overall Stats */}
|
{/* Overall Stats */}
|
||||||
<div className={styles.statsGrid}>
|
<RequestDataSummary
|
||||||
<div className={styles.statCard}>
|
totalRequests={overallStats.totalRequests}
|
||||||
<h3>Total Requests</h3>
|
totalSize={overallStats.totalSize}
|
||||||
<div className={styles.statValue}>{overallStats.totalRequests}</div>
|
averageResponseTime={overallStats.averageResponseTime}
|
||||||
</div>
|
successRate={overallStats.successRate}
|
||||||
<div className={styles.statCard}>
|
cacheHitRate={overallStats.cacheHitRate}
|
||||||
<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 */}
|
<ResourceTypeBreakdown httpRequests={httpRequests} />
|
||||||
<div className={styles.breakdownSection}>
|
<StatusCodeBreakdown httpRequests={httpRequests} />
|
||||||
<h3>By Resource Type</h3>
|
<HostnameBreakdown httpRequests={httpRequests} />
|
||||||
<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>
|
|
||||||
</div>
|
</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