@testsmith/perfornium 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +360 -0
  2. package/dist/cli/cli.d.ts +2 -0
  3. package/dist/cli/cli.js +192 -0
  4. package/dist/cli/commands/distributed.d.ts +11 -0
  5. package/dist/cli/commands/distributed.js +179 -0
  6. package/dist/cli/commands/import.d.ts +23 -0
  7. package/dist/cli/commands/import.js +461 -0
  8. package/dist/cli/commands/init.d.ts +7 -0
  9. package/dist/cli/commands/init.js +923 -0
  10. package/dist/cli/commands/mock.d.ts +7 -0
  11. package/dist/cli/commands/mock.js +281 -0
  12. package/dist/cli/commands/report.d.ts +5 -0
  13. package/dist/cli/commands/report.js +70 -0
  14. package/dist/cli/commands/run.d.ts +12 -0
  15. package/dist/cli/commands/run.js +260 -0
  16. package/dist/cli/commands/validate.d.ts +3 -0
  17. package/dist/cli/commands/validate.js +35 -0
  18. package/dist/cli/commands/worker.d.ts +27 -0
  19. package/dist/cli/commands/worker.js +320 -0
  20. package/dist/config/index.d.ts +2 -0
  21. package/dist/config/index.js +20 -0
  22. package/dist/config/parser.d.ts +19 -0
  23. package/dist/config/parser.js +330 -0
  24. package/dist/config/types/global-config.d.ts +74 -0
  25. package/dist/config/types/global-config.js +2 -0
  26. package/dist/config/types/hooks.d.ts +58 -0
  27. package/dist/config/types/hooks.js +3 -0
  28. package/dist/config/types/import-types.d.ts +33 -0
  29. package/dist/config/types/import-types.js +2 -0
  30. package/dist/config/types/index.d.ts +11 -0
  31. package/dist/config/types/index.js +27 -0
  32. package/dist/config/types/load-config.d.ts +32 -0
  33. package/dist/config/types/load-config.js +9 -0
  34. package/dist/config/types/output-config.d.ts +10 -0
  35. package/dist/config/types/output-config.js +2 -0
  36. package/dist/config/types/report-config.d.ts +10 -0
  37. package/dist/config/types/report-config.js +2 -0
  38. package/dist/config/types/runtime-types.d.ts +6 -0
  39. package/dist/config/types/runtime-types.js +2 -0
  40. package/dist/config/types/scenario-config.d.ts +30 -0
  41. package/dist/config/types/scenario-config.js +2 -0
  42. package/dist/config/types/step-types.d.ts +139 -0
  43. package/dist/config/types/step-types.js +2 -0
  44. package/dist/config/types/test-configuration.d.ts +18 -0
  45. package/dist/config/types/test-configuration.js +2 -0
  46. package/dist/config/types/worker-config.d.ts +12 -0
  47. package/dist/config/types/worker-config.js +2 -0
  48. package/dist/config/validator.d.ts +19 -0
  49. package/dist/config/validator.js +198 -0
  50. package/dist/core/csv-data-provider.d.ts +47 -0
  51. package/dist/core/csv-data-provider.js +265 -0
  52. package/dist/core/hooks-manager.d.ts +33 -0
  53. package/dist/core/hooks-manager.js +129 -0
  54. package/dist/core/index.d.ts +5 -0
  55. package/dist/core/index.js +11 -0
  56. package/dist/core/script-executor.d.ts +14 -0
  57. package/dist/core/script-executor.js +290 -0
  58. package/dist/core/step-executor.d.ts +41 -0
  59. package/dist/core/step-executor.js +680 -0
  60. package/dist/core/test-runner.d.ts +34 -0
  61. package/dist/core/test-runner.js +465 -0
  62. package/dist/core/threshold-evaluator.d.ts +43 -0
  63. package/dist/core/threshold-evaluator.js +170 -0
  64. package/dist/core/virtual-user-pool.d.ts +42 -0
  65. package/dist/core/virtual-user-pool.js +136 -0
  66. package/dist/core/virtual-user.d.ts +51 -0
  67. package/dist/core/virtual-user.js +488 -0
  68. package/dist/distributed/coordinator.d.ts +34 -0
  69. package/dist/distributed/coordinator.js +158 -0
  70. package/dist/distributed/health-monitor.d.ts +18 -0
  71. package/dist/distributed/health-monitor.js +72 -0
  72. package/dist/distributed/load-distributor.d.ts +17 -0
  73. package/dist/distributed/load-distributor.js +106 -0
  74. package/dist/distributed/remote-worker.d.ts +37 -0
  75. package/dist/distributed/remote-worker.js +241 -0
  76. package/dist/distributed/result-aggregator.d.ts +43 -0
  77. package/dist/distributed/result-aggregator.js +146 -0
  78. package/dist/dsl/index.d.ts +3 -0
  79. package/dist/dsl/index.js +11 -0
  80. package/dist/dsl/test-builder.d.ts +111 -0
  81. package/dist/dsl/test-builder.js +514 -0
  82. package/dist/importers/har-importer.d.ts +17 -0
  83. package/dist/importers/har-importer.js +172 -0
  84. package/dist/importers/open-api-importer.d.ts +23 -0
  85. package/dist/importers/open-api-importer.js +181 -0
  86. package/dist/importers/wsdl-importer.d.ts +42 -0
  87. package/dist/importers/wsdl-importer.js +440 -0
  88. package/dist/index.d.ts +5 -0
  89. package/dist/index.js +17 -0
  90. package/dist/load-patterns/arrivals.d.ts +7 -0
  91. package/dist/load-patterns/arrivals.js +118 -0
  92. package/dist/load-patterns/base.d.ts +9 -0
  93. package/dist/load-patterns/base.js +2 -0
  94. package/dist/load-patterns/basic.d.ts +7 -0
  95. package/dist/load-patterns/basic.js +117 -0
  96. package/dist/load-patterns/stepping.d.ts +6 -0
  97. package/dist/load-patterns/stepping.js +122 -0
  98. package/dist/metrics/collector.d.ts +72 -0
  99. package/dist/metrics/collector.js +662 -0
  100. package/dist/metrics/types.d.ts +135 -0
  101. package/dist/metrics/types.js +2 -0
  102. package/dist/outputs/base.d.ts +7 -0
  103. package/dist/outputs/base.js +2 -0
  104. package/dist/outputs/csv.d.ts +13 -0
  105. package/dist/outputs/csv.js +163 -0
  106. package/dist/outputs/graphite.d.ts +13 -0
  107. package/dist/outputs/graphite.js +126 -0
  108. package/dist/outputs/influxdb.d.ts +12 -0
  109. package/dist/outputs/influxdb.js +82 -0
  110. package/dist/outputs/json.d.ts +14 -0
  111. package/dist/outputs/json.js +107 -0
  112. package/dist/outputs/streaming-csv.d.ts +37 -0
  113. package/dist/outputs/streaming-csv.js +254 -0
  114. package/dist/outputs/streaming-json.d.ts +43 -0
  115. package/dist/outputs/streaming-json.js +353 -0
  116. package/dist/outputs/webhook.d.ts +16 -0
  117. package/dist/outputs/webhook.js +96 -0
  118. package/dist/protocols/base.d.ts +33 -0
  119. package/dist/protocols/base.js +2 -0
  120. package/dist/protocols/rest/handler.d.ts +67 -0
  121. package/dist/protocols/rest/handler.js +776 -0
  122. package/dist/protocols/soap/handler.d.ts +12 -0
  123. package/dist/protocols/soap/handler.js +165 -0
  124. package/dist/protocols/web/core-web-vitals.d.ts +121 -0
  125. package/dist/protocols/web/core-web-vitals.js +373 -0
  126. package/dist/protocols/web/handler.d.ts +50 -0
  127. package/dist/protocols/web/handler.js +706 -0
  128. package/dist/recorder/native-recorder.d.ts +14 -0
  129. package/dist/recorder/native-recorder.js +533 -0
  130. package/dist/recorder/scenario-recorder.d.ts +55 -0
  131. package/dist/recorder/scenario-recorder.js +296 -0
  132. package/dist/reporting/constants.d.ts +94 -0
  133. package/dist/reporting/constants.js +82 -0
  134. package/dist/reporting/enhanced-html-generator.d.ts +55 -0
  135. package/dist/reporting/enhanced-html-generator.js +965 -0
  136. package/dist/reporting/generator.d.ts +42 -0
  137. package/dist/reporting/generator.js +1217 -0
  138. package/dist/reporting/statistics.d.ts +144 -0
  139. package/dist/reporting/statistics.js +742 -0
  140. package/dist/reporting/templates/enhanced-report.hbs +2812 -0
  141. package/dist/reporting/templates/html.hbs +2453 -0
  142. package/dist/utils/faker-manager.d.ts +55 -0
  143. package/dist/utils/faker-manager.js +166 -0
  144. package/dist/utils/file-manager.d.ts +33 -0
  145. package/dist/utils/file-manager.js +154 -0
  146. package/dist/utils/handlebars-manager.d.ts +42 -0
  147. package/dist/utils/handlebars-manager.js +172 -0
  148. package/dist/utils/logger.d.ts +16 -0
  149. package/dist/utils/logger.js +46 -0
  150. package/dist/utils/template.d.ts +80 -0
  151. package/dist/utils/template.js +513 -0
  152. package/dist/utils/test-output-writer.d.ts +56 -0
  153. package/dist/utils/test-output-writer.js +643 -0
  154. package/dist/utils/time.d.ts +3 -0
  155. package/dist/utils/time.js +23 -0
  156. package/dist/utils/timestamp-helper.d.ts +17 -0
  157. package/dist/utils/timestamp-helper.js +53 -0
  158. package/dist/workers/manager.d.ts +18 -0
  159. package/dist/workers/manager.js +95 -0
  160. package/dist/workers/server.d.ts +21 -0
  161. package/dist/workers/server.js +205 -0
  162. package/dist/workers/worker.d.ts +19 -0
  163. package/dist/workers/worker.js +147 -0
  164. package/package.json +102 -0
