@testsmith/perfornium 0.6.3 → 0.6.5

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 (191) hide show
  1. package/dist/cli/commands/distributed.js +2 -2
  2. package/dist/cli/commands/report.js +2 -2
  3. package/dist/cli/commands/run.js +2 -0
  4. package/dist/config/parser.js +2 -2
  5. package/dist/config/types/global-config.d.ts +82 -2
  6. package/dist/config/types/scenario-config.d.ts +2 -2
  7. package/dist/core/data/data-manager.d.ts +70 -0
  8. package/dist/core/data/data-manager.js +186 -0
  9. package/dist/core/data/data-provider.d.ts +85 -0
  10. package/dist/core/data/data-provider.js +468 -0
  11. package/dist/core/data/index.d.ts +8 -0
  12. package/dist/core/data/index.js +13 -0
  13. package/dist/core/execution/check-evaluator.d.ts +10 -0
  14. package/dist/core/execution/check-evaluator.js +79 -0
  15. package/dist/core/execution/data-extractor.d.ts +6 -0
  16. package/dist/core/execution/data-extractor.js +70 -0
  17. package/dist/core/execution/index.d.ts +3 -0
  18. package/dist/core/execution/index.js +9 -0
  19. package/dist/core/execution/json-payload-processor.d.ts +7 -0
  20. package/dist/core/execution/json-payload-processor.js +140 -0
  21. package/dist/core/factories/index.d.ts +2 -0
  22. package/dist/core/factories/index.js +7 -0
  23. package/dist/core/factories/output-handler-factory.d.ts +10 -0
  24. package/dist/core/factories/output-handler-factory.js +91 -0
  25. package/dist/core/factories/protocol-handler-factory.d.ts +12 -0
  26. package/dist/core/factories/protocol-handler-factory.js +96 -0
  27. package/dist/core/index.d.ts +3 -2
  28. package/dist/core/index.js +8 -3
  29. package/dist/core/reporting/dashboard-reporter.d.ts +17 -0
  30. package/dist/core/reporting/dashboard-reporter.js +127 -0
  31. package/dist/core/reporting/index.d.ts +1 -0
  32. package/dist/core/reporting/index.js +5 -0
  33. package/dist/core/step-executor.d.ts +6 -20
  34. package/dist/core/step-executor.js +72 -366
  35. package/dist/core/strategies/index.d.ts +2 -0
  36. package/dist/core/strategies/index.js +7 -0
  37. package/dist/core/strategies/scenario-selector.d.ts +13 -0
  38. package/dist/core/strategies/scenario-selector.js +37 -0
  39. package/dist/core/strategies/think-time-strategy.d.ts +15 -0
  40. package/dist/core/strategies/think-time-strategy.js +71 -0
  41. package/dist/core/test-runner.d.ts +4 -11
  42. package/dist/core/test-runner.js +105 -312
  43. package/dist/core/virtual-user.d.ts +7 -37
  44. package/dist/core/virtual-user.js +29 -269
  45. package/dist/dashboard/routes/api.d.ts +64 -0
  46. package/dist/dashboard/routes/api.js +569 -0
  47. package/dist/dashboard/routes/index.d.ts +2 -0
  48. package/dist/dashboard/routes/index.js +7 -0
  49. package/dist/dashboard/routes/static.d.ts +6 -0
  50. package/dist/dashboard/routes/static.js +76 -0
  51. package/dist/dashboard/server.d.ts +8 -84
  52. package/dist/dashboard/server.js +76 -2007
  53. package/dist/dashboard/services/file-scanner.d.ts +7 -0
  54. package/dist/dashboard/services/file-scanner.js +114 -0
  55. package/dist/dashboard/services/index.d.ts +5 -0
  56. package/dist/dashboard/services/index.js +13 -0
  57. package/dist/dashboard/services/influxdb-service.d.ts +41 -0
  58. package/dist/dashboard/services/influxdb-service.js +329 -0
  59. package/dist/dashboard/services/metrics-parser.d.ts +12 -0
  60. package/dist/dashboard/services/metrics-parser.js +209 -0
  61. package/dist/dashboard/services/results-manager.d.ts +17 -0
  62. package/dist/dashboard/services/results-manager.js +311 -0
  63. package/dist/dashboard/services/test-executor.d.ts +41 -0
  64. package/dist/dashboard/services/test-executor.js +250 -0
  65. package/dist/dashboard/services/workers-manager.d.ts +13 -0
  66. package/dist/dashboard/services/workers-manager.js +81 -0
  67. package/dist/dashboard/templates/index.html +122 -0
  68. package/dist/dashboard/templates/scripts/main.js +3280 -0
  69. package/dist/dashboard/templates/styles.css +402 -0
  70. package/dist/dashboard/types.d.ts +168 -0
  71. package/dist/dashboard/types.js +2 -0
  72. package/dist/distributed/result-aggregator.js +1 -3
  73. package/dist/metrics/batch/batch-processor.d.ts +27 -0
  74. package/dist/metrics/batch/batch-processor.js +85 -0
  75. package/dist/metrics/batch/index.d.ts +1 -0
  76. package/dist/metrics/batch/index.js +5 -0
  77. package/dist/metrics/collector.d.ts +46 -45
  78. package/dist/metrics/collector.js +179 -640
  79. package/dist/metrics/core/error-tracker.d.ts +9 -0
  80. package/dist/metrics/core/error-tracker.js +52 -0
  81. package/dist/metrics/core/index.d.ts +3 -0
  82. package/dist/metrics/core/index.js +9 -0
  83. package/dist/metrics/core/result-storage.d.ts +19 -0
  84. package/dist/metrics/core/result-storage.js +56 -0
  85. package/dist/metrics/core/statistics-engine.d.ts +27 -0
  86. package/dist/metrics/core/statistics-engine.js +91 -0
  87. package/dist/metrics/output/file-writer.d.ts +19 -0
  88. package/dist/metrics/output/file-writer.js +129 -0
  89. package/dist/metrics/output/index.d.ts +2 -0
  90. package/dist/metrics/output/index.js +10 -0
  91. package/dist/metrics/output/influxdb-writer.d.ts +89 -0
  92. package/dist/metrics/output/influxdb-writer.js +404 -0
  93. package/dist/metrics/realtime/dispatcher.d.ts +18 -0
  94. package/dist/metrics/realtime/dispatcher.js +45 -0
  95. package/dist/metrics/realtime/endpoints/graphite.d.ts +3 -0
  96. package/dist/metrics/realtime/endpoints/graphite.js +61 -0
  97. package/dist/metrics/realtime/endpoints/influxdb.d.ts +3 -0
  98. package/dist/metrics/realtime/endpoints/influxdb.js +35 -0
  99. package/dist/metrics/realtime/endpoints/webhook.d.ts +3 -0
  100. package/dist/metrics/realtime/endpoints/webhook.js +22 -0
  101. package/dist/metrics/realtime/endpoints/websocket.d.ts +3 -0
  102. package/dist/metrics/realtime/endpoints/websocket.js +25 -0
  103. package/dist/metrics/realtime/index.d.ts +5 -0
  104. package/dist/metrics/realtime/index.js +13 -0
  105. package/dist/metrics/reporting/index.d.ts +3 -0
  106. package/dist/metrics/reporting/index.js +9 -0
  107. package/dist/metrics/reporting/step-statistics.d.ts +6 -0
  108. package/dist/metrics/reporting/step-statistics.js +59 -0
  109. package/dist/metrics/reporting/summary-generator.d.ts +16 -0
  110. package/dist/metrics/reporting/summary-generator.js +46 -0
  111. package/dist/metrics/reporting/timeline-calculator.d.ts +7 -0
  112. package/dist/metrics/reporting/timeline-calculator.js +86 -0
  113. package/dist/metrics/types.d.ts +58 -0
  114. package/dist/outputs/csv.d.ts +2 -0
  115. package/dist/outputs/csv.js +21 -2
  116. package/dist/outputs/json.js +6 -2
  117. package/dist/protocols/rest/handler.d.ts +4 -53
  118. package/dist/protocols/rest/handler.js +73 -454
  119. package/dist/protocols/rest/request/auth-handler.d.ts +4 -0
  120. package/dist/protocols/rest/request/auth-handler.js +30 -0
  121. package/dist/protocols/rest/request/body-processor.d.ts +11 -0
  122. package/dist/protocols/rest/request/body-processor.js +62 -0
  123. package/dist/protocols/rest/request/index.d.ts +2 -0
  124. package/dist/protocols/rest/request/index.js +7 -0
  125. package/dist/protocols/rest/response/checks.d.ts +6 -0
  126. package/dist/protocols/rest/response/checks.js +71 -0
  127. package/dist/protocols/rest/response/index.d.ts +2 -0
  128. package/dist/protocols/rest/response/index.js +7 -0
  129. package/dist/protocols/rest/response/size-calculator.d.ts +12 -0
  130. package/dist/protocols/rest/response/size-calculator.js +64 -0
  131. package/dist/protocols/web/browser/highlight.d.ts +7 -0
  132. package/dist/protocols/web/browser/highlight.js +47 -0
  133. package/dist/protocols/web/browser/index.d.ts +4 -0
  134. package/dist/protocols/web/browser/index.js +11 -0
  135. package/dist/protocols/web/browser/manager.d.ts +20 -0
  136. package/dist/protocols/web/browser/manager.js +189 -0
  137. package/dist/protocols/web/browser/screenshot.d.ts +8 -0
  138. package/dist/protocols/web/browser/screenshot.js +69 -0
  139. package/dist/protocols/web/browser/storage.d.ts +5 -0
  140. package/dist/protocols/web/browser/storage.js +45 -0
  141. package/dist/protocols/web/commands/index.d.ts +5 -0
  142. package/dist/protocols/web/commands/index.js +11 -0
  143. package/dist/protocols/web/commands/interaction.d.ts +13 -0
  144. package/dist/protocols/web/commands/interaction.js +68 -0
  145. package/dist/protocols/web/commands/measurement.d.ts +16 -0
  146. package/dist/protocols/web/commands/measurement.js +33 -0
  147. package/dist/protocols/web/commands/navigation.d.ts +11 -0
  148. package/dist/protocols/web/commands/navigation.js +43 -0
  149. package/dist/protocols/web/commands/types.d.ts +12 -0
  150. package/dist/protocols/web/commands/types.js +2 -0
  151. package/dist/protocols/web/commands/verification.d.ts +11 -0
  152. package/dist/protocols/web/commands/verification.js +98 -0
  153. package/dist/protocols/web/handler.d.ts +19 -30
  154. package/dist/protocols/web/handler.js +160 -650
  155. package/dist/protocols/web/network/capture.d.ts +19 -0
  156. package/dist/protocols/web/network/capture.js +225 -0
  157. package/dist/protocols/web/network/filters.d.ts +5 -0
  158. package/dist/protocols/web/network/filters.js +49 -0
  159. package/dist/protocols/web/network/index.d.ts +4 -0
  160. package/dist/protocols/web/network/index.js +9 -0
  161. package/dist/protocols/web/network/types.d.ts +13 -0
  162. package/dist/protocols/web/network/types.js +2 -0
  163. package/dist/protocols/web/network/utils.d.ts +8 -0
  164. package/dist/protocols/web/network/utils.js +29 -0
  165. package/dist/recorder/native-recorder.js +2 -1
  166. package/dist/reporting/chart-data/index.d.ts +5 -0
  167. package/dist/reporting/chart-data/index.js +13 -0
  168. package/dist/reporting/chart-data/network.d.ts +25 -0
  169. package/dist/reporting/chart-data/network.js +78 -0
  170. package/dist/reporting/chart-data/scenario.d.ts +37 -0
  171. package/dist/reporting/chart-data/scenario.js +76 -0
  172. package/dist/reporting/chart-data/step-statistics.d.ts +24 -0
  173. package/dist/reporting/chart-data/step-statistics.js +94 -0
  174. package/dist/reporting/chart-data/throughput.d.ts +16 -0
  175. package/dist/reporting/chart-data/throughput.js +24 -0
  176. package/dist/reporting/chart-data/timeline.d.ts +17 -0
  177. package/dist/reporting/chart-data/timeline.js +46 -0
  178. package/dist/reporting/handlebars-helpers.d.ts +1 -0
  179. package/dist/reporting/handlebars-helpers.js +63 -0
  180. package/dist/reporting/{enhanced-html-generator.d.ts → html-generator.d.ts} +1 -1
  181. package/dist/reporting/{enhanced-html-generator.js → html-generator.js} +10 -7
  182. package/dist/reporting/templates/{enhanced-report.hbs → report.hbs} +9 -9
  183. package/dist/utils/data-utils.d.ts +17 -0
  184. package/dist/utils/data-utils.js +129 -0
  185. package/dist/utils/template.js +2 -2
  186. package/package.json +5 -2
  187. package/dist/core/csv-data-provider.d.ts +0 -47
  188. package/dist/core/csv-data-provider.js +0 -265
  189. package/dist/reporting/generator.d.ts +0 -42
  190. package/dist/reporting/generator.js +0 -1217
  191. package/dist/reporting/templates/html.hbs +0 -2453
