perfViz/src/components/httprequestviewer/lib/csvExport.ts
Michael Mainguy 327ef29d55 Fix CSV export timing columns and add units to headers
Corrects CSV export to properly include timing data by accessing nested timing properties and calculating derived values like DNS time, connection time, and data rate. Headers now include units (μs, bytes, bytes/sec) for clarity.

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

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

206 lines
6.0 KiB
TypeScript

import type { HTTPRequest } from '../types/httpRequest'
export interface CSVExportOptions {
includeHeaders?: boolean
selectedColumns?: string[]
}
const DEFAULT_COLUMNS = [
'method',
'url',
'hostname',
'statusCode',
'resourceType',
'priority',
'protocol',
'connectionNumber',
'requestNumberOnConnection',
'timing.start',
'timing.queueTime',
'dnsTime',
'connectionTime',
'timing.serverLatency',
'timing.duration',
'timing.totalResponseTime',
'dataRate',
'size',
'contentLength',
'fromCache',
'connectionReused',
'cdnAnalysis.provider',
'queueAnalysis.reason'
]
// Format a value for CSV output
const formatCSVValue = (value: any): string => {
if (value === null || value === undefined) {
return ''
}
if (typeof value === 'number') {
return value.toString()
}
if (typeof value === 'boolean') {
return value ? 'true' : 'false'
}
const stringValue = value.toString()
// Escape quotes and wrap in quotes if contains comma, quote, or newline
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
return `"${stringValue.replace(/"/g, '""')}"`
}
return stringValue
}
// Calculate derived values for CSV export
const calculateDerivedValue = (request: HTTPRequest, path: string): any => {
switch (path) {
case 'dnsTime':
if (request.timing.dnsStart !== undefined && request.timing.dnsEnd !== undefined &&
request.timing.dnsStart >= 0 && request.timing.dnsEnd >= 0) {
return request.timing.dnsEnd - request.timing.dnsStart
}
return null
case 'connectionTime':
if (request.timing.connectStart !== undefined && request.timing.connectEnd !== undefined &&
request.timing.connectStart >= 0 && request.timing.connectEnd >= 0) {
return request.timing.connectEnd - request.timing.connectStart
}
return null
case 'dataRate':
if (!request.timing.duration || request.timing.duration <= 0) return null
const bytes = request.contentLength && request.contentLength > 0 ? request.contentLength : request.encodedDataLength
if (!bytes) return null
const durationSeconds = request.timing.duration / 1000000
return bytes / durationSeconds // bytes per second
case 'size':
return request.encodedDataLength || request.contentLength || null
default:
return getNestedValue(request, path)
}
}
// Get nested property value from object
const getNestedValue = (obj: any, path: string): any => {
return path.split('.').reduce((current, key) => {
return current && current[key] !== undefined ? current[key] : null
}, obj)
}
// Convert HTTP requests to CSV format
export const exportRequestsToCSV = (
requests: HTTPRequest[],
options: CSVExportOptions = {}
): string => {
const {
includeHeaders = true,
selectedColumns = DEFAULT_COLUMNS
} = options
if (requests.length === 0) {
return includeHeaders ? selectedColumns.join(',') + '\n' : ''
}
const lines: string[] = []
// Add headers if requested
if (includeHeaders) {
const headers = selectedColumns.map(col => {
// Add units to timing-related headers
switch (col) {
case 'timing.start':
return 'Start Time (μs)'
case 'timing.queueTime':
return 'Queue Time (μs)'
case 'dnsTime':
return 'DNS Time (μs)'
case 'connectionTime':
return 'Connection Time (μs)'
case 'timing.serverLatency':
return 'Server Latency (μs)'
case 'timing.duration':
return 'Duration (μs)'
case 'timing.totalResponseTime':
return 'Total Response Time (μs)'
case 'timing.downloadTime':
return 'Download Time (μs)'
case 'dataRate':
return 'Data Rate (bytes/sec)'
case 'size':
return 'Size (bytes)'
case 'contentLength':
return 'Content Length (bytes)'
case 'encodedDataLength':
return 'Encoded Data Length (bytes)'
default:
// Clean up column names for other headers
return col
.replace(/([A-Z])/g, ' $1') // Add space before capitals
.replace(/^\w/, c => c.toUpperCase()) // Capitalize first letter
.replace(/\./g, ' ') // Replace dots with spaces
.trim()
}
})
lines.push(headers.map(formatCSVValue).join(','))
}
// Add data rows
for (const request of requests) {
const values = selectedColumns.map(column => {
const value = calculateDerivedValue(request, column)
return formatCSVValue(value)
})
lines.push(values.join(','))
}
return lines.join('\n')
}
// Download CSV file
export const downloadCSV = (csvContent: string, filename: string = 'http-requests.csv'): void => {
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
if (link.download !== undefined) {
// Use HTML5 download attribute
const url = URL.createObjectURL(blob)
link.setAttribute('href', url)
link.setAttribute('download', filename)
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
}
// Get all available columns from HTTP requests
export const getAvailableColumns = (requests: HTTPRequest[]): string[] => {
if (requests.length === 0) {
return DEFAULT_COLUMNS
}
const columns = new Set<string>()
const extractColumns = (obj: any, prefix = ''): void => {
Object.keys(obj).forEach(key => {
const fullKey = prefix ? `${prefix}.${key}` : key
const value = obj[key]
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
// Recursively extract nested properties
extractColumns(value, fullKey)
} else {
columns.add(fullKey)
}
})
}
// Extract columns from first request
extractColumns(requests[0])
return Array.from(columns).sort()
}