@testsmith/perfornium 0.6.4 → 0.6.6

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 (194) hide show
  1. package/dist/cli/cli.js +16 -1
  2. package/dist/cli/commands/distributed.js +2 -2
  3. package/dist/cli/commands/report.js +2 -2
  4. package/dist/cli/commands/run.js +2 -0
  5. package/dist/config/parser.js +2 -2
  6. package/dist/config/types/global-config.d.ts +82 -2
  7. package/dist/config/types/scenario-config.d.ts +2 -2
  8. package/dist/config/types/step-types.d.ts +1 -1
  9. package/dist/core/data/data-manager.d.ts +70 -0
  10. package/dist/core/data/data-manager.js +186 -0
  11. package/dist/core/data/data-provider.d.ts +85 -0
  12. package/dist/core/data/data-provider.js +468 -0
  13. package/dist/core/data/index.d.ts +8 -0
  14. package/dist/core/data/index.js +13 -0
  15. package/dist/core/execution/check-evaluator.d.ts +10 -0
  16. package/dist/core/execution/check-evaluator.js +79 -0
  17. package/dist/core/execution/data-extractor.d.ts +6 -0
  18. package/dist/core/execution/data-extractor.js +70 -0
  19. package/dist/core/execution/index.d.ts +3 -0
  20. package/dist/core/execution/index.js +9 -0
  21. package/dist/core/execution/json-payload-processor.d.ts +7 -0
  22. package/dist/core/execution/json-payload-processor.js +140 -0
  23. package/dist/core/factories/index.d.ts +2 -0
  24. package/dist/core/factories/index.js +7 -0
  25. package/dist/core/factories/output-handler-factory.d.ts +10 -0
  26. package/dist/core/factories/output-handler-factory.js +91 -0
  27. package/dist/core/factories/protocol-handler-factory.d.ts +12 -0
  28. package/dist/core/factories/protocol-handler-factory.js +96 -0
  29. package/dist/core/index.d.ts +3 -2
  30. package/dist/core/index.js +8 -3
  31. package/dist/core/reporting/dashboard-reporter.d.ts +17 -0
  32. package/dist/core/reporting/dashboard-reporter.js +127 -0
  33. package/dist/core/reporting/index.d.ts +1 -0
  34. package/dist/core/reporting/index.js +5 -0
  35. package/dist/core/step-executor.d.ts +6 -20
  36. package/dist/core/step-executor.js +72 -366
  37. package/dist/core/strategies/index.d.ts +2 -0
  38. package/dist/core/strategies/index.js +7 -0
  39. package/dist/core/strategies/scenario-selector.d.ts +13 -0
  40. package/dist/core/strategies/scenario-selector.js +37 -0
  41. package/dist/core/strategies/think-time-strategy.d.ts +15 -0
  42. package/dist/core/strategies/think-time-strategy.js +71 -0
  43. package/dist/core/test-runner.d.ts +4 -11
  44. package/dist/core/test-runner.js +105 -312
  45. package/dist/core/virtual-user.d.ts +7 -37
  46. package/dist/core/virtual-user.js +29 -269
  47. package/dist/dashboard/routes/api.d.ts +64 -0
  48. package/dist/dashboard/routes/api.js +569 -0
  49. package/dist/dashboard/routes/index.d.ts +2 -0
  50. package/dist/dashboard/routes/index.js +7 -0
  51. package/dist/dashboard/routes/static.d.ts +6 -0
  52. package/dist/dashboard/routes/static.js +76 -0
  53. package/dist/dashboard/server.d.ts +8 -84
  54. package/dist/dashboard/server.js +76 -2007
  55. package/dist/dashboard/services/file-scanner.d.ts +7 -0
  56. package/dist/dashboard/services/file-scanner.js +114 -0
  57. package/dist/dashboard/services/index.d.ts +5 -0
  58. package/dist/dashboard/services/index.js +13 -0
  59. package/dist/dashboard/services/influxdb-service.d.ts +41 -0
  60. package/dist/dashboard/services/influxdb-service.js +329 -0
  61. package/dist/dashboard/services/metrics-parser.d.ts +12 -0
  62. package/dist/dashboard/services/metrics-parser.js +209 -0
  63. package/dist/dashboard/services/results-manager.d.ts +17 -0
  64. package/dist/dashboard/services/results-manager.js +311 -0
  65. package/dist/dashboard/services/test-executor.d.ts +41 -0
  66. package/dist/dashboard/services/test-executor.js +250 -0
  67. package/dist/dashboard/services/workers-manager.d.ts +13 -0
  68. package/dist/dashboard/services/workers-manager.js +81 -0
  69. package/dist/dashboard/templates/index.html +122 -0
  70. package/dist/dashboard/templates/scripts/main.js +3280 -0
  71. package/dist/dashboard/templates/styles.css +402 -0
  72. package/dist/dashboard/types.d.ts +168 -0
  73. package/dist/dashboard/types.js +2 -0
  74. package/dist/distributed/result-aggregator.js +1 -3
  75. package/dist/metrics/batch/batch-processor.d.ts +27 -0
  76. package/dist/metrics/batch/batch-processor.js +85 -0
  77. package/dist/metrics/batch/index.d.ts +1 -0
  78. package/dist/metrics/batch/index.js +5 -0
  79. package/dist/metrics/collector.d.ts +46 -45
  80. package/dist/metrics/collector.js +179 -640
  81. package/dist/metrics/core/error-tracker.d.ts +9 -0
  82. package/dist/metrics/core/error-tracker.js +52 -0
  83. package/dist/metrics/core/index.d.ts +3 -0
  84. package/dist/metrics/core/index.js +9 -0
  85. package/dist/metrics/core/result-storage.d.ts +19 -0
  86. package/dist/metrics/core/result-storage.js +56 -0
  87. package/dist/metrics/core/statistics-engine.d.ts +27 -0
  88. package/dist/metrics/core/statistics-engine.js +91 -0
  89. package/dist/metrics/output/file-writer.d.ts +19 -0
  90. package/dist/metrics/output/file-writer.js +129 -0
  91. package/dist/metrics/output/index.d.ts +2 -0
  92. package/dist/metrics/output/index.js +10 -0
  93. package/dist/metrics/output/influxdb-writer.d.ts +89 -0
  94. package/dist/metrics/output/influxdb-writer.js +404 -0
  95. package/dist/metrics/realtime/dispatcher.d.ts +18 -0
  96. package/dist/metrics/realtime/dispatcher.js +45 -0
  97. package/dist/metrics/realtime/endpoints/graphite.d.ts +3 -0
  98. package/dist/metrics/realtime/endpoints/graphite.js +61 -0
  99. package/dist/metrics/realtime/endpoints/influxdb.d.ts +3 -0
  100. package/dist/metrics/realtime/endpoints/influxdb.js +35 -0
  101. package/dist/metrics/realtime/endpoints/webhook.d.ts +3 -0
  102. package/dist/metrics/realtime/endpoints/webhook.js +22 -0
  103. package/dist/metrics/realtime/endpoints/websocket.d.ts +3 -0
  104. package/dist/metrics/realtime/endpoints/websocket.js +25 -0
  105. package/dist/metrics/realtime/index.d.ts +5 -0
  106. package/dist/metrics/realtime/index.js +13 -0
  107. package/dist/metrics/reporting/index.d.ts +3 -0
  108. package/dist/metrics/reporting/index.js +9 -0
  109. package/dist/metrics/reporting/step-statistics.d.ts +6 -0
  110. package/dist/metrics/reporting/step-statistics.js +59 -0
  111. package/dist/metrics/reporting/summary-generator.d.ts +16 -0
  112. package/dist/metrics/reporting/summary-generator.js +46 -0
  113. package/dist/metrics/reporting/timeline-calculator.d.ts +7 -0
  114. package/dist/metrics/reporting/timeline-calculator.js +86 -0
  115. package/dist/metrics/types.d.ts +58 -0
  116. package/dist/outputs/csv.d.ts +2 -0
  117. package/dist/outputs/csv.js +21 -2
  118. package/dist/outputs/json.js +6 -2
  119. package/dist/protocols/rest/handler.d.ts +4 -53
  120. package/dist/protocols/rest/handler.js +73 -454
  121. package/dist/protocols/rest/request/auth-handler.d.ts +4 -0
  122. package/dist/protocols/rest/request/auth-handler.js +30 -0
  123. package/dist/protocols/rest/request/body-processor.d.ts +11 -0
  124. package/dist/protocols/rest/request/body-processor.js +62 -0
  125. package/dist/protocols/rest/request/index.d.ts +2 -0
  126. package/dist/protocols/rest/request/index.js +7 -0
  127. package/dist/protocols/rest/response/checks.d.ts +6 -0
  128. package/dist/protocols/rest/response/checks.js +71 -0
  129. package/dist/protocols/rest/response/index.d.ts +2 -0
  130. package/dist/protocols/rest/response/index.js +7 -0
  131. package/dist/protocols/rest/response/size-calculator.d.ts +12 -0
  132. package/dist/protocols/rest/response/size-calculator.js +64 -0
  133. package/dist/protocols/web/browser/highlight.d.ts +7 -0
  134. package/dist/protocols/web/browser/highlight.js +47 -0
  135. package/dist/protocols/web/browser/index.d.ts +4 -0
  136. package/dist/protocols/web/browser/index.js +11 -0
  137. package/dist/protocols/web/browser/manager.d.ts +20 -0
  138. package/dist/protocols/web/browser/manager.js +189 -0
  139. package/dist/protocols/web/browser/screenshot.d.ts +8 -0
  140. package/dist/protocols/web/browser/screenshot.js +69 -0
  141. package/dist/protocols/web/browser/storage.d.ts +5 -0
  142. package/dist/protocols/web/browser/storage.js +45 -0
  143. package/dist/protocols/web/commands/index.d.ts +5 -0
  144. package/dist/protocols/web/commands/index.js +11 -0
  145. package/dist/protocols/web/commands/interaction.d.ts +13 -0
  146. package/dist/protocols/web/commands/interaction.js +68 -0
  147. package/dist/protocols/web/commands/measurement.d.ts +16 -0
  148. package/dist/protocols/web/commands/measurement.js +33 -0
  149. package/dist/protocols/web/commands/navigation.d.ts +11 -0
  150. package/dist/protocols/web/commands/navigation.js +43 -0
  151. package/dist/protocols/web/commands/types.d.ts +12 -0
  152. package/dist/protocols/web/commands/types.js +2 -0
  153. package/dist/protocols/web/commands/verification.d.ts +12 -0
  154. package/dist/protocols/web/commands/verification.js +118 -0
  155. package/dist/protocols/web/handler.d.ts +19 -30
  156. package/dist/protocols/web/handler.js +164 -651
  157. package/dist/protocols/web/network/capture.d.ts +19 -0
  158. package/dist/protocols/web/network/capture.js +225 -0
  159. package/dist/protocols/web/network/filters.d.ts +5 -0
  160. package/dist/protocols/web/network/filters.js +49 -0
  161. package/dist/protocols/web/network/index.d.ts +4 -0
  162. package/dist/protocols/web/network/index.js +9 -0
  163. package/dist/protocols/web/network/types.d.ts +13 -0
  164. package/dist/protocols/web/network/types.js +2 -0
  165. package/dist/protocols/web/network/utils.d.ts +8 -0
  166. package/dist/protocols/web/network/utils.js +29 -0
  167. package/dist/recorder/continue-recorder.d.ts +11 -0
  168. package/dist/recorder/continue-recorder.js +872 -0
  169. package/dist/reporting/chart-data/index.d.ts +5 -0
  170. package/dist/reporting/chart-data/index.js +13 -0
  171. package/dist/reporting/chart-data/network.d.ts +25 -0
  172. package/dist/reporting/chart-data/network.js +78 -0
  173. package/dist/reporting/chart-data/scenario.d.ts +37 -0
  174. package/dist/reporting/chart-data/scenario.js +76 -0
  175. package/dist/reporting/chart-data/step-statistics.d.ts +24 -0
  176. package/dist/reporting/chart-data/step-statistics.js +94 -0
  177. package/dist/reporting/chart-data/throughput.d.ts +16 -0
  178. package/dist/reporting/chart-data/throughput.js +24 -0
  179. package/dist/reporting/chart-data/timeline.d.ts +17 -0
  180. package/dist/reporting/chart-data/timeline.js +46 -0
  181. package/dist/reporting/handlebars-helpers.d.ts +1 -0
  182. package/dist/reporting/handlebars-helpers.js +63 -0
  183. package/dist/reporting/{enhanced-html-generator.d.ts → html-generator.d.ts} +1 -1
  184. package/dist/reporting/{enhanced-html-generator.js → html-generator.js} +10 -7
  185. package/dist/reporting/templates/{enhanced-report.hbs → report.hbs} +9 -9
  186. package/dist/utils/data-utils.d.ts +17 -0
  187. package/dist/utils/data-utils.js +129 -0
  188. package/dist/utils/template.js +2 -2
  189. package/package.json +5 -2
  190. package/dist/core/csv-data-provider.d.ts +0 -47
  191. package/dist/core/csv-data-provider.js +0 -265
  192. package/dist/reporting/generator.d.ts +0 -42
  193. package/dist/reporting/generator.js +0 -1217
  194. package/dist/reporting/templates/html.hbs +0 -2453