@@ -0,0 +1,569 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApiRoutes = void 0;
4
+ const influxdb_service_1 = require("../services/influxdb-service");
5
+ const influxdb_writer_1 = require("../../metrics/output/influxdb-writer");
6
+ const logger_1 = require("../../utils/logger");
7
+ class ApiRoutes {
8
+ constructor(fileScanner, resultsManager, testExecutor, workersManager, liveTests, callbacks, influxService) {
9
+ this.testMetricsWriter = null;
10
+ this.fileScanner = fileScanner;
11
+ this.resultsManager = resultsManager;
12
+ this.testExecutor = testExecutor;
13
+ this.workersManager = workersManager;
14
+ this.liveTests = liveTests;
15
+ this.onInfraUpdate = callbacks?.onInfraUpdate;
16
+ this.influxService = influxService || new influxdb_service_1.InfluxDBService();
17
+ }
18
+ async initialize() {
19
+ await this.influxService.connect();
20
+ // Initialize test metrics writer for querying stored test data
21
+ this.testMetricsWriter = await (0, influxdb_writer_1.initInfluxDBWriter)();
22
+ }
23
+ async handleGetResults(res) {
24
+ const results = await this.resultsManager.scanResults();
25
+ res.writeHead(200, { 'Content-Type': 'application/json' });
26
+ res.end(JSON.stringify(results));
27
+ }
28
+ async handleGetResult(res, id) {
29
+ // First load the base result from file (for metadata, summary, etc.)
30
+ const fullResult = await this.resultsManager.loadFullResult(id);
31
+ if (!fullResult) {
32
+ res.writeHead(404, { 'Content-Type': 'application/json' });
33
+ res.end(JSON.stringify({ error: 'Result not found' }));
34
+ return;
35
+ }
36
+ // Add source metadata
37
+ const sourceInfo = {
38
+ summary: 'file', // Summary/metadata always from file (contains aggregated stats)
39
+ individual_results: 'file', // Will be updated to 'influxdb' if available
40
+ network_calls: fullResult.network_calls?.length ? 'file' : 'none',
41
+ infrastructure_metrics: fullResult.infrastructure_metrics ? 'file' : 'none'
42
+ };
43
+ // Try to load individual test results from InfluxDB
44
+ if (this.testMetricsWriter?.isEnabled() && fullResult.timestamp && fullResult.duration) {
45
+ try {
46
+ const startTime = new Date(fullResult.timestamp);
47
+ const endTime = new Date(startTime.getTime() + (fullResult.duration * 1000));
48
+ logger_1.logger.info(`Querying InfluxDB for test: "${fullResult.name}", time range: ${startTime.toISOString()} - ${endTime.toISOString()}`);
49
+ // Query test results from InfluxDB
50
+ const influxResults = await this.testMetricsWriter.queryResults({
51
+ testName: fullResult.name,
52
+ startTime,
53
+ endTime
54
+ });
55
+ logger_1.logger.info(`InfluxDB returned ${influxResults.length} test results`);
56
+ if (influxResults.length > 0) {
57
+ // Replace file-based results with InfluxDB results
58
+ fullResult.results = influxResults;
59
+ sourceInfo.individual_results = 'influxdb';
60
+ sourceInfo.influxdb_result_count = influxResults.length;
61
+ }
62
+ // Query network calls from InfluxDB
63
+ const influxNetworkCalls = await this.testMetricsWriter.queryNetworkCalls({
64
+ testName: fullResult.name,
65
+ startTime,
66
+ endTime
67
+ });
68
+ logger_1.logger.info(`InfluxDB returned ${influxNetworkCalls.length} network calls`);
69
+ if (influxNetworkCalls.length > 0) {
70
+ fullResult.network_calls = influxNetworkCalls;
71
+ sourceInfo.network_calls = 'influxdb';
72
+ sourceInfo.influxdb_network_call_count = influxNetworkCalls.length;
73
+ }
74
+ }
75
+ catch (e) {
76
+ logger_1.logger.error(`Could not fetch test results from InfluxDB: ${e.message}`);
77
+ }
78
+ }
79
+ else {
80
+ logger_1.logger.debug(`InfluxDB query skipped: enabled=${this.testMetricsWriter?.isEnabled()}, timestamp=${fullResult.timestamp}, duration=${fullResult.duration}`);
81
+ }
82
+ // Check if we can augment infrastructure from InfluxDB
83
+ if (!fullResult.infrastructure_metrics || Object.keys(fullResult.infrastructure_metrics).length === 0) {
84
+ if (this.influxService.isEnabled() && fullResult.timestamp && fullResult.duration) {
85
+ try {
86
+ const startTime = new Date(fullResult.timestamp);
87
+ const endTime = new Date(startTime.getTime() + (fullResult.duration * 1000));
88
+ const infraFromDB = await this.influxService.queryMetricsByTestRun('', startTime, endTime);
89
+ if (Object.keys(infraFromDB).length > 0) {
90
+ fullResult.infrastructure_metrics = infraFromDB;
91
+ sourceInfo.infrastructure_metrics = 'influxdb';
92
+ }
93
+ }
94
+ catch (e) {
95
+ logger_1.logger.debug(`Could not fetch infra from InfluxDB: ${e.message}`);
96
+ }
97
+ }
98
+ }
99
+ // Add source info to response
100
+ fullResult._source = sourceInfo;
101
+ res.writeHead(200, { 'Content-Type': 'application/json' });
102
+ res.end(JSON.stringify(fullResult));
103
+ }
104
+ async handleDeleteResult(res, id) {
105
+ try {
106
+ await this.resultsManager.deleteResult(id);
107
+ res.writeHead(200, { 'Content-Type': 'application/json' });
108
+ res.end(JSON.stringify({ status: 'deleted', id: decodeURIComponent(id) }));
109
+ }
110
+ catch (e) {
111
+ logger_1.logger.error(`Failed to delete result ${id}:`, e.message);
112
+ res.writeHead(404, { 'Content-Type': 'application/json' });
113
+ res.end(JSON.stringify({ error: 'Result not found', details: e.message }));
114
+ }
115
+ }
116
+ async handleExportResult(res, id, url) {
117
+ try {
118
+ const format = (url.searchParams.get('format') || 'json');
119
+ const includeNetworkCalls = url.searchParams.get('includeNetworkCalls') === 'true';
120
+ const fullResult = await this.resultsManager.loadFullResult(id);
121
+ if (!fullResult) {
122
+ res.writeHead(404, { 'Content-Type': 'application/json' });
123
+ res.end(JSON.stringify({ error: 'Result not found' }));
124
+ return;
125
+ }
126
+ // Optionally exclude network calls
127
+ const exportData = includeNetworkCalls ? fullResult : { ...fullResult, network_calls: undefined };
128
+ if (format === 'csv') {
129
+ // Export as CSV
130
+ const csv = this.resultToCSV(exportData, includeNetworkCalls);
131
+ res.writeHead(200, {
132
+ 'Content-Type': 'text/csv',
133
+ 'Content-Disposition': `attachment; filename="${fullResult.name}-${new Date(fullResult.timestamp).toISOString().slice(0, 10)}.csv"`
134
+ });
135
+ res.end(csv);
136
+ }
137
+ else {
138
+ // Export as JSON
139
+ res.writeHead(200, {
140
+ 'Content-Type': 'application/json',
141
+ 'Content-Disposition': `attachment; filename="${fullResult.name}-${new Date(fullResult.timestamp).toISOString().slice(0, 10)}.json"`
142
+ });
143
+ res.end(JSON.stringify(exportData, null, 2));
144
+ }
145
+ }
146
+ catch (e) {
147
+ logger_1.logger.error(`Failed to export result ${id}:`, e.message);
148
+ res.writeHead(500, { 'Content-Type': 'application/json' });
149
+ res.end(JSON.stringify({ error: 'Export failed', details: e.message }));
150
+ }
151
+ }
152
+ async handleImportResult(req, res) {
153
+ try {
154
+ const body = await this.readBody(req);
155
+ const data = JSON.parse(body);
156
+ // Validate required fields
157
+ if (!data.summary) {
158
+ res.writeHead(400, { 'Content-Type': 'application/json' });
159
+ res.end(JSON.stringify({ error: 'Invalid result format: missing summary' }));
160
+ return;
161
+ }
162
+ // Generate a unique ID if not present
163
+ const timestamp = data.timestamp || new Date().toISOString();
164
+ const name = data.name || 'Imported Result';
165
+ const timestampStr = new Date(timestamp).toISOString().replace(/[:.]/g, '-').slice(0, 19);
166
+ const id = data.id || `${name}-${timestampStr}`;
167
+ // Save to results directory
168
+ const result = await this.resultsManager.saveResult(id, {
169
+ ...data,
170
+ id,
171
+ name,
172
+ timestamp,
173
+ _imported: true,
174
+ _imported_at: new Date().toISOString()
175
+ });
176
+ res.writeHead(200, { 'Content-Type': 'application/json' });
177
+ res.end(JSON.stringify({ status: 'imported', id: result.id, name: result.name }));
178
+ }
179
+ catch (e) {
180
+ logger_1.logger.error('Failed to import result:', e.message);
181
+ res.writeHead(400, { 'Content-Type': 'application/json' });
182
+ res.end(JSON.stringify({ error: 'Import failed', details: e.message }));
183
+ }
184
+ }
185
+ resultToCSV(result, includeNetworkCalls = false) {
186
+ const lines = [];
187
+ // Summary header
188
+ lines.push('# Test Result Summary');
189
+ lines.push(`Name,${result.name}`);
190
+ lines.push(`Timestamp,${result.timestamp}`);
191
+ lines.push(`Duration,${result.duration}`);
192
+ lines.push('');
193
+ // Summary metrics
194
+ lines.push('# Summary Metrics');
195
+ lines.push('Metric,Value');
196
+ lines.push(`Total Requests,${result.summary.total_requests}`);
197
+ lines.push(`Successful Requests,${result.summary.successful_requests}`);
198
+ lines.push(`Failed Requests,${result.summary.failed_requests}`);
199
+ lines.push(`Avg Response Time (ms),${result.summary.avg_response_time}`);
200
+ lines.push(`Min Response Time (ms),${result.summary.min_response_time}`);
201
+ lines.push(`Max Response Time (ms),${result.summary.max_response_time}`);
202
+ lines.push(`P50 Response Time (ms),${result.summary.p50_response_time}`);
203
+ lines.push(`P95 Response Time (ms),${result.summary.p95_response_time}`);
204
+ lines.push(`P99 Response Time (ms),${result.summary.p99_response_time}`);
205
+ lines.push(`Requests per Second,${result.summary.requests_per_second}`);
206
+ lines.push(`Success Rate (%),${result.summary.success_rate}`);
207
+ lines.push(`Error Rate (%),${result.summary.error_rate}`);
208
+ lines.push('');
209
+ // Step statistics if available
210
+ if (result.step_statistics && result.step_statistics.length > 0) {
211
+ lines.push('# Step Statistics');
212
+ lines.push('Step Name,Scenario,Total Requests,Failed Requests,Success Rate,Avg Response Time,Min Response Time,Max Response Time,P50,P95,P99');
213
+ for (const step of result.step_statistics) {
214
+ lines.push([
215
+ step.step_name,
216
+ step.scenario || '',
217
+ step.total_requests || 0,
218
+ step.failed_requests || 0,
219
+ step.success_rate || 0,
220
+ step.avg_response_time || 0,
221
+ step.min_response_time || 0,
222
+ step.max_response_time || 0,
223
+ step.percentiles?.['50'] || 0,
224
+ step.percentiles?.['95'] || 0,
225
+ step.percentiles?.['99'] || 0
226
+ ].join(','));
227
+ }
228
+ lines.push('');
229
+ }
230
+ // Individual results if available
231
+ if (result.results && result.results.length > 0) {
232
+ lines.push('# Individual Results');
233
+ lines.push('Timestamp,Scenario,Action,VU ID,Duration (ms),Success,Status,Error');
234
+ for (const r of result.results) {
235
+ lines.push([
236
+ r.timestamp || '',
237
+ r.scenario || '',
238
+ r.action || '',
239
+ r.vu_id || 0,
240
+ r.duration || 0,
241
+ r.success ? 'true' : 'false',
242
+ r.status || '',
243
+ (r.error || '').replace(/,/g, ';')
244
+ ].join(','));
245
+ }
246
+ lines.push('');
247
+ }
248
+ // Network calls if available and requested
249
+ if (includeNetworkCalls && result.network_calls && result.network_calls.length > 0) {
250
+ lines.push('# Network Calls');
251
+ lines.push('Timestamp,URL,Method,Status,Duration (ms),Size,Success,Error');
252
+ for (const call of result.network_calls) {
253
+ lines.push([
254
+ call.timestamp || '',
255
+ (call.request_url || call.url || '').replace(/,/g, '%2C'),
256
+ call.request_method || call.method || 'GET',
257
+ call.response_status || call.status || 0,
258
+ call.duration || 0,
259
+ call.response_size || call.size || 0,
260
+ call.success ? 'true' : 'false',
261
+ (call.error || '').replace(/,/g, ';')
262
+ ].join(','));
263
+ }
264
+ }
265
+ return lines.join('\n');
266
+ }
267
+ async handleCompare(res, ids) {
268
+ const results = await Promise.all(ids.map(id => this.resultsManager.loadFullResult(id)));
269
+ const validResults = results.filter(r => r !== null);
270
+ res.writeHead(200, { 'Content-Type': 'application/json' });
271
+ res.end(JSON.stringify({
272
+ results: validResults,
273
+ comparison: this.resultsManager.generateComparison(validResults)
274
+ }));
275
+ }
276
+ handleGetLive(res) {
277
+ res.writeHead(200, { 'Content-Type': 'application/json' });
278
+ res.end(JSON.stringify(Array.from(this.liveTests.values())));
279
+ }
280
+ async handleGetTests(res) {
281
+ const tests = await this.fileScanner.scanTestFiles();
282
+ res.writeHead(200, { 'Content-Type': 'application/json' });
283
+ res.end(JSON.stringify(tests));
284
+ }
285
+ async handleRunTest(req, res) {
286
+ const body = await this.readBody(req);
287
+ const { testPath, options } = JSON.parse(body);
288
+ const result = this.testExecutor.runTest(testPath, options);
289
+ res.writeHead(200, { 'Content-Type': 'application/json' });
290
+ res.end(JSON.stringify(result));
291
+ }
292
+ handleStopTest(res, testId) {
293
+ const stopped = this.testExecutor.stopTest(testId);
294
+ if (stopped) {
295
+ res.writeHead(200, { 'Content-Type': 'application/json' });
296
+ res.end(JSON.stringify({ status: 'stopped' }));
297
+ }
298
+ else {
299
+ res.writeHead(404, { 'Content-Type': 'application/json' });
300
+ res.end(JSON.stringify({ error: 'Test not found' }));
301
+ }
302
+ }
303
+ async handleGetWorkers(res) {
304
+ const workersInfo = await this.workersManager.getWorkers();
305
+ res.writeHead(200, { 'Content-Type': 'application/json' });
306
+ res.end(JSON.stringify(workersInfo));
307
+ }
308
+ async handleInfraMetrics(req, res) {
309
+ try {
310
+ const body = await this.readBody(req);
311
+ const payload = JSON.parse(body);
312
+ // Validate required fields
313
+ if (!payload.host || payload.type !== 'infrastructure_metrics') {
314
+ res.writeHead(400, { 'Content-Type': 'application/json' });
315
+ res.end(JSON.stringify({ error: 'Invalid payload: missing host or type' }));
316
+ return;
317
+ }
318
+ const metrics = {
319
+ host: payload.host,
320
+ timestamp: payload.timestamp || new Date().toISOString(),
321
+ interval_seconds: payload.interval_seconds || 5,
322
+ metrics: payload.metrics || {}
323
+ };
324
+ // Write to InfluxDB (also stores in fallback buffer)
325
+ await this.influxService.writeMetrics(metrics);
326
+ // Broadcast to WebSocket clients
327
+ if (this.onInfraUpdate) {
328
+ this.onInfraUpdate(metrics);
329
+ }
330
+ logger_1.logger.debug(`Infrastructure metrics received from ${metrics.host}`);
331
+ res.writeHead(202, { 'Content-Type': 'application/json' });
332
+ res.end(JSON.stringify({
333
+ accepted: true,
334
+ host: metrics.host,
335
+ influxdb: this.influxService.isEnabled()
336
+ }));
337
+ }
338
+ catch (error) {
339
+ logger_1.logger.error('Failed to process infrastructure metrics:', error.message);
340
+ res.writeHead(400, { 'Content-Type': 'application/json' });
341
+ res.end(JSON.stringify({ error: 'Invalid JSON payload' }));
342
+ }
343
+ }
344
+ async handleGetInfra(res, host) {
345
+ try {
346
+ if (host) {
347
+ const metrics = await this.influxService.queryMetrics({ host });
348
+ res.writeHead(200, { 'Content-Type': 'application/json' });
349
+ res.end(JSON.stringify({ host, metrics }));
350
+ }
351
+ else {
352
+ const result = await this.influxService.getLatestMetrics();
353
+ res.writeHead(200, { 'Content-Type': 'application/json' });
354
+ res.end(JSON.stringify(result));
355
+ }
356
+ }
357
+ catch (error) {
358
+ logger_1.logger.error('Failed to get infra metrics:', error.message);
359
+ res.writeHead(500, { 'Content-Type': 'application/json' });
360
+ res.end(JSON.stringify({ error: error.message }));
361
+ }
362
+ }
363
+ async handleGetInfraByTestRun(res, startTime, endTime) {
364
+ try {
365
+ const start = new Date(startTime);
366
+ const end = new Date(endTime);
367
+ if (isNaN(start.getTime()) || isNaN(end.getTime())) {
368
+ res.writeHead(400, { 'Content-Type': 'application/json' });
369
+ res.end(JSON.stringify({ error: 'Invalid date format. Use ISO 8601.' }));
370
+ return;
371
+ }
372
+ const metrics = await this.influxService.queryMetricsByTestRun('', start, end);
373
+ res.writeHead(200, { 'Content-Type': 'application/json' });
374
+ res.end(JSON.stringify({
375
+ startTime: start.toISOString(),
376
+ endTime: end.toISOString(),
377
+ infrastructure_metrics: metrics
378
+ }));
379
+ }
380
+ catch (error) {
381
+ logger_1.logger.error('Failed to get infra by test run:', error.message);
382
+ res.writeHead(500, { 'Content-Type': 'application/json' });
383
+ res.end(JSON.stringify({ error: error.message }));
384
+ }
385
+ }
386
+ async handleExportInfra(req, res, url) {
387
+ try {
388
+ const format = (url.searchParams.get('format') || 'json');
389
+ const host = url.searchParams.get('host') || undefined;
390
+ const startTime = url.searchParams.get('start') ? new Date(url.searchParams.get('start')) : undefined;
391
+ const endTime = url.searchParams.get('end') ? new Date(url.searchParams.get('end')) : undefined;
392
+ const data = await this.influxService.exportMetrics({ host, startTime, endTime }, format);
393
+ const contentType = format === 'csv' ? 'text/csv' : 'application/json';
394
+ const filename = `infra-metrics-${new Date().toISOString().slice(0, 10)}.${format}`;
395
+ res.writeHead(200, {
396
+ 'Content-Type': contentType,
397
+ 'Content-Disposition': `attachment; filename="${filename}"`
398
+ });
399
+ res.end(data);
400
+ }
401
+ catch (error) {
402
+ logger_1.logger.error('Failed to export infra metrics:', error.message);
403
+ res.writeHead(500, { 'Content-Type': 'application/json' });
404
+ res.end(JSON.stringify({ error: error.message }));
405
+ }
406
+ }
407
+ async handleImportInfra(req, res, url) {
408
+ try {
409
+ const format = (url.searchParams.get('format') || 'json');
410
+ const body = await this.readBody(req);
411
+ const count = await this.influxService.importMetrics(body, format);
412
+ res.writeHead(200, { 'Content-Type': 'application/json' });
413
+ res.end(JSON.stringify({
414
+ imported: count,
415
+ format,
416
+ influxdb: this.influxService.isEnabled()
417
+ }));
418
+ }
419
+ catch (error) {
420
+ logger_1.logger.error('Failed to import infra metrics:', error.message);
421
+ res.writeHead(400, { 'Content-Type': 'application/json' });
422
+ res.end(JSON.stringify({ error: error.message }));
423
+ }
424
+ }
425
+ async handleGetInfraStatus(res) {
426
+ try {
427
+ const hosts = await this.influxService.getHosts();
428
+ res.writeHead(200, { 'Content-Type': 'application/json' });
429
+ res.end(JSON.stringify({
430
+ influxdb_enabled: this.influxService.isEnabled(),
431
+ hosts_count: hosts.length,
432
+ hosts
433
+ }));
434
+ }
435
+ catch (error) {
436
+ res.writeHead(500, { 'Content-Type': 'application/json' });
437
+ res.end(JSON.stringify({ error: error.message }));
438
+ }
439
+ }
440
+ /**
441
+ * Get a snapshot of all current infrastructure metrics for saving with test results
442
+ */
443
+ getInfraSnapshot() {
444
+ return this.influxService.getSnapshot();
445
+ }
446
+ /**
447
+ * Query infrastructure metrics for a specific test run time range
448
+ */
449
+ async getInfraForTestRun(startTime, endTime) {
450
+ return this.influxService.queryMetricsByTestRun('', startTime, endTime);
451
+ }
452
+ /**
453
+ * Get list of test runs stored in InfluxDB
454
+ */
455
+ async handleGetTestRuns(res) {
456
+ try {
457
+ if (!this.testMetricsWriter?.isEnabled()) {
458
+ res.writeHead(200, { 'Content-Type': 'application/json' });
459
+ res.end(JSON.stringify({ enabled: false, runs: [] }));
460
+ return;
461
+ }
462
+ const runs = await this.testMetricsWriter.getTestRuns(100);
463
+ res.writeHead(200, { 'Content-Type': 'application/json' });
464
+ res.end(JSON.stringify({
465
+ enabled: true,
466
+ runs
467
+ }));
468
+ }
469
+ catch (error) {
470
+ logger_1.logger.error('Failed to get test runs:', error.message);
471
+ res.writeHead(500, { 'Content-Type': 'application/json' });
472
+ res.end(JSON.stringify({ error: error.message }));
473
+ }
474
+ }
475
+ /**
476
+ * Query test results from InfluxDB for a specific test run
477
+ */
478
+ async handleGetTestMetrics(res, url) {
479
+ try {
480
+ if (!this.testMetricsWriter?.isEnabled()) {
481
+ res.writeHead(200, { 'Content-Type': 'application/json' });
482
+ res.end(JSON.stringify({ enabled: false, results: [], networkCalls: [] }));
483
+ return;
484
+ }
485
+ const testId = url.searchParams.get('testId') || undefined;
486
+ const testName = url.searchParams.get('testName') || undefined;
487
+ const startTime = url.searchParams.get('start') ? new Date(url.searchParams.get('start')) : undefined;
488
+ const endTime = url.searchParams.get('end') ? new Date(url.searchParams.get('end')) : undefined;
489
+ const limit = url.searchParams.get('limit') ? parseInt(url.searchParams.get('limit')) : undefined;
490
+ const results = await this.testMetricsWriter.queryResults({
491
+ testId,
492
+ testName,
493
+ startTime,
494
+ endTime,
495
+ limit
496
+ });
497
+ const networkCalls = await this.testMetricsWriter.queryNetworkCalls({
498
+ testId,
499
+ testName,
500
+ startTime,
501
+ endTime,
502
+ limit
503
+ });
504
+ res.writeHead(200, { 'Content-Type': 'application/json' });
505
+ res.end(JSON.stringify({
506
+ enabled: true,
507
+ results,
508
+ networkCalls
509
+ }));
510
+ }
511
+ catch (error) {
512
+ logger_1.logger.error('Failed to get test metrics:', error.message);
513
+ res.writeHead(500, { 'Content-Type': 'application/json' });
514
+ res.end(JSON.stringify({ error: error.message }));
515
+ }
516
+ }
517
+ /**
518
+ * Export test data from InfluxDB
519
+ */
520
+ async handleExportTestData(res, url) {
521
+ try {
522
+ if (!this.testMetricsWriter?.isEnabled()) {
523
+ res.writeHead(400, { 'Content-Type': 'application/json' });
524
+ res.end(JSON.stringify({ error: 'InfluxDB not enabled' }));
525
+ return;
526
+ }
527
+ const testId = url.searchParams.get('testId');
528
+ const format = (url.searchParams.get('format') || 'json');
529
+ if (!testId) {
530
+ res.writeHead(400, { 'Content-Type': 'application/json' });
531
+ res.end(JSON.stringify({ error: 'testId parameter required' }));
532
+ return;
533
+ }
534
+ const data = await this.testMetricsWriter.exportTestData(testId, format);
535
+ const contentType = format === 'csv' ? 'text/csv' : 'application/json';
536
+ const filename = `test-data-${testId}.${format}`;
537
+ res.writeHead(200, {
538
+ 'Content-Type': contentType,
539
+ 'Content-Disposition': `attachment; filename="${filename}"`
540
+ });
541
+ res.end(data);
542
+ }
543
+ catch (error) {
544
+ logger_1.logger.error('Failed to export test data:', error.message);
545
+ res.writeHead(500, { 'Content-Type': 'application/json' });
546
+ res.end(JSON.stringify({ error: error.message }));
547
+ }
548
+ }
549
+ /**
550
+ * Get InfluxDB status for test metrics
551
+ */
552
+ async handleGetTestMetricsStatus(res) {
553
+ const enabled = this.testMetricsWriter?.isEnabled() || false;
554
+ res.writeHead(200, { 'Content-Type': 'application/json' });
555
+ res.end(JSON.stringify({
556
+ influxdb_enabled: enabled,
557
+ message: enabled ? 'Test metrics storage active' : 'Test metrics stored in files only'
558
+ }));
559
+ }
560
+ async readBody(req) {
561
+ return new Promise((resolve, reject) => {
562
+ let body = '';
563
+ req.on('data', chunk => body += chunk);
564
+ req.on('end', () => resolve(body));
565
+ req.on('error', reject);
566
+ });
567
+ }
568
+ }
569
+ exports.ApiRoutes = ApiRoutes;
@@ -0,0 +1,2 @@
1
+ export { ApiRoutes } from './api';
2
+ export { StaticRoutes } from './static';
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StaticRoutes = exports.ApiRoutes = void 0;
4
+ var api_1 = require("./api");
5
+ Object.defineProperty(exports, "ApiRoutes", { enumerable: true, get: function () { return api_1.ApiRoutes; } });
6
+ var static_1 = require("./static");
7
+ Object.defineProperty(exports, "StaticRoutes", { enumerable: true, get: function () { return static_1.StaticRoutes; } });
@@ -0,0 +1,6 @@
1
+ import * as http from 'http';
2
+ export declare class StaticRoutes {
3
+ private templatesDir;
4
+ constructor();
5
+ serve(req: http.IncomingMessage, res: http.ServerResponse, pathname: string): Promise<void>;
6
+ }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.StaticRoutes = void 0;
37
+ const fs = __importStar(require("fs/promises"));
38
+ const path = __importStar(require("path"));
39
+ const logger_1 = require("../../utils/logger");
40
+ class StaticRoutes {
41
+ constructor() {
42
+ this.templatesDir = path.join(__dirname, '../templates');
43
+ }
44
+ async serve(req, res, pathname) {
45
+ try {
46
+ let filePath;
47
+ let contentType;
48
+ if (pathname === '/' || pathname === '/index.html') {
49
+ filePath = path.join(this.templatesDir, 'index.html');
50
+ contentType = 'text/html';
51
+ }
52
+ else if (pathname === '/styles.css') {
53
+ filePath = path.join(this.templatesDir, 'styles.css');
54
+ contentType = 'text/css';
55
+ }
56
+ else if (pathname.startsWith('/scripts/')) {
57
+ filePath = path.join(this.templatesDir, pathname);
58
+ contentType = 'application/javascript';
59
+ }
60
+ else {
61
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
62
+ res.end('Not found');
63
+ return;
64
+ }
65
+ const content = await fs.readFile(filePath, 'utf-8');
66
+ res.writeHead(200, { 'Content-Type': contentType });
67
+ res.end(content);
68
+ }
69
+ catch (error) {
70
+ logger_1.logger.error(`Failed to serve static file ${pathname}:`, error);
71
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
72
+ res.end('Not found');
73
+ }
74
+ }
75
+ }
76
+ exports.StaticRoutes = StaticRoutes;