@@ -0,0 +1,662 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MetricsCollector = void 0;
4
+ const events_1 = require("events");
5
+ const logger_1 = require("../utils/logger");
6
+ class MetricsCollector extends events_1.EventEmitter {
7
+ constructor(realtimeConfig) {
8
+ super();
9
+ this.results = [];
10
+ this.startTime = 0;
11
+ this.errorDetails = new Map();
12
+ this.vuStartEvents = [];
13
+ this.loadPatternType = 'basic';
14
+ this.batchBuffer = [];
15
+ this.batchTimer = null;
16
+ this.batchCounter = 0;
17
+ this.csvHeaderWritten = false;
18
+ // Default output paths
19
+ this.defaultJsonPath = 'results/live-results.json';
20
+ this.defaultCsvPath = 'results/live-results.csv';
21
+ // Enable incremental files by default with sensible defaults
22
+ this.realtimeConfig = {
23
+ enabled: true,
24
+ batch_size: 10, // Default batch size
25
+ incremental_files: {
26
+ enabled: true,
27
+ json_path: this.defaultJsonPath,
28
+ csv_path: this.defaultCsvPath,
29
+ update_summary: true
30
+ },
31
+ ...realtimeConfig // Override with provided config if any
32
+ };
33
+ if (this.realtimeConfig.enabled) {
34
+ this.initializeRealtime();
35
+ }
36
+ }
37
+ initializeRealtime() {
38
+ // Use interval-based batching if specified, otherwise use count-based
39
+ if (this.realtimeConfig.interval_ms) {
40
+ this.startBatchTimer();
41
+ logger_1.logger.info(`📊 Real-time metrics enabled with ${this.realtimeConfig.interval_ms}ms intervals`);
42
+ }
43
+ else {
44
+ const batchSize = this.realtimeConfig.batch_size || 10;
45
+ logger_1.logger.info(`📊 Real-time metrics enabled with batch size: ${batchSize}`);
46
+ }
47
+ if (this.realtimeConfig.file_output?.enabled) {
48
+ logger_1.logger.info(`📁 Real-time file output enabled: ${this.realtimeConfig.file_output.path}`);
49
+ }
50
+ if (this.realtimeConfig.incremental_files?.enabled) {
51
+ logger_1.logger.info(`📄 Incremental JSON/CSV files enabled (JSON: ${this.realtimeConfig.incremental_files.json_path}, CSV: ${this.realtimeConfig.incremental_files.csv_path})`);
52
+ this.initializeIncrementalFiles();
53
+ }
54
+ }
55
+ startBatchTimer() {
56
+ const interval = this.realtimeConfig.interval_ms || 5000;
57
+ this.batchTimer = setInterval(() => {
58
+ if (this.batchBuffer.length > 0) {
59
+ this.flushBatch();
60
+ }
61
+ }, interval);
62
+ }
63
+ async initializeIncrementalFiles() {
64
+ const config = this.realtimeConfig.incremental_files;
65
+ try {
66
+ const fs = require('fs').promises;
67
+ const path = require('path');
68
+ // Initialize JSON file
69
+ if (config.json_path) {
70
+ const dir = path.dirname(config.json_path);
71
+ await fs.mkdir(dir, { recursive: true });
72
+ // Start with empty array
73
+ await fs.writeFile(config.json_path, '[]');
74
+ logger_1.logger.debug(`📄 Initialized incremental JSON file: ${config.json_path}`);
75
+ }
76
+ // Initialize CSV file with header
77
+ if (config.csv_path) {
78
+ const dir = path.dirname(config.csv_path);
79
+ await fs.mkdir(dir, { recursive: true });
80
+ const csvHeader = 'timestamp,vu_id,scenario,action,step_name,duration,success,status,error,request_url\n';
81
+ await fs.writeFile(config.csv_path, csvHeader);
82
+ this.csvHeaderWritten = true;
83
+ logger_1.logger.debug(`📄 Initialized incremental CSV file: ${config.csv_path}`);
84
+ }
85
+ }
86
+ catch (error) {
87
+ logger_1.logger.error('❌ Failed to initialize incremental files:', error);
88
+ }
89
+ }
90
+ start() {
91
+ this.startTime = Date.now();
92
+ this.results = [];
93
+ this.errorDetails.clear();
94
+ this.vuStartEvents = [];
95
+ this.batchBuffer = [];
96
+ this.batchCounter = 0;
97
+ this.csvHeaderWritten = false;
98
+ if (this.realtimeConfig.enabled && this.realtimeConfig.interval_ms) {
99
+ this.startBatchTimer();
100
+ }
101
+ }
102
+ recordVUStart(vuId) {
103
+ this.vuStartEvents.push({
104
+ vu_id: vuId,
105
+ start_time: Date.now(),
106
+ load_pattern: this.loadPatternType
107
+ });
108
+ }
109
+ recordResult(result) {
110
+ this.results.push(result);
111
+ this.emit('result', result);
112
+ // Track detailed error information
113
+ if (!result.success) {
114
+ this.trackErrorDetail(result);
115
+ }
116
+ // Add to batch buffer for real-time processing
117
+ if (this.realtimeConfig.enabled) {
118
+ this.batchBuffer.push(result);
119
+ // Check if we should flush based on batch size (if not using intervals)
120
+ if (!this.realtimeConfig.interval_ms) {
121
+ const batchSize = this.realtimeConfig.batch_size || 10;
122
+ if (this.batchBuffer.length >= batchSize) {
123
+ this.flushBatch();
124
+ }
125
+ }
126
+ }
127
+ }
128
+ recordError(vuId, scenario, action, error) {
129
+ const result = {
130
+ id: `${vuId}-${Date.now()}`,
131
+ vu_id: vuId,
132
+ iteration: 0,
133
+ scenario,
134
+ action,
135
+ timestamp: Date.now(),
136
+ duration: 0,
137
+ success: false,
138
+ error: error.message
139
+ };
140
+ this.recordResult(result);
141
+ }
142
+ async flushBatch() {
143
+ if (this.batchBuffer.length === 0)
144
+ return;
145
+ const batch = [...this.batchBuffer];
146
+ this.batchBuffer = [];
147
+ this.batchCounter++;
148
+ logger_1.logger.debug(`📤 Flushing batch #${this.batchCounter} with ${batch.length} results`);
149
+ try {
150
+ // Write to file if configured
151
+ if (this.realtimeConfig.file_output?.enabled) {
152
+ await this.writeBatchToFile(batch);
153
+ }
154
+ // Send to real-time endpoints
155
+ if (this.realtimeConfig.endpoints) {
156
+ await this.sendToRealTimeEndpoints(batch);
157
+ }
158
+ // Update incremental JSON/CSV files
159
+ if (this.realtimeConfig.incremental_files?.enabled) {
160
+ await this.updateIncrementalFiles(batch);
161
+ }
162
+ // Emit batch event for custom listeners
163
+ this.emit('batch', {
164
+ batch_number: this.batchCounter,
165
+ batch_size: batch.length,
166
+ results: batch,
167
+ timestamp: Date.now()
168
+ });
169
+ }
170
+ catch (error) {
171
+ logger_1.logger.error('❌ Failed to flush metrics batch:', error);
172
+ }
173
+ }
174
+ async writeBatchToFile(batch) {
175
+ const config = this.realtimeConfig.file_output;
176
+ try {
177
+ const fs = require('fs').promises;
178
+ const path = require('path');
179
+ // Ensure directory exists
180
+ const dir = path.dirname(config.path);
181
+ await fs.mkdir(dir, { recursive: true });
182
+ let content;
183
+ if (config.format === 'csv') {
184
+ content = this.formatBatchAsCSV(batch);
185
+ }
186
+ else {
187
+ // JSONL format (default)
188
+ content = batch.map(result => JSON.stringify({
189
+ ...result,
190
+ timestamp: new Date(result.timestamp).toISOString(),
191
+ batch_number: this.batchCounter
192
+ })).join('\n') + '\n';
193
+ }
194
+ await fs.appendFile(config.path, content);
195
+ }
196
+ catch (error) {
197
+ logger_1.logger.error('❌ Failed to write batch to file:', error);
198
+ }
199
+ }
200
+ formatBatchAsCSV(batch) {
201
+ return batch.map(result => [
202
+ new Date(result.timestamp).toISOString(),
203
+ this.batchCounter,
204
+ result.vu_id,
205
+ result.scenario,
206
+ result.action,
207
+ result.step_name || '',
208
+ result.duration,
209
+ result.success,
210
+ result.status || '',
211
+ (result.error || '').replace(/"/g, '""') // Escape quotes
212
+ ].join(',')).join('\n') + '\n';
213
+ }
214
+ async updateIncrementalFiles(batch) {
215
+ const config = this.realtimeConfig.incremental_files;
216
+ try {
217
+ // Update incremental JSON file
218
+ if (config.json_path) {
219
+ await this.updateIncrementalJSON(batch, config.json_path);
220
+ }
221
+ // Update incremental CSV file
222
+ if (config.csv_path) {
223
+ await this.updateIncrementalCSV(batch, config.csv_path);
224
+ }
225
+ // Update summary files if configured
226
+ if (config.update_summary) {
227
+ await this.updateIncrementalSummary();
228
+ }
229
+ }
230
+ catch (error) {
231
+ logger_1.logger.error('❌ Failed to update incremental files:', error);
232
+ }
233
+ }
234
+ async updateIncrementalJSON(batch, filePath) {
235
+ const fs = require('fs').promises;
236
+ try {
237
+ // Read existing file
238
+ const existingContent = await fs.readFile(filePath, 'utf8');
239
+ let existingData = [];
240
+ if (existingContent.trim()) {
241
+ existingData = JSON.parse(existingContent);
242
+ }
243
+ // Append new batch
244
+ const updatedData = [...existingData, ...batch];
245
+ // Write back to file
246
+ await fs.writeFile(filePath, JSON.stringify(updatedData, null, 2));
247
+ }
248
+ catch (error) {
249
+ // If file doesn't exist or is corrupted, start fresh
250
+ await fs.writeFile(filePath, JSON.stringify(batch, null, 2));
251
+ }
252
+ }
253
+ async updateIncrementalCSV(batch, filePath) {
254
+ const fs = require('fs').promises;
255
+ const csvRows = batch.map(result => [
256
+ new Date(result.timestamp).toISOString(),
257
+ result.vu_id,
258
+ result.scenario,
259
+ result.action,
260
+ result.step_name || '',
261
+ result.duration,
262
+ result.success,
263
+ result.status || '',
264
+ (result.error || '').replace(/"/g, '""'), // Escape quotes
265
+ result.request_url || ''
266
+ ].map(field => `"${field}"`).join(',')).join('\n') + '\n';
267
+ await fs.appendFile(filePath, csvRows);
268
+ }
269
+ async updateIncrementalSummary() {
270
+ const summary = this.getSummary();
271
+ const fs = require('fs').promises;
272
+ const path = require('path');
273
+ const config = this.realtimeConfig.incremental_files;
274
+ // Generate summary file paths based on the JSON path
275
+ const basePath = config.json_path ? path.dirname(config.json_path) : 'results';
276
+ const summaryJsonPath = path.join(basePath, 'summary-incremental.json');
277
+ const summaryHtmlPath = path.join(basePath, 'summary-incremental.html');
278
+ try {
279
+ // Write JSON summary
280
+ await fs.writeFile(summaryJsonPath, JSON.stringify({
281
+ last_updated: new Date().toISOString(),
282
+ test_duration: summary.total_duration,
283
+ ...summary
284
+ }, null, 2));
285
+ // Generate simple HTML summary
286
+ const htmlSummary = this.generateSimpleHTMLSummary(summary);
287
+ await fs.writeFile(summaryHtmlPath, htmlSummary);
288
+ }
289
+ catch (error) {
290
+ logger_1.logger.error('❌ Failed to update incremental summary:', error);
291
+ }
292
+ }
293
+ generateSimpleHTMLSummary(summary) {
294
+ const lastUpdated = new Date().toISOString();
295
+ return `<!DOCTYPE html>
296
+ <html>
297
+ <head>
298
+ <title>Load Test Summary (Live)</title>
299
+ <meta http-equiv="refresh" content="5">
300
+ <style>
301
+ body { font-family: Arial, sans-serif; margin: 20px; }
302
+ .metric { margin: 10px 0; padding: 10px; background: #f5f5f5; border-radius: 4px; }
303
+ .success { color: #28a745; }
304
+ .error { color: #dc3545; }
305
+ .header { background: #007bff; color: white; padding: 15px; border-radius: 4px; }
306
+ </style>
307
+ </head>
308
+ <body>
309
+ <div class="header">
310
+ <h1>🚀 Load Test Progress</h1>
311
+ <p>Last Updated: ${lastUpdated}</p>
312
+ <p>Test Duration: ${summary.total_duration.toFixed(1)}s</p>
313
+ </div>
314
+
315
+ <div class="metric">
316
+ <h3>📊 Overall Statistics</h3>
317
+ <p><strong>Total Requests:</strong> ${summary.total_requests}</p>
318
+ <p><strong class="success">Successful:</strong> ${summary.successful_requests}</p>
319
+ <p><strong class="error">Failed:</strong> ${summary.failed_requests}</p>
320
+ <p><strong>Success Rate:</strong> ${summary.success_rate.toFixed(2)}%</p>
321
+ </div>
322
+
323
+ <div class="metric">
324
+ <h3>⏱️ Response Times</h3>
325
+ <p><strong>Average:</strong> ${summary.avg_response_time.toFixed(0)}ms</p>
326
+ <p><strong>Min:</strong> ${summary.min_response_time}ms</p>
327
+ <p><strong>Max:</strong> ${summary.max_response_time}ms</p>
328
+ <p><strong>95th Percentile:</strong> ${summary.percentiles[95] || 0}ms</p>
329
+ </div>
330
+
331
+ <div class="metric">
332
+ <h3>🔄 Throughput</h3>
333
+ <p><strong>Requests/sec:</strong> ${summary.requests_per_second.toFixed(2)}</p>
334
+ <p><strong>Bytes/sec:</strong> ${(summary.bytes_per_second || 0).toFixed(0)}</p>
335
+ </div>
336
+
337
+ ${summary.step_statistics.length > 0 ? `
338
+ <div class="metric">
339
+ <h3>📝 Step Statistics</h3>
340
+ ${summary.step_statistics.slice(0, 5).map(step => `
341
+ <div style="margin: 10px 0; padding: 8px; background: white; border-left: 4px solid ${step.success_rate > 95 ? '#28a745' : '#ffc107'};">
342
+ <strong>${step.step_name}</strong> (${step.scenario})
343
+ <br>Success: ${step.success_rate.toFixed(1)}% | Avg: ${step.avg_response_time.toFixed(0)}ms | Count: ${step.total_requests}
344
+ </div>
345
+ `).join('')}
346
+ </div>
347
+ ` : ''}
348
+
349
+ <div class="metric">
350
+ <small>Auto-refreshes every 5 seconds</small>
351
+ </div>
352
+ </body>
353
+ </html>`;
354
+ }
355
+ async sendToRealTimeEndpoints(batch) {
356
+ if (!this.realtimeConfig.endpoints)
357
+ return;
358
+ const promises = this.realtimeConfig.endpoints.map(endpoint => this.sendToEndpoint(batch, endpoint).catch(error => logger_1.logger.warn(`⚠️ Failed to send to ${endpoint.type} endpoint:`, error)));
359
+ await Promise.allSettled(promises);
360
+ }
361
+ async sendToEndpoint(batch, endpoint) {
362
+ switch (endpoint.type) {
363
+ case 'graphite':
364
+ await this.sendToGraphite(batch, endpoint);
365
+ break;
366
+ case 'webhook':
367
+ await this.sendToWebhook(batch, endpoint);
368
+ break;
369
+ case 'influxdb':
370
+ await this.sendToInfluxDB(batch, endpoint);
371
+ break;
372
+ case 'websocket':
373
+ await this.sendToWebSocket(batch, endpoint);
374
+ break;
375
+ default:
376
+ logger_1.logger.warn(`⚠️ Unknown endpoint type: ${endpoint.type}`);
377
+ }
378
+ }
379
+ async sendToGraphite(batch, config) {
380
+ const net = require('net');
381
+ return new Promise((resolve, reject) => {
382
+ const client = net.createConnection(config.port, config.host);
383
+ client.on('connect', () => {
384
+ const metrics = batch.map(result => {
385
+ const timestamp = Math.floor(result.timestamp / 1000);
386
+ const metricName = `loadtest.${result.scenario}.${result.step_name || result.action}`;
387
+ return [
388
+ `${metricName}.duration ${result.duration} ${timestamp}`,
389
+ `${metricName}.success ${result.success ? 1 : 0} ${timestamp}`,
390
+ `${metricName}.count 1 ${timestamp}`
391
+ ].join('\n');
392
+ }).join('\n') + '\n';
393
+ client.write(metrics);
394
+ client.end();
395
+ });
396
+ client.on('close', () => resolve());
397
+ client.on('error', reject);
398
+ setTimeout(() => {
399
+ client.destroy();
400
+ reject(new Error('Graphite connection timeout'));
401
+ }, 5000);
402
+ });
403
+ }
404
+ async sendToWebhook(batch, config) {
405
+ const response = await fetch(config.url, {
406
+ method: 'POST',
407
+ headers: {
408
+ 'Content-Type': 'application/json',
409
+ ...config.headers
410
+ },
411
+ body: JSON.stringify({
412
+ timestamp: new Date().toISOString(),
413
+ batch_number: this.batchCounter,
414
+ batch_size: batch.length,
415
+ test_start_time: new Date(this.startTime).toISOString(),
416
+ results: batch
417
+ })
418
+ });
419
+ if (!response.ok) {
420
+ throw new Error(`Webhook failed: ${response.status} ${response.statusText}`);
421
+ }
422
+ }
423
+ async sendToInfluxDB(batch, config) {
424
+ const lines = batch.map(result => {
425
+ const tags = [
426
+ `scenario=${result.scenario}`,
427
+ `step=${result.step_name || result.action}`,
428
+ `vu_id=${result.vu_id}`,
429
+ `success=${result.success}`
430
+ ].join(',');
431
+ const fields = [
432
+ `duration=${result.duration}`,
433
+ `success=${result.success ? 'true' : 'false'}`,
434
+ `batch_number=${this.batchCounter}i`
435
+ ];
436
+ if (result.status) {
437
+ fields.push(`status=${result.status}i`);
438
+ }
439
+ const timestamp = result.timestamp * 1000000; // Convert to nanoseconds
440
+ return `loadtest,${tags} ${fields.join(',')} ${timestamp}`;
441
+ }).join('\n');
442
+ const response = await fetch(`${config.url}/write?db=${config.database}`, {
443
+ method: 'POST',
444
+ headers: {
445
+ 'Authorization': `Bearer ${config.token}`,
446
+ 'Content-Type': 'text/plain'
447
+ },
448
+ body: lines
449
+ });
450
+ if (!response.ok) {
451
+ const errorText = await response.text();
452
+ throw new Error(`InfluxDB write failed: ${response.status} ${errorText}`);
453
+ }
454
+ }
455
+ async sendToWebSocket(batch, config) {
456
+ return new Promise((resolve, reject) => {
457
+ const WebSocket = require('ws');
458
+ const ws = new WebSocket(config.url);
459
+ ws.on('open', () => {
460
+ ws.send(JSON.stringify({
461
+ type: 'metrics_batch',
462
+ timestamp: new Date().toISOString(),
463
+ batch_number: this.batchCounter,
464
+ test_start_time: new Date(this.startTime).toISOString(),
465
+ data: batch
466
+ }));
467
+ ws.close();
468
+ resolve();
469
+ });
470
+ ws.on('error', reject);
471
+ setTimeout(() => {
472
+ ws.close();
473
+ reject(new Error('WebSocket connection timeout'));
474
+ }, 5000);
475
+ });
476
+ }
477
+ // Force flush when test completes or stops
478
+ async finalize() {
479
+ if (this.batchTimer) {
480
+ clearInterval(this.batchTimer);
481
+ this.batchTimer = null;
482
+ }
483
+ // Flush any remaining results
484
+ if (this.batchBuffer.length > 0) {
485
+ await this.flushBatch();
486
+ }
487
+ logger_1.logger.info(`📊 Metrics collection finalized. Total batches: ${this.batchCounter}, Total results: ${this.results.length}`);
488
+ }
489
+ trackErrorDetail(result) {
490
+ const errorKey = `${result.scenario}:${result.action}:${result.status || 'NO_STATUS'}:${result.error}`;
491
+ const existing = this.errorDetails.get(errorKey);
492
+ if (existing) {
493
+ existing.count++;
494
+ }
495
+ else {
496
+ this.errorDetails.set(errorKey, {
497
+ timestamp: result.timestamp,
498
+ vu_id: result.vu_id,
499
+ scenario: result.scenario,
500
+ action: result.action,
501
+ status: result.status,
502
+ error: result.error || 'Unknown error',
503
+ request_url: result.request_url,
504
+ response_body: result.response_body,
505
+ count: 1
506
+ });
507
+ }
508
+ }
509
+ getResults() {
510
+ return [...this.results];
511
+ }
512
+ // Add method to configure output paths without recreating the collector
513
+ // Add method to disable incremental files if needed
514
+ getSummary() {
515
+ const totalRequests = this.results.length;
516
+ const successfulRequests = this.results.filter(r => r.success).length;
517
+ const failedRequests = totalRequests - successfulRequests;
518
+ const durations = this.results.filter(r => r.success).map(r => r.duration);
519
+ const totalDuration = (Date.now() - this.startTime) / 1000;
520
+ // Error distribution by error message
521
+ const errorDistribution = {};
522
+ this.results.filter(r => !r.success).forEach(r => {
523
+ const error = r.error || 'Unknown error';
524
+ errorDistribution[error] = (errorDistribution[error] || 0) + 1;
525
+ });
526
+ // Status code distribution
527
+ const statusDistribution = {};
528
+ this.results.forEach(r => {
529
+ if (r.status) {
530
+ statusDistribution[r.status] = (statusDistribution[r.status] || 0) + 1;
531
+ }
532
+ });
533
+ const responseSizes = this.results
534
+ .filter(r => r.response_size)
535
+ .map(r => r.response_size);
536
+ return {
537
+ total_requests: totalRequests,
538
+ successful_requests: successfulRequests,
539
+ failed_requests: failedRequests,
540
+ success_rate: totalRequests > 0 ? (successfulRequests / totalRequests) * 100 : 0,
541
+ avg_response_time: durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0,
542
+ min_response_time: durations.length > 0 ? Math.min(...durations) : 0,
543
+ max_response_time: durations.length > 0 ? Math.max(...durations) : 0,
544
+ percentiles: this.calculatePercentiles(durations),
545
+ requests_per_second: totalDuration > 0 ? (totalRequests / totalDuration) : 0,
546
+ bytes_per_second: responseSizes.length > 0 && totalDuration > 0
547
+ ? (responseSizes.reduce((a, b) => a + b, 0) / totalDuration) : 0,
548
+ total_duration: totalDuration,
549
+ error_distribution: errorDistribution,
550
+ status_distribution: statusDistribution,
551
+ error_details: Array.from(this.errorDetails.values()).sort((a, b) => b.count - a.count),
552
+ // New enhanced statistics
553
+ step_statistics: this.calculateStepStatistics(),
554
+ vu_ramp_up: this.vuStartEvents,
555
+ timeline_data: this.calculateTimelineData()
556
+ };
557
+ }
558
+ calculateStepStatistics() {
559
+ const stepGroups = new Map();
560
+ // Group results by step name and scenario
561
+ this.results.forEach(result => {
562
+ const key = `${result.scenario}:${result.step_name || result.action}`;
563
+ if (!stepGroups.has(key)) {
564
+ stepGroups.set(key, []);
565
+ }
566
+ stepGroups.get(key).push(result);
567
+ });
568
+ const stepStats = [];
569
+ for (const [key, results] of stepGroups) {
570
+ const [scenario, stepName] = key.split(':');
571
+ const successfulResults = results.filter(r => r.success);
572
+ // Include ALL results (both successful and failed) for response time calculations
573
+ // Failed requests also have response times that should be included in statistics
574
+ const responseTimes = results
575
+ .map(r => r.response_time || r.duration || 0)
576
+ .filter(rt => rt > 0);
577
+ // Error distribution for this step
578
+ const errorDistribution = {};
579
+ results.filter(r => !r.success).forEach(r => {
580
+ const error = r.error || 'Unknown error';
581
+ errorDistribution[error] = (errorDistribution[error] || 0) + 1;
582
+ });
583
+ // Status distribution for this step
584
+ const statusDistribution = {};
585
+ results.forEach(r => {
586
+ if (r.status) {
587
+ statusDistribution[r.status] = (statusDistribution[r.status] || 0) + 1;
588
+ }
589
+ });
590
+ stepStats.push({
591
+ step_name: stepName,
592
+ scenario: scenario,
593
+ total_requests: results.length,
594
+ successful_requests: successfulResults.length,
595
+ failed_requests: results.length - successfulResults.length,
596
+ success_rate: results.length > 0 ? (successfulResults.length / results.length) * 100 : 0,
597
+ avg_response_time: responseTimes.length > 0 ? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length : 0,
598
+ min_response_time: responseTimes.length > 0 ? Math.min(...responseTimes) : 0,
599
+ max_response_time: responseTimes.length > 0 ? Math.max(...responseTimes) : 0,
600
+ percentiles: this.calculatePercentiles(responseTimes),
601
+ response_times: responseTimes,
602
+ error_distribution: errorDistribution,
603
+ status_distribution: statusDistribution
604
+ });
605
+ }
606
+ return stepStats.sort((a, b) => b.total_requests - a.total_requests);
607
+ }
608
+ calculateTimelineData() {
609
+ if (this.results.length === 0)
610
+ return [];
611
+ const intervalMs = 5000; // 5 second intervals
612
+ const startTime = this.startTime;
613
+ const endTime = Date.now();
614
+ const timeline = [];
615
+ for (let time = startTime; time <= endTime; time += intervalMs) {
616
+ const intervalResults = this.results.filter(r => r.timestamp >= time && r.timestamp < time + intervalMs);
617
+ const successfulResults = intervalResults.filter(r => r.success);
618
+ // Calculate active VUs at this time
619
+ const activeVUs = this.vuStartEvents.filter(vu => vu.start_time <= time).length;
620
+ timeline.push({
621
+ timestamp: time,
622
+ time_label: new Date(time).toISOString(),
623
+ active_vus: activeVUs,
624
+ requests_count: intervalResults.length,
625
+ avg_response_time: successfulResults.length > 0
626
+ ? successfulResults.reduce((sum, r) => sum + r.duration, 0) / successfulResults.length
627
+ : 0,
628
+ success_rate: intervalResults.length > 0
629
+ ? (successfulResults.length / intervalResults.length) * 100
630
+ : 0,
631
+ throughput: intervalResults.length / (intervalMs / 1000)
632
+ });
633
+ }
634
+ return timeline;
635
+ }
636
+ calculatePercentiles(values) {
637
+ if (values.length === 0)
638
+ return {};
639
+ const sorted = [...values].sort((a, b) => a - b);
640
+ const percentiles = [50, 90, 95, 99, 99.9, 99.99];
641
+ const result = {};
642
+ percentiles.forEach(p => {
643
+ const index = Math.ceil((p / 100) * sorted.length) - 1;
644
+ result[p] = sorted[Math.max(0, index)];
645
+ });
646
+ return result;
647
+ }
648
+ clear() {
649
+ if (this.batchTimer) {
650
+ clearInterval(this.batchTimer);
651
+ this.batchTimer = null;
652
+ }
653
+ this.results = [];
654
+ this.errorDetails.clear();
655
+ this.vuStartEvents = [];
656
+ this.batchBuffer = [];
657
+ this.batchCounter = 0;
658
+ this.csvHeaderWritten = false;
659
+ this.startTime = 0;
660
+ }
661
+ }
662
+ exports.MetricsCollector = MetricsCollector;