UNPKG

lightweight-browser-load-tester

Version:

A lightweight load testing tool using real browsers for streaming applications with DRM support

536 lines (477 loc) 17.6 kB
import { TestResults, BrowserMetrics, DRMMetrics, NetworkMetrics, ErrorLog, PrometheusConfiguration } from '../types/index'; import { PrometheusExporter } from '../exporters/prometheus-exporter'; /** * Configuration for report generation */ export interface ReportConfig { outputPath?: string; formats: ('html' | 'json' | 'csv')[]; includeDetailedMetrics?: boolean; includeErrorAnalysis?: boolean; } /** * Aggregates test results from multiple browser instances and generates reports */ export class ResultsAggregator { private testResults: TestResults; private startTime: Date; private endTime?: Date; private prometheusExporter?: PrometheusExporter; constructor(prometheusConfig?: PrometheusConfiguration) { this.testResults = { summary: { totalRequests: 0, successfulRequests: 0, failedRequests: 0, averageResponseTime: 0, peakConcurrentUsers: 0, testDuration: 0 }, browserMetrics: [], drmMetrics: [], networkMetrics: [], errors: [] }; this.startTime = new Date(); // Initialize Prometheus exporter if configuration is provided if (prometheusConfig) { this.prometheusExporter = new PrometheusExporter(prometheusConfig); } } /** * Add browser metrics from a single instance */ addBrowserMetrics(metrics: BrowserMetrics): void { this.testResults.browserMetrics.push(metrics); // Export to Prometheus in real-time if configured if (this.prometheusExporter) { this.prometheusExporter.exportBrowserMetrics(metrics).catch(error => { console.error('Failed to export browser metrics to Prometheus:', error); }); } } /** * Add DRM metrics */ addDRMMetrics(metrics: DRMMetrics): void { this.testResults.drmMetrics.push(metrics); // Export to Prometheus in real-time if configured if (this.prometheusExporter) { this.prometheusExporter.exportDRMMetrics(metrics).catch(error => { console.error('Failed to export DRM metrics to Prometheus:', error); }); } } /** * Add network metrics */ addNetworkMetrics(metrics: NetworkMetrics[]): void { this.testResults.networkMetrics.push(...metrics); // Export to Prometheus in real-time if configured if (this.prometheusExporter && metrics.length > 0) { this.prometheusExporter.exportNetworkMetrics(metrics).catch(error => { console.error('Failed to export network metrics to Prometheus:', error); }); } } /** * Add error log entry */ addError(error: ErrorLog): void { this.testResults.errors.push(error); // Export to Prometheus in real-time if configured if (this.prometheusExporter) { this.prometheusExporter.exportErrorMetrics([error]).catch(error => { console.error('Failed to export error metrics to Prometheus:', error); }); } } /** * Finalize test results and calculate summary statistics */ finalize(): void { this.endTime = new Date(); this.calculateSummary(); // Export final summary to Prometheus if configured if (this.prometheusExporter) { this.prometheusExporter.exportTestSummary(this.testResults.summary).catch(error => { console.error('Failed to export test summary to Prometheus:', error); }); } } /** * Get the aggregated test results */ getResults(): TestResults { return { ...this.testResults }; } /** * Shutdown the aggregator and flush any remaining Prometheus metrics */ async shutdown(): Promise<void> { if (this.prometheusExporter) { await this.prometheusExporter.shutdown(); } } /** * Generate reports in specified formats */ async generateReports(config: ReportConfig): Promise<void> { if (!this.endTime) { this.finalize(); } for (const format of config.formats) { switch (format) { case 'json': await this.generateJSONReport(config); break; case 'html': await this.generateHTMLReport(config); break; case 'csv': await this.generateCSVReport(config); break; } } } /** * Calculate summary statistics from collected metrics */ private calculateSummary(): void { const networkMetrics = this.testResults.networkMetrics; const browserMetrics = this.testResults.browserMetrics; // Calculate request statistics this.testResults.summary.totalRequests = networkMetrics.length; this.testResults.summary.successfulRequests = networkMetrics.filter( m => m.statusCode >= 200 && m.statusCode < 400 ).length; this.testResults.summary.failedRequests = this.testResults.summary.totalRequests - this.testResults.summary.successfulRequests; // Calculate average response time if (networkMetrics.length > 0) { this.testResults.summary.averageResponseTime = networkMetrics.reduce((sum, m) => sum + m.responseTime, 0) / networkMetrics.length; } // Calculate peak concurrent users (max active browser instances) this.testResults.summary.peakConcurrentUsers = browserMetrics.length; // Calculate test duration if (this.endTime) { this.testResults.summary.testDuration = (this.endTime.getTime() - this.startTime.getTime()) / 1000; } } /** * Generate JSON report */ private async generateJSONReport(config: ReportConfig): Promise<void> { const fs = await import('fs/promises'); const path = await import('path'); const outputPath = config.outputPath || './test-results'; const filename = path.join(outputPath, `load-test-results-${Date.now()}.json`); await fs.mkdir(outputPath, { recursive: true }); await fs.writeFile(filename, JSON.stringify(this.testResults, null, 2)); } /** * Generate HTML report */ private async generateHTMLReport(config: ReportConfig): Promise<void> { const fs = await import('fs/promises'); const path = await import('path'); const outputPath = config.outputPath || './test-results'; const filename = path.join(outputPath, `load-test-results-${Date.now()}.html`); const html = this.generateHTMLContent(config); await fs.mkdir(outputPath, { recursive: true }); await fs.writeFile(filename, html); } /** * Generate CSV report */ private async generateCSVReport(config: ReportConfig): Promise<void> { const fs = await import('fs/promises'); const path = await import('path'); const outputPath = config.outputPath || './test-results'; const timestamp = Date.now(); await fs.mkdir(outputPath, { recursive: true }); // Generate summary CSV const summaryCSV = this.generateSummaryCSV(); await fs.writeFile( path.join(outputPath, `load-test-summary-${timestamp}.csv`), summaryCSV ); // Generate network metrics CSV const networkCSV = this.generateNetworkMetricsCSV(); await fs.writeFile( path.join(outputPath, `load-test-network-${timestamp}.csv`), networkCSV ); // Generate browser metrics CSV const browserCSV = this.generateBrowserMetricsCSV(); await fs.writeFile( path.join(outputPath, `load-test-browser-${timestamp}.csv`), browserCSV ); } /** * Generate HTML content for the report */ private generateHTMLContent(config: ReportConfig): string { const summary = this.testResults.summary; const errors = this.testResults.errors; const drmMetrics = this.testResults.drmMetrics; return ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Load Test Results</title> <style> body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; } .container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .header { text-align: center; margin-bottom: 30px; } .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; } .metric-card { background: #f8f9fa; padding: 15px; border-radius: 6px; text-align: center; } .metric-value { font-size: 2em; font-weight: bold; color: #007bff; } .metric-label { color: #666; margin-top: 5px; } .section { margin-bottom: 30px; } .section h2 { border-bottom: 2px solid #007bff; padding-bottom: 10px; } table { width: 100%; border-collapse: collapse; margin-top: 15px; } th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; } th { background-color: #f8f9fa; font-weight: bold; } .error { color: #dc3545; } .success { color: #28a745; } .warning { color: #ffc107; } .chart-placeholder { background: #f8f9fa; height: 200px; display: flex; align-items: center; justify-content: center; border-radius: 6px; margin: 15px 0; } </style> </head> <body> <div class="container"> <div class="header"> <h1>Load Test Results</h1> <p>Generated on ${new Date().toLocaleString()}</p> <p>Test Duration: ${summary.testDuration.toFixed(2)} seconds</p> </div> <div class="section"> <h2>Summary</h2> <div class="summary"> <div class="metric-card"> <div class="metric-value">${summary.totalRequests}</div> <div class="metric-label">Total Requests</div> </div> <div class="metric-card"> <div class="metric-value ${summary.successfulRequests > 0 ? 'success' : ''}">${summary.successfulRequests}</div> <div class="metric-label">Successful Requests</div> </div> <div class="metric-card"> <div class="metric-value ${summary.failedRequests > 0 ? 'error' : ''}">${summary.failedRequests}</div> <div class="metric-label">Failed Requests</div> </div> <div class="metric-card"> <div class="metric-value">${summary.averageResponseTime.toFixed(2)}ms</div> <div class="metric-label">Avg Response Time</div> </div> <div class="metric-card"> <div class="metric-value">${summary.peakConcurrentUsers}</div> <div class="metric-label">Peak Concurrent Users</div> </div> <div class="metric-card"> <div class="metric-value">${((summary.successfulRequests / summary.totalRequests) * 100).toFixed(1)}%</div> <div class="metric-label">Success Rate</div> </div> </div> </div> ${config.includeDetailedMetrics ? this.generateBrowserMetricsHTML() : ''} ${drmMetrics.length > 0 ? this.generateDRMMetricsHTML() : ''} ${config.includeErrorAnalysis && errors.length > 0 ? this.generateErrorAnalysisHTML() : ''} </div> </body> </html>`; } /** * Generate browser metrics section for HTML report */ private generateBrowserMetricsHTML(): string { const browserMetrics = this.testResults.browserMetrics; if (browserMetrics.length === 0) { return ''; } const rows = browserMetrics.map(metrics => ` <tr> <td>${metrics.instanceId}</td> <td>${metrics.memoryUsage.toFixed(2)} MB</td> <td>${metrics.cpuUsage.toFixed(1)}%</td> <td>${metrics.requestCount}</td> <td>${metrics.errorCount}</td> <td>${metrics.uptime.toFixed(2)}s</td> </tr> `).join(''); return ` <div class="section"> <h2>Browser Instance Metrics</h2> <table> <thead> <tr> <th>Instance ID</th> <th>Memory Usage</th> <th>CPU Usage</th> <th>Requests</th> <th>Errors</th> <th>Uptime</th> </tr> </thead> <tbody> ${rows} </tbody> </table> </div> `; } /** * Generate DRM metrics section for HTML report */ private generateDRMMetricsHTML(): string { const drmMetrics = this.testResults.drmMetrics; const rows = drmMetrics.map(metrics => ` <tr> <td>${metrics.drmType}</td> <td>${metrics.licenseRequestCount}</td> <td>${metrics.averageLicenseTime.toFixed(2)}ms</td> <td>${(metrics.licenseSuccessRate * 100).toFixed(1)}%</td> <td>${metrics.errors.length}</td> </tr> `).join(''); return ` <div class="section"> <h2>DRM Metrics</h2> <table> <thead> <tr> <th>DRM Type</th> <th>License Requests</th> <th>Avg License Time</th> <th>Success Rate</th> <th>Errors</th> </tr> </thead> <tbody> ${rows} </tbody> </table> </div> `; } /** * Generate error analysis section for HTML report */ private generateErrorAnalysisHTML(): string { const errors = this.testResults.errors; const errorRows = errors.slice(0, 50).map(error => ` <tr> <td>${error.timestamp.toLocaleString()}</td> <td><span class="${error.level}">${error.level.toUpperCase()}</span></td> <td>${error.message}</td> <td>${error.context ? JSON.stringify(error.context) : '-'}</td> </tr> `).join(''); return ` <div class="section"> <h2>Error Analysis</h2> <p>Showing ${Math.min(50, errors.length)} of ${errors.length} total errors</p> <table> <thead> <tr> <th>Timestamp</th> <th>Level</th> <th>Message</th> <th>Context</th> </tr> </thead> <tbody> ${errorRows} </tbody> </table> </div> `; } /** * Generate summary CSV content */ private generateSummaryCSV(): string { const summary = this.testResults.summary; const headers = [ 'Total Requests', 'Successful Requests', 'Failed Requests', 'Average Response Time (ms)', 'Peak Concurrent Users', 'Test Duration (s)', 'Success Rate (%)' ]; const values = [ summary.totalRequests, summary.successfulRequests, summary.failedRequests, summary.averageResponseTime.toFixed(2), summary.peakConcurrentUsers, summary.testDuration.toFixed(2), ((summary.successfulRequests / summary.totalRequests) * 100).toFixed(1) ]; return headers.join(',') + '\n' + values.join(','); } /** * Generate network metrics CSV content */ private generateNetworkMetricsCSV(): string { const headers = [ 'URL', 'Method', 'Response Time (ms)', 'Status Code', 'Timestamp', 'Request Size (bytes)', 'Response Size (bytes)', 'Streaming Related', 'Streaming Type' ]; const rows = this.testResults.networkMetrics.map(metric => [ `"${metric.url}"`, metric.method, metric.responseTime, metric.statusCode, metric.timestamp.toISOString(), metric.requestSize, metric.responseSize, metric.isStreamingRelated || false, metric.streamingType || '' ].join(',')); return headers.join(',') + '\n' + rows.join('\n'); } /** * Generate browser metrics CSV content */ private generateBrowserMetricsCSV(): string { const headers = [ 'Instance ID', 'Memory Usage (MB)', 'CPU Usage (%)', 'Request Count', 'Error Count', 'Uptime (s)' ]; const rows = this.testResults.browserMetrics.map(metric => [ metric.instanceId, metric.memoryUsage.toFixed(2), metric.cpuUsage.toFixed(1), metric.requestCount, metric.errorCount, metric.uptime.toFixed(2) ].join(',')); return headers.join(',') + '\n' + rows.join('\n'); } }