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>
206 lines
6.0 KiB
TypeScript
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()
|
|
} |