@@ -3,178 +3,184 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MetricsCollector = void 0;
4
4
  const events_1 = require("events");
5
5
  const logger_1 = require("../utils/logger");
6
+ const statistics_engine_1 = require("./core/statistics-engine");
7
+ const error_tracker_1 = require("./core/error-tracker");
8
+ const result_storage_1 = require("./core/result-storage");
9
+ const batch_processor_1 = require("./batch/batch-processor");
10
+ const file_writer_1 = require("./output/file-writer");
11
+ const dispatcher_1 = require("./realtime/dispatcher");
12
+ const summary_generator_1 = require("./reporting/summary-generator");
13
+ const influxdb_writer_1 = require("./output/influxdb-writer");
6
14
  class MetricsCollector extends events_1.EventEmitter {
7
15
  constructor(realtimeConfig) {
8
16
  super();
9
- this.results = [];
10
17
  this.startTime = 0;
11
- this.errorDetails = new Map();
12
- this.vuStartEvents = [];
13
- this.loadPatternType = 'basic';
14
- // Running statistics (accurate even when individual results are dropped)
15
- this.runningStats = {
16
- totalRequests: 0,
17
- successfulRequests: 0,
18
- failedRequests: 0,
19
- totalDuration: 0, // Sum of all durations for averaging
20
- minDuration: Infinity,
21
- maxDuration: 0,
22
- durations: [], // For percentile calculation (limited size)
23
- };
24
- this.maxDurationsForPercentiles = 10000; // Keep last N for percentiles
25
- this.maxStoredResults = 50000; // Max individual results to keep in memory
26
- this.batchBuffer = [];
27
- this.batchTimer = null;
28
- this.batchCounter = 0;
29
- this.csvHeaderWritten = false;
30
- // Default output paths
18
+ this.influxDBWriter = null;
19
+ // Default output path for live results (used by dashboard)
31
20
  this.defaultJsonPath = 'results/live-results.json';
32
- this.defaultCsvPath = 'results/live-results.csv';
33
- // Enable incremental files by default with sensible defaults
21
+ // Current test info for InfluxDB
22
+ this.currentTestId = '';
23
+ this.currentTestName = '';
24
+ // Initialize core modules
25
+ this.statisticsEngine = new statistics_engine_1.StatisticsEngine();
26
+ this.errorTracker = new error_tracker_1.ErrorTracker();
27
+ this.resultStorage = new result_storage_1.ResultStorage();
28
+ this.fileWriter = new file_writer_1.FileWriter();
29
+ this.realtimeDispatcher = new dispatcher_1.RealtimeDispatcher();
30
+ this.summaryGenerator = new summary_generator_1.SummaryGenerator();
31
+ // Enable incremental files by default for dashboard support
34
32
  this.realtimeConfig = {
35
33
  enabled: true,
36
- batch_size: 10, // Default batch size
34
+ batch_size: 10,
37
35
  incremental_files: {
38
36
  enabled: true,
39
- json_path: this.defaultJsonPath,
40
- csv_path: this.defaultCsvPath,
41
- update_summary: true
37
+ json_path: this.defaultJsonPath
42
38
  },
43
- ...realtimeConfig // Override with provided config if any
39
+ ...realtimeConfig
44
40
  };
41
+ // Initialize batch processor with flush handler
42
+ this.batchProcessor = new batch_processor_1.BatchProcessor({
43
+ batchSize: this.realtimeConfig.batch_size || 10,
44
+ intervalMs: this.realtimeConfig.interval_ms,
45
+ maxBufferSize: 1000
46
+ });
47
+ this.batchProcessor.setFlushHandler(async (batch, batchNumber) => {
48
+ await this.handleBatchFlush(batch, batchNumber);
49
+ });
50
+ // Note: initializeRealtime is called separately via initialize() to properly await async operations
51
+ }
52
+ /**
53
+ * Initialize async components (call before starting metrics collection)
54
+ */
55
+ async initialize() {
45
56
  if (this.realtimeConfig.enabled) {
46
- this.initializeRealtime();
57
+ await this.initializeRealtime();
47
58
  }
48
59
  }
49
- initializeRealtime() {
50
- // Use interval-based batching if specified, otherwise use count-based
60
+ async initializeRealtime() {
51
61
  if (this.realtimeConfig.interval_ms) {
52
- this.startBatchTimer();
53
- logger_1.logger.info(`📊 Real-time metrics enabled with ${this.realtimeConfig.interval_ms}ms intervals`);
62
+ this.batchProcessor.start();
63
+ logger_1.logger.info(`Real-time metrics enabled with ${this.realtimeConfig.interval_ms}ms intervals`);
54
64
  }
55
65
  else {
56
66
  const batchSize = this.realtimeConfig.batch_size || 10;
57
- logger_1.logger.info(`📊 Real-time metrics enabled with batch size: ${batchSize}`);
67
+ logger_1.logger.info(`Real-time metrics enabled with batch size: ${batchSize}`);
58
68
  }
59
69
  if (this.realtimeConfig.file_output?.enabled) {
60
- logger_1.logger.info(`📁 Real-time file output enabled: ${this.realtimeConfig.file_output.path}`);
70
+ logger_1.logger.info(`Real-time file output enabled: ${this.realtimeConfig.file_output.path}`);
61
71
  }
62
72
  if (this.realtimeConfig.incremental_files?.enabled) {
63
- logger_1.logger.info(`📄 Incremental JSON/CSV files enabled (JSON: ${this.realtimeConfig.incremental_files.json_path}, CSV: ${this.realtimeConfig.incremental_files.csv_path})`);
64
- this.initializeIncrementalFiles();
73
+ const config = this.realtimeConfig.incremental_files;
74
+ logger_1.logger.info(`Live results file enabled: ${config.json_path}`);
75
+ await this.fileWriter.initialize({
76
+ enabled: true,
77
+ jsonPath: config.json_path
78
+ });
65
79
  }
66
- }
67
- startBatchTimer() {
68
- const interval = this.realtimeConfig.interval_ms || 5000;
69
- this.batchTimer = setInterval(() => {
70
- if (this.batchBuffer.length > 0) {
71
- this.flushBatch();
80
+ if (this.realtimeConfig.endpoints) {
81
+ this.realtimeDispatcher.setEndpoints(this.realtimeConfig.endpoints);
82
+ }
83
+ // Initialize InfluxDB writer if configured
84
+ if (this.realtimeConfig.influxdb?.enabled) {
85
+ logger_1.logger.info(`Initializing InfluxDB connection to ${this.realtimeConfig.influxdb.url || 'default URL'}...`);
86
+ this.influxDBWriter = new influxdb_writer_1.InfluxDBWriter({
87
+ url: this.realtimeConfig.influxdb.url,
88
+ token: this.realtimeConfig.influxdb.token,
89
+ org: this.realtimeConfig.influxdb.org,
90
+ bucket: this.realtimeConfig.influxdb.bucket,
91
+ batchSize: this.realtimeConfig.influxdb.batch_size,
92
+ flushInterval: this.realtimeConfig.influxdb.flush_interval
93
+ });
94
+ const connected = await this.influxDBWriter.connect();
95
+ if (connected) {
96
+ logger_1.logger.info('InfluxDB test metrics storage enabled and connected');
72
97
  }
73
- }, interval);
98
+ else {
99
+ logger_1.logger.warn('InfluxDB configured but connection failed - metrics will only be stored in files');
100
+ }
101
+ }
102
+ else {
103
+ logger_1.logger.debug('InfluxDB not configured - test metrics will be stored in files only');
104
+ }
74
105
  }
75
- async initializeIncrementalFiles() {
76
- const config = this.realtimeConfig.incremental_files;
106
+ async handleBatchFlush(batch, batchNumber) {
77
107
  try {
78
- const fs = require('fs').promises;
79
- const path = require('path');
80
- // Initialize JSON file
81
- if (config.json_path) {
82
- const dir = path.dirname(config.json_path);
83
- await fs.mkdir(dir, { recursive: true });
84
- // Start with empty array
85
- await fs.writeFile(config.json_path, '[]');
86
- logger_1.logger.debug(`📄 Initialized incremental JSON file: ${config.json_path}`);
108
+ // Write to file if configured
109
+ if (this.realtimeConfig.file_output?.enabled) {
110
+ await this.fileWriter.writeBatchToFile(batch, {
111
+ enabled: true,
112
+ path: this.realtimeConfig.file_output.path,
113
+ format: this.realtimeConfig.file_output.format
114
+ }, batchNumber);
87
115
  }
88
- // Initialize CSV file with header
89
- if (config.csv_path) {
90
- const dir = path.dirname(config.csv_path);
91
- await fs.mkdir(dir, { recursive: true });
92
- const csvHeader = 'timestamp,vu_id,scenario,action,step_name,duration,success,status,error,request_url\n';
93
- await fs.writeFile(config.csv_path, csvHeader);
94
- this.csvHeaderWritten = true;
95
- logger_1.logger.debug(`📄 Initialized incremental CSV file: ${config.csv_path}`);
116
+ // Send to real-time endpoints
117
+ await this.realtimeDispatcher.dispatch(batch, batchNumber);
118
+ // Update live results file (for dashboard)
119
+ if (this.realtimeConfig.incremental_files?.enabled) {
120
+ await this.fileWriter.updateIncrementalFiles(batch);
96
121
  }
122
+ // Write to InfluxDB if enabled
123
+ if (this.influxDBWriter?.isEnabled()) {
124
+ await this.influxDBWriter.writeBatch(batch);
125
+ }
126
+ // Emit batch event for custom listeners
127
+ this.emit('batch', {
128
+ batch_number: batchNumber,
129
+ batch_size: batch.length,
130
+ results: batch,
131
+ timestamp: Date.now()
132
+ });
97
133
  }
98
134
  catch (error) {
99
- logger_1.logger.error('Failed to initialize incremental files:', error);
135
+ logger_1.logger.error('Failed to flush metrics batch:', error);
100
136
  }
101
137
  }
102
- start() {
138
+ start(testName) {
103
139
  this.startTime = Date.now();
104
- this.results = [];
105
- this.errorDetails.clear();
106
- this.vuStartEvents = [];
107
- this.batchBuffer = [];
108
- this.batchCounter = 0;
109
- this.csvHeaderWritten = false;
110
- // Reset running statistics
111
- this.runningStats = {
112
- totalRequests: 0,
113
- successfulRequests: 0,
114
- failedRequests: 0,
115
- totalDuration: 0,
116
- minDuration: Infinity,
117
- maxDuration: 0,
118
- durations: [],
119
- };
140
+ this.currentTestId = `test-${this.startTime}`;
141
+ this.currentTestName = testName || 'unnamed-test';
142
+ // Reset all modules
143
+ this.statisticsEngine.reset();
144
+ this.errorTracker.clear();
145
+ this.resultStorage.clear();
146
+ this.batchProcessor.reset();
147
+ this.fileWriter.reset();
148
+ this.realtimeDispatcher.setStartTime(this.startTime);
149
+ // Start InfluxDB test if enabled
150
+ if (this.influxDBWriter?.isEnabled()) {
151
+ this.influxDBWriter.startTest(this.currentTestId, this.currentTestName);
152
+ }
120
153
  if (this.realtimeConfig.enabled && this.realtimeConfig.interval_ms) {
121
- this.startBatchTimer();
154
+ this.batchProcessor.start();
155
+ }
156
+ }
157
+ /**
158
+ * Set the test name/ID for InfluxDB tagging
159
+ */
160
+ setTestInfo(testName, testId) {
161
+ this.currentTestName = testName;
162
+ this.currentTestId = testId || `test-${Date.now()}`;
163
+ if (this.influxDBWriter?.isEnabled()) {
164
+ this.influxDBWriter.startTest(this.currentTestId, this.currentTestName);
122
165
  }
123
166
  }
124
167
  recordVUStart(vuId) {
125
- this.vuStartEvents.push({
126
- vu_id: vuId,
127
- start_time: Date.now(),
128
- load_pattern: this.loadPatternType
129
- });
168
+ this.resultStorage.recordVUStart(vuId);
130
169
  }
131
170
  recordResult(result) {
132
- // Update running statistics (always accurate regardless of stored results)
133
- this.runningStats.totalRequests++;
134
- if (result.success) {
135
- this.runningStats.successfulRequests++;
136
- const duration = result.duration || 0;
137
- this.runningStats.totalDuration += duration;
138
- this.runningStats.minDuration = Math.min(this.runningStats.minDuration, duration);
139
- this.runningStats.maxDuration = Math.max(this.runningStats.maxDuration, duration);
140
- // Keep limited durations for percentile calculation (reservoir sampling)
141
- if (this.runningStats.durations.length < this.maxDurationsForPercentiles) {
142
- this.runningStats.durations.push(duration);
143
- }
144
- else {
145
- // Randomly replace an existing duration (reservoir sampling)
146
- const replaceIndex = Math.floor(Math.random() * this.runningStats.totalRequests);
147
- if (replaceIndex < this.maxDurationsForPercentiles) {
148
- this.runningStats.durations[replaceIndex] = duration;
149
- }
150
- }
151
- }
152
- else {
153
- this.runningStats.failedRequests++;
154
- }
155
- // Store result only if under limit (for detailed analysis)
156
- if (this.results.length < this.maxStoredResults) {
157
- this.results.push(result);
158
- }
171
+ // Update running statistics
172
+ const duration = result.duration || 0;
173
+ this.statisticsEngine.recordResult(duration, result.success);
174
+ // Store result
175
+ this.resultStorage.addResult(result);
159
176
  this.emit('result', result);
160
- // Track detailed error information
177
+ // Track error details
161
178
  if (!result.success) {
162
- this.trackErrorDetail(result);
179
+ this.errorTracker.trackError(result);
163
180
  }
164
- // Add to batch buffer for real-time processing (with safety limit)
181
+ // Add to batch for real-time processing
165
182
  if (this.realtimeConfig.enabled) {
166
- // Safety limit: if buffer exceeds 1000 items, force flush to prevent memory issues
167
- if (this.batchBuffer.length >= 1000) {
168
- this.flushBatch();
169
- }
170
- this.batchBuffer.push(result);
171
- // Check if we should flush based on batch size (if not using intervals)
172
- if (!this.realtimeConfig.interval_ms) {
173
- const batchSize = this.realtimeConfig.batch_size || 10;
174
- if (this.batchBuffer.length >= batchSize) {
175
- this.flushBatch();
176
- }
177
- }
183
+ this.batchProcessor.add(result);
178
184
  }
179
185
  }
180
186
  recordError(vuId, scenario, action, error) {
@@ -191,528 +197,61 @@ class MetricsCollector extends events_1.EventEmitter {
191
197
  };
192
198
  this.recordResult(result);
193
199
  }
194
- async flushBatch() {
195
- if (this.batchBuffer.length === 0)
196
- return;
197
- const batch = [...this.batchBuffer];
198
- this.batchBuffer = [];
199
- this.batchCounter++;
200
- logger_1.logger.debug(`📤 Flushing batch #${this.batchCounter} with ${batch.length} results`);
201
- try {
202
- // Write to file if configured
203
- if (this.realtimeConfig.file_output?.enabled) {
204
- await this.writeBatchToFile(batch);
205
- }
206
- // Send to real-time endpoints
207
- if (this.realtimeConfig.endpoints) {
208
- await this.sendToRealTimeEndpoints(batch);
209
- }
210
- // Update incremental JSON/CSV files
211
- if (this.realtimeConfig.incremental_files?.enabled) {
212
- await this.updateIncrementalFiles(batch);
213
- }
214
- // Emit batch event for custom listeners
215
- this.emit('batch', {
216
- batch_number: this.batchCounter,
217
- batch_size: batch.length,
218
- results: batch,
219
- timestamp: Date.now()
220
- });
221
- }
222
- catch (error) {
223
- logger_1.logger.error('❌ Failed to flush metrics batch:', error);
224
- }
225
- }
226
- async writeBatchToFile(batch) {
227
- const config = this.realtimeConfig.file_output;
228
- try {
229
- const fs = require('fs').promises;
230
- const path = require('path');
231
- // Ensure directory exists
232
- const dir = path.dirname(config.path);
233
- await fs.mkdir(dir, { recursive: true });
234
- let content;
235
- if (config.format === 'csv') {
236
- content = this.formatBatchAsCSV(batch);
237
- }
238
- else {
239
- // JSONL format (default)
240
- content = batch.map(result => JSON.stringify({
241
- ...result,
242
- timestamp: new Date(result.timestamp).toISOString(),
243
- batch_number: this.batchCounter
244
- })).join('\n') + '\n';
245
- }
246
- await fs.appendFile(config.path, content);
247
- }
248
- catch (error) {
249
- logger_1.logger.error('❌ Failed to write batch to file:', error);
250
- }
251
- }
252
- formatBatchAsCSV(batch) {
253
- return batch.map(result => [
254
- new Date(result.timestamp).toISOString(),
255
- this.batchCounter,
256
- result.vu_id,
257
- result.scenario,
258
- result.action,
259
- result.step_name || '',
260
- result.duration,
261
- result.success,
262
- result.status || '',
263
- (result.error || '').replace(/"/g, '""') // Escape quotes
264
- ].join(',')).join('\n') + '\n';
265
- }
266
- async updateIncrementalFiles(batch) {
267
- const config = this.realtimeConfig.incremental_files;
268
- try {
269
- // Update incremental JSON file
270
- if (config.json_path) {
271
- await this.updateIncrementalJSON(batch, config.json_path);
272
- }
273
- // Update incremental CSV file
274
- if (config.csv_path) {
275
- await this.updateIncrementalCSV(batch, config.csv_path);
276
- }
277
- // Update summary files if configured
278
- if (config.update_summary) {
279
- await this.updateIncrementalSummary();
280
- }
281
- }
282
- catch (error) {
283
- logger_1.logger.error('❌ Failed to update incremental files:', error);
284
- }
285
- }
286
- async updateIncrementalJSON(batch, filePath) {
287
- const fs = require('fs').promises;
288
- try {
289
- // Read existing file
290
- const existingContent = await fs.readFile(filePath, 'utf8');
291
- let existingData = [];
292
- if (existingContent.trim()) {
293
- existingData = JSON.parse(existingContent);
294
- }
295
- // Append new batch
296
- const updatedData = [...existingData, ...batch];
297
- // Write back to file
298
- await fs.writeFile(filePath, JSON.stringify(updatedData, null, 2));
299
- }
300
- catch (error) {
301
- // If file doesn't exist or is corrupted, start fresh
302
- await fs.writeFile(filePath, JSON.stringify(batch, null, 2));
303
- }
304
- }
305
- async updateIncrementalCSV(batch, filePath) {
306
- const fs = require('fs').promises;
307
- const csvRows = batch.map(result => [
308
- new Date(result.timestamp).toISOString(),
309
- result.vu_id,
310
- result.scenario,
311
- result.action,
312
- result.step_name || '',
313
- result.duration,
314
- result.success,
315
- result.status || '',
316
- (result.error || '').replace(/"/g, '""'), // Escape quotes
317
- result.request_url || ''
318
- ].map(field => `"${field}"`).join(',')).join('\n') + '\n';
319
- await fs.appendFile(filePath, csvRows);
320
- }
321
- async updateIncrementalSummary() {
322
- const summary = this.getSummary();
323
- const fs = require('fs').promises;
324
- const path = require('path');
325
- const config = this.realtimeConfig.incremental_files;
326
- // Generate summary file paths based on the JSON path
327
- const basePath = config.json_path ? path.dirname(config.json_path) : 'results';
328
- const summaryJsonPath = path.join(basePath, 'summary-incremental.json');
329
- const summaryHtmlPath = path.join(basePath, 'summary-incremental.html');
330
- try {
331
- // Write JSON summary
332
- await fs.writeFile(summaryJsonPath, JSON.stringify({
333
- last_updated: new Date().toISOString(),
334
- test_duration: summary.total_duration,
335
- ...summary
336
- }, null, 2));
337
- // Generate simple HTML summary
338
- const htmlSummary = this.generateSimpleHTMLSummary(summary);
339
- await fs.writeFile(summaryHtmlPath, htmlSummary);
340
- }
341
- catch (error) {
342
- logger_1.logger.error('❌ Failed to update incremental summary:', error);
343
- }
344
- }
345
- generateSimpleHTMLSummary(summary) {
346
- const lastUpdated = new Date().toISOString();
347
- return `<!DOCTYPE html>
348
- <html>
349
- <head>
350
- <title>Load Test Summary (Live)</title>
351
- <meta http-equiv="refresh" content="5">
352
- <style>
353
- body { font-family: Arial, sans-serif; margin: 20px; }
354
- .metric { margin: 10px 0; padding: 10px; background: #f5f5f5; border-radius: 4px; }
355
- .success { color: #28a745; }
356
- .error { color: #dc3545; }
357
- .header { background: #007bff; color: white; padding: 15px; border-radius: 4px; }
358
- </style>
359
- </head>
360
- <body>
361
- <div class="header">
362
- <h1>🚀 Load Test Progress</h1>
363
- <p>Last Updated: ${lastUpdated}</p>
364
- <p>Test Duration: ${summary.total_duration.toFixed(1)}s</p>
365
- </div>
366
-
367
- <div class="metric">
368
- <h3>📊 Overall Statistics</h3>
369
- <p><strong>Total Requests:</strong> ${summary.total_requests}</p>
370
- <p><strong class="success">Successful:</strong> ${summary.successful_requests}</p>
371
- <p><strong class="error">Failed:</strong> ${summary.failed_requests}</p>
372
- <p><strong>Success Rate:</strong> ${summary.success_rate.toFixed(2)}%</p>
373
- </div>
374
-
375
- <div class="metric">
376
- <h3>⏱️ Response Times</h3>
377
- <p><strong>Average:</strong> ${summary.avg_response_time.toFixed(0)}ms</p>
378
- <p><strong>Min:</strong> ${summary.min_response_time}ms</p>
379
- <p><strong>Max:</strong> ${summary.max_response_time}ms</p>
380
- <p><strong>95th Percentile:</strong> ${summary.percentiles[95] || 0}ms</p>
381
- </div>
382
-
383
- <div class="metric">
384
- <h3>🔄 Throughput</h3>
385
- <p><strong>Requests/sec:</strong> ${summary.requests_per_second.toFixed(2)}</p>
386
- <p><strong>Bytes/sec:</strong> ${(summary.bytes_per_second || 0).toFixed(0)}</p>
387
- </div>
388
-
389
- ${summary.step_statistics.length > 0 ? `
390
- <div class="metric">
391
- <h3>📝 Step Statistics</h3>
392
- ${summary.step_statistics.slice(0, 5).map(step => `
393
- <div style="margin: 10px 0; padding: 8px; background: white; border-left: 4px solid ${step.success_rate > 95 ? '#28a745' : '#ffc107'};">
394
- <strong>${step.step_name}</strong> (${step.scenario})
395
- <br>Success: ${step.success_rate.toFixed(1)}% | Avg: ${step.avg_response_time.toFixed(0)}ms | Count: ${step.total_requests}
396
- </div>
397
- `).join('')}
398
- </div>
399
- ` : ''}
400
-
401
- <div class="metric">
402
- <small>Auto-refreshes every 5 seconds</small>
403
- </div>
404
- </body>
405
- </html>`;
406
- }
407
- async sendToRealTimeEndpoints(batch) {
408
- if (!this.realtimeConfig.endpoints)
409
- return;
410
- 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)));
411
- await Promise.allSettled(promises);
412
- }
413
- async sendToEndpoint(batch, endpoint) {
414
- switch (endpoint.type) {
415
- case 'graphite':
416
- await this.sendToGraphite(batch, endpoint);
417
- break;
418
- case 'webhook':
419
- await this.sendToWebhook(batch, endpoint);
420
- break;
421
- case 'influxdb':
422
- await this.sendToInfluxDB(batch, endpoint);
423
- break;
424
- case 'websocket':
425
- await this.sendToWebSocket(batch, endpoint);
426
- break;
427
- default:
428
- logger_1.logger.warn(`⚠️ Unknown endpoint type: ${endpoint.type}`);
429
- }
430
- }
431
- async sendToGraphite(batch, config) {
432
- const net = require('net');
433
- return new Promise((resolve, reject) => {
434
- const client = net.createConnection(config.port, config.host);
435
- client.on('connect', () => {
436
- const metrics = batch.map(result => {
437
- const timestamp = Math.floor(result.timestamp / 1000);
438
- const metricName = `loadtest.${result.scenario}.${result.step_name || result.action}`;
439
- return [
440
- `${metricName}.duration ${result.duration} ${timestamp}`,
441
- `${metricName}.success ${result.success ? 1 : 0} ${timestamp}`,
442
- `${metricName}.count 1 ${timestamp}`
443
- ].join('\n');
444
- }).join('\n') + '\n';
445
- client.write(metrics);
446
- client.end();
447
- });
448
- client.on('close', () => resolve());
449
- client.on('error', reject);
450
- setTimeout(() => {
451
- client.destroy();
452
- reject(new Error('Graphite connection timeout'));
453
- }, 5000);
454
- });
455
- }
456
- async sendToWebhook(batch, config) {
457
- const response = await fetch(config.url, {
458
- method: 'POST',
459
- headers: {
460
- 'Content-Type': 'application/json',
461
- ...config.headers
462
- },
463
- body: JSON.stringify({
464
- timestamp: new Date().toISOString(),
465
- batch_number: this.batchCounter,
466
- batch_size: batch.length,
467
- test_start_time: new Date(this.startTime).toISOString(),
468
- results: batch
469
- })
470
- });
471
- if (!response.ok) {
472
- throw new Error(`Webhook failed: ${response.status} ${response.statusText}`);
473
- }
474
- }
475
- async sendToInfluxDB(batch, config) {
476
- const lines = batch.map(result => {
477
- const tags = [
478
- `scenario=${result.scenario}`,
479
- `step=${result.step_name || result.action}`,
480
- `vu_id=${result.vu_id}`,
481
- `success=${result.success}`
482
- ].join(',');
483
- const fields = [
484
- `duration=${result.duration}`,
485
- `success=${result.success ? 'true' : 'false'}`,
486
- `batch_number=${this.batchCounter}i`
487
- ];
488
- if (result.status) {
489
- fields.push(`status=${result.status}i`);
490
- }
491
- const timestamp = result.timestamp * 1000000; // Convert to nanoseconds
492
- return `loadtest,${tags} ${fields.join(',')} ${timestamp}`;
493
- }).join('\n');
494
- const response = await fetch(`${config.url}/write?db=${config.database}`, {
495
- method: 'POST',
496
- headers: {
497
- 'Authorization': `Bearer ${config.token}`,
498
- 'Content-Type': 'text/plain'
499
- },
500
- body: lines
501
- });
502
- if (!response.ok) {
503
- const errorText = await response.text();
504
- throw new Error(`InfluxDB write failed: ${response.status} ${errorText}`);
505
- }
506
- }
507
- async sendToWebSocket(batch, config) {
508
- return new Promise((resolve, reject) => {
509
- const WebSocket = require('ws');
510
- const ws = new WebSocket(config.url);
511
- ws.on('open', () => {
512
- ws.send(JSON.stringify({
513
- type: 'metrics_batch',
514
- timestamp: new Date().toISOString(),
515
- batch_number: this.batchCounter,
516
- test_start_time: new Date(this.startTime).toISOString(),
517
- data: batch
518
- }));
519
- ws.close();
520
- resolve();
521
- });
522
- ws.on('error', reject);
523
- setTimeout(() => {
524
- ws.close();
525
- reject(new Error('WebSocket connection timeout'));
526
- }, 5000);
527
- });
528
- }
529
- // Force flush when test completes or stops
530
200
  async finalize() {
531
- if (this.batchTimer) {
532
- clearInterval(this.batchTimer);
533
- this.batchTimer = null;
534
- }
535
- // Flush any remaining results
536
- if (this.batchBuffer.length > 0) {
537
- await this.flushBatch();
538
- }
539
- logger_1.logger.info(`📊 Metrics collection finalized. Total batches: ${this.batchCounter}, Total results: ${this.results.length}`);
201
+ await this.batchProcessor.finalize();
202
+ // Write summary to InfluxDB and finalize
203
+ if (this.influxDBWriter?.isEnabled()) {
204
+ const summary = this.getSummary();
205
+ await this.influxDBWriter.writeSummary(summary);
206
+ await this.influxDBWriter.finalize();
207
+ }
208
+ logger_1.logger.info(`Metrics collection finalized. Total batches: ${this.batchProcessor.getBatchCounter()}, Total results: ${this.resultStorage.getResultCount()}`);
540
209
  }
541
- trackErrorDetail(result) {
542
- const errorKey = `${result.scenario}:${result.action}:${result.status || 'NO_STATUS'}:${result.error}`;
543
- const existing = this.errorDetails.get(errorKey);
544
- if (existing) {
545
- existing.count++;
546
- }
547
- else {
548
- this.errorDetails.set(errorKey, {
549
- timestamp: result.timestamp,
550
- vu_id: result.vu_id,
551
- scenario: result.scenario,
552
- action: result.action,
553
- status: result.status,
554
- error: result.error || 'Unknown error',
555
- request_url: result.request_url,
556
- response_body: result.response_body,
557
- count: 1
558
- });
210
+ /**
211
+ * Record a network call to InfluxDB (if enabled)
212
+ */
213
+ recordNetworkCall(call) {
214
+ if (this.influxDBWriter?.isEnabled()) {
215
+ this.influxDBWriter.writeNetworkCall(call);
559
216
  }
560
217
  }
218
+ /**
219
+ * Get the InfluxDB writer instance (for direct access if needed)
220
+ */
221
+ getInfluxDBWriter() {
222
+ return this.influxDBWriter;
223
+ }
561
224
  getResults() {
562
- return [...this.results];
225
+ return this.resultStorage.getResults();
563
226
  }
564
227
  getSummary() {
565
- // Use running statistics for accurate totals (even when individual results are limited)
566
- const totalRequests = this.runningStats.totalRequests;
567
- const successfulRequests = this.runningStats.successfulRequests;
568
- const failedRequests = this.runningStats.failedRequests;
569
- // Use sampled durations for percentiles (reservoir sampling ensures representative sample)
570
- const durations = this.runningStats.durations;
571
- const totalDuration = (Date.now() - this.startTime) / 1000;
572
- // Calculate average from running totals (accurate even with limited stored results)
573
- const avgResponseTime = successfulRequests > 0
574
- ? this.runningStats.totalDuration / successfulRequests
575
- : 0;
576
- // Error distribution from stored results (may be limited but representative)
577
- const errorDistribution = {};
578
- this.results.filter(r => !r.success).forEach(r => {
579
- const error = r.error || 'Unknown error';
580
- errorDistribution[error] = (errorDistribution[error] || 0) + 1;
581
- });
582
- // Status code distribution from stored results
583
- const statusDistribution = {};
584
- this.results.forEach(r => {
585
- if (r.status) {
586
- statusDistribution[r.status] = (statusDistribution[r.status] || 0) + 1;
587
- }
228
+ return this.summaryGenerator.generate({
229
+ statisticsEngine: this.statisticsEngine,
230
+ errorTracker: this.errorTracker,
231
+ results: this.resultStorage.getResults(),
232
+ vuStartEvents: this.resultStorage.getVUStartEvents(),
233
+ startTime: this.startTime
588
234
  });
589
- const responseSizes = this.results
590
- .filter(r => r.response_size)
591
- .map(r => r.response_size);
592
- return {
593
- total_requests: totalRequests,
594
- successful_requests: successfulRequests,
595
- failed_requests: failedRequests,
596
- success_rate: totalRequests > 0 ? (successfulRequests / totalRequests) * 100 : 0,
597
- avg_response_time: avgResponseTime,
598
- min_response_time: this.runningStats.minDuration === Infinity ? 0 : this.runningStats.minDuration,
599
- max_response_time: this.runningStats.maxDuration,
600
- percentiles: this.calculatePercentiles(durations),
601
- requests_per_second: totalDuration > 0 ? (totalRequests / totalDuration) : 0,
602
- bytes_per_second: responseSizes.length > 0 && totalDuration > 0
603
- ? (responseSizes.reduce((a, b) => a + b, 0) / totalDuration) : 0,
604
- total_duration: totalDuration,
605
- error_distribution: errorDistribution,
606
- status_distribution: statusDistribution,
607
- error_details: Array.from(this.errorDetails.values()).sort((a, b) => b.count - a.count),
608
- // New enhanced statistics
609
- step_statistics: this.calculateStepStatistics(),
610
- vu_ramp_up: this.vuStartEvents,
611
- timeline_data: this.calculateTimelineData()
612
- };
613
- }
614
- calculateStepStatistics() {
615
- const stepGroups = new Map();
616
- // Group results by step name and scenario
617
- this.results.forEach(result => {
618
- const key = `${result.scenario}:${result.step_name || result.action}`;
619
- if (!stepGroups.has(key)) {
620
- stepGroups.set(key, []);
621
- }
622
- stepGroups.get(key).push(result);
623
- });
624
- const stepStats = [];
625
- for (const [key, results] of stepGroups) {
626
- const [scenario, stepName] = key.split(':');
627
- const successfulResults = results.filter(r => r.success);
628
- // Include ALL results (both successful and failed) for response time calculations
629
- // Failed requests also have response times that should be included in statistics
630
- const responseTimes = results
631
- .map(r => r.response_time || r.duration || 0)
632
- .filter(rt => rt > 0);
633
- // Error distribution for this step
634
- const errorDistribution = {};
635
- results.filter(r => !r.success).forEach(r => {
636
- const error = r.error || 'Unknown error';
637
- errorDistribution[error] = (errorDistribution[error] || 0) + 1;
638
- });
639
- // Status distribution for this step
640
- const statusDistribution = {};
641
- results.forEach(r => {
642
- if (r.status) {
643
- statusDistribution[r.status] = (statusDistribution[r.status] || 0) + 1;
644
- }
645
- });
646
- stepStats.push({
647
- step_name: stepName,
648
- scenario: scenario,
649
- total_requests: results.length,
650
- successful_requests: successfulResults.length,
651
- failed_requests: results.length - successfulResults.length,
652
- success_rate: results.length > 0 ? (successfulResults.length / results.length) * 100 : 0,
653
- avg_response_time: responseTimes.length > 0 ? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length : 0,
654
- min_response_time: responseTimes.length > 0 ? Math.min(...responseTimes) : 0,
655
- max_response_time: responseTimes.length > 0 ? Math.max(...responseTimes) : 0,
656
- percentiles: this.calculatePercentiles(responseTimes),
657
- response_times: responseTimes,
658
- error_distribution: errorDistribution,
659
- status_distribution: statusDistribution
660
- });
661
- }
662
- return stepStats.sort((a, b) => b.total_requests - a.total_requests);
663
- }
664
- calculateTimelineData() {
665
- if (this.results.length === 0)
666
- return [];
667
- const intervalMs = 5000; // 5 second intervals
668
- const startTime = this.startTime;
669
- const endTime = Date.now();
670
- const timeline = [];
671
- for (let time = startTime; time <= endTime; time += intervalMs) {
672
- const intervalResults = this.results.filter(r => r.timestamp >= time && r.timestamp < time + intervalMs);
673
- const successfulResults = intervalResults.filter(r => r.success);
674
- // Calculate active VUs at this time
675
- const activeVUs = this.vuStartEvents.filter(vu => vu.start_time <= time).length;
676
- timeline.push({
677
- timestamp: time,
678
- time_label: new Date(time).toISOString(),
679
- active_vus: activeVUs,
680
- requests_count: intervalResults.length,
681
- avg_response_time: successfulResults.length > 0
682
- ? successfulResults.reduce((sum, r) => sum + r.duration, 0) / successfulResults.length
683
- : 0,
684
- success_rate: intervalResults.length > 0
685
- ? (successfulResults.length / intervalResults.length) * 100
686
- : 0,
687
- throughput: intervalResults.length / (intervalMs / 1000)
688
- });
689
- }
690
- return timeline;
691
- }
692
- calculatePercentiles(values) {
693
- if (values.length === 0)
694
- return {};
695
- const sorted = [...values].sort((a, b) => a - b);
696
- const percentiles = [50, 90, 95, 99, 99.9, 99.99];
697
- const result = {};
698
- percentiles.forEach(p => {
699
- const index = Math.ceil((p / 100) * sorted.length) - 1;
700
- result[p] = sorted[Math.max(0, index)];
701
- });
702
- return result;
703
235
  }
704
236
  clear() {
705
- if (this.batchTimer) {
706
- clearInterval(this.batchTimer);
707
- this.batchTimer = null;
708
- }
709
- this.results = [];
710
- this.errorDetails.clear();
711
- this.vuStartEvents = [];
712
- this.batchBuffer = [];
713
- this.batchCounter = 0;
714
- this.csvHeaderWritten = false;
237
+ this.batchProcessor.stop();
238
+ this.statisticsEngine.reset();
239
+ this.errorTracker.clear();
240
+ this.resultStorage.clear();
241
+ this.batchProcessor.reset();
242
+ this.fileWriter.reset();
715
243
  this.startTime = 0;
244
+ this.currentTestId = '';
245
+ this.currentTestName = '';
246
+ }
247
+ /**
248
+ * Close all connections (call when done with the collector)
249
+ */
250
+ async close() {
251
+ this.clear();
252
+ if (this.influxDBWriter) {
253
+ await this.influxDBWriter.close();
254
+ }
716
255
  }
717
256
  }
718
257
  exports.MetricsCollector = MetricsCollector;