@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,404 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InfluxDBWriter = void 0;
4
+ exports.getInfluxDBWriter = getInfluxDBWriter;
5
+ exports.setInfluxDBWriter = setInfluxDBWriter;
6
+ exports.initInfluxDBWriter = initInfluxDBWriter;
7
+ const influxdb_client_1 = require("@influxdata/influxdb-client");
8
+ const logger_1 = require("../../utils/logger");
9
+ /**
10
+ * InfluxDB writer for test metrics (response times, network calls, etc.)
11
+ * This is optional - test results can still be stored as JSON files.
12
+ */
13
+ class InfluxDBWriter {
14
+ constructor(config) {
15
+ this.client = null;
16
+ this.writeApi = null;
17
+ this.queryApi = null;
18
+ this.enabled = false;
19
+ this.currentTestId = '';
20
+ this.currentTestName = '';
21
+ this.config = {
22
+ url: config?.url || process.env.INFLUXDB_URL || 'http://localhost:8086',
23
+ token: config?.token || process.env.INFLUXDB_TOKEN || '',
24
+ org: config?.org || process.env.INFLUXDB_ORG || 'perfornium',
25
+ bucket: config?.bucket || process.env.INFLUXDB_BUCKET || 'metrics',
26
+ batchSize: config?.batchSize || 100,
27
+ flushInterval: config?.flushInterval || 1000
28
+ };
29
+ }
30
+ async connect() {
31
+ if (!this.config.token) {
32
+ logger_1.logger.debug('InfluxDB token not configured, test metrics will only be stored in files');
33
+ return false;
34
+ }
35
+ try {
36
+ this.client = new influxdb_client_1.InfluxDB({
37
+ url: this.config.url,
38
+ token: this.config.token
39
+ });
40
+ this.writeApi = this.client.getWriteApi(this.config.org, this.config.bucket, 'ms', {
41
+ batchSize: this.config.batchSize,
42
+ flushInterval: this.config.flushInterval
43
+ });
44
+ this.queryApi = this.client.getQueryApi(this.config.org);
45
+ // Test connection
46
+ const query = `from(bucket: "${this.config.bucket}") |> range(start: -1s) |> limit(n: 1)`;
47
+ await this.queryApi.collectRows(query);
48
+ this.enabled = true;
49
+ logger_1.logger.info(`InfluxDB connected for test metrics at ${this.config.url}`);
50
+ return true;
51
+ }
52
+ catch (error) {
53
+ logger_1.logger.warn(`Failed to connect to InfluxDB for test metrics: ${error.message}`);
54
+ this.enabled = false;
55
+ return false;
56
+ }
57
+ }
58
+ isEnabled() {
59
+ return this.enabled;
60
+ }
61
+ /**
62
+ * Start a new test run - sets the test ID and name for all subsequent writes
63
+ */
64
+ startTest(testId, testName) {
65
+ this.currentTestId = testId;
66
+ this.currentTestName = testName;
67
+ logger_1.logger.debug(`InfluxDB writer started for test: ${testName} (${testId})`);
68
+ }
69
+ /**
70
+ * Write a single test result to InfluxDB
71
+ */
72
+ async writeResult(result) {
73
+ if (!this.enabled || !this.writeApi)
74
+ return;
75
+ try {
76
+ const point = new influxdb_client_1.Point('test_result')
77
+ .tag('test_id', this.currentTestId)
78
+ .tag('test_name', this.currentTestName)
79
+ .tag('scenario', result.scenario || 'default')
80
+ .tag('action', result.action || result.step_name || 'unknown')
81
+ .tag('success', result.success ? 'true' : 'false')
82
+ .intField('vu_id', result.vu_id)
83
+ .intField('iteration', result.iteration || 0)
84
+ .floatField('duration', result.duration || 0)
85
+ .intField('status', result.status || 0)
86
+ .timestamp(new Date(result.timestamp));
87
+ if (result.response_size) {
88
+ point.intField('response_size', result.response_size);
89
+ }
90
+ if (result.connect_time !== undefined) {
91
+ point.floatField('connect_time', result.connect_time);
92
+ }
93
+ if (result.latency !== undefined) {
94
+ point.floatField('latency', result.latency);
95
+ }
96
+ if (result.error) {
97
+ point.stringField('error', result.error.substring(0, 255));
98
+ }
99
+ if (result.request_url) {
100
+ point.stringField('request_url', result.request_url.substring(0, 500));
101
+ }
102
+ if (result.request_method) {
103
+ point.tag('method', result.request_method);
104
+ }
105
+ this.writeApi.writePoint(point);
106
+ }
107
+ catch (error) {
108
+ logger_1.logger.error(`Failed to write test result to InfluxDB: ${error.message}`);
109
+ }
110
+ }
111
+ /**
112
+ * Write a batch of test results to InfluxDB
113
+ */
114
+ async writeBatch(results) {
115
+ if (!this.enabled || !this.writeApi)
116
+ return;
117
+ for (const result of results) {
118
+ await this.writeResult(result);
119
+ }
120
+ try {
121
+ await this.writeApi.flush();
122
+ }
123
+ catch (error) {
124
+ logger_1.logger.error(`Failed to flush test results to InfluxDB: ${error.message}`);
125
+ }
126
+ }
127
+ /**
128
+ * Write a network call to InfluxDB
129
+ */
130
+ async writeNetworkCall(call) {
131
+ if (!this.enabled || !this.writeApi)
132
+ return;
133
+ try {
134
+ const point = new influxdb_client_1.Point('network_call')
135
+ .tag('test_id', this.currentTestId)
136
+ .tag('test_name', this.currentTestName)
137
+ .tag('method', call.request_method || 'GET')
138
+ .tag('success', call.success ? 'true' : 'false')
139
+ .tag('resource_type', call.resource_type || 'other')
140
+ .intField('vu_id', call.vu_id)
141
+ .floatField('duration', call.duration || 0)
142
+ .intField('status', call.response_status || 0)
143
+ .intField('response_size', call.response_size || 0)
144
+ .stringField('url', (call.request_url || '').substring(0, 500))
145
+ .timestamp(new Date(call.timestamp));
146
+ if (call.scenario) {
147
+ point.tag('scenario', call.scenario);
148
+ }
149
+ if (call.step_name) {
150
+ point.tag('step_name', call.step_name);
151
+ }
152
+ if (call.error) {
153
+ point.stringField('error', call.error.substring(0, 255));
154
+ }
155
+ this.writeApi.writePoint(point);
156
+ }
157
+ catch (error) {
158
+ logger_1.logger.error(`Failed to write network call to InfluxDB: ${error.message}`);
159
+ }
160
+ }
161
+ /**
162
+ * Write test summary to InfluxDB
163
+ */
164
+ async writeSummary(summary) {
165
+ if (!this.enabled || !this.writeApi)
166
+ return;
167
+ try {
168
+ const point = new influxdb_client_1.Point('test_summary')
169
+ .tag('test_id', this.currentTestId)
170
+ .tag('test_name', this.currentTestName)
171
+ .intField('total_requests', summary.total_requests)
172
+ .intField('successful_requests', summary.successful_requests)
173
+ .intField('failed_requests', summary.failed_requests)
174
+ .floatField('success_rate', summary.success_rate)
175
+ .floatField('avg_response_time', summary.avg_response_time)
176
+ .floatField('min_response_time', summary.min_response_time)
177
+ .floatField('max_response_time', summary.max_response_time)
178
+ .floatField('p50', summary.percentiles[50] || 0)
179
+ .floatField('p90', summary.percentiles[90] || 0)
180
+ .floatField('p95', summary.percentiles[95] || 0)
181
+ .floatField('p99', summary.percentiles[99] || 0)
182
+ .floatField('requests_per_second', summary.requests_per_second)
183
+ .floatField('total_duration', summary.total_duration)
184
+ .timestamp(new Date());
185
+ this.writeApi.writePoint(point);
186
+ await this.writeApi.flush();
187
+ }
188
+ catch (error) {
189
+ logger_1.logger.error(`Failed to write test summary to InfluxDB: ${error.message}`);
190
+ }
191
+ }
192
+ /**
193
+ * Query test results from InfluxDB
194
+ */
195
+ async queryResults(options = {}) {
196
+ if (!this.enabled || !this.queryApi)
197
+ return [];
198
+ try {
199
+ const startTime = options.startTime || new Date(Date.now() - 24 * 60 * 60 * 1000);
200
+ const endTime = options.endTime || new Date();
201
+ // Validate time range
202
+ if (startTime.getTime() >= endTime.getTime()) {
203
+ logger_1.logger.debug('Invalid time range for test results query');
204
+ return [];
205
+ }
206
+ let query = `from(bucket: "${this.config.bucket}")
207
+ |> range(start: ${startTime.toISOString()}, stop: ${endTime.toISOString()})
208
+ |> filter(fn: (r) => r._measurement == "test_result")`;
209
+ if (options.testId) {
210
+ query += `\n |> filter(fn: (r) => r.test_id == "${options.testId}")`;
211
+ }
212
+ if (options.testName) {
213
+ query += `\n |> filter(fn: (r) => r.test_name == "${options.testName}")`;
214
+ }
215
+ if (options.scenario) {
216
+ query += `\n |> filter(fn: (r) => r.scenario == "${options.scenario}")`;
217
+ }
218
+ query += `\n |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")`;
219
+ if (options.limit) {
220
+ query += `\n |> limit(n: ${options.limit})`;
221
+ }
222
+ const rows = await this.queryApi.collectRows(query);
223
+ return this.rowsToResults(rows);
224
+ }
225
+ catch (error) {
226
+ logger_1.logger.error(`Failed to query test results from InfluxDB: ${error.message}`);
227
+ return [];
228
+ }
229
+ }
230
+ /**
231
+ * Query network calls from InfluxDB
232
+ */
233
+ async queryNetworkCalls(options = {}) {
234
+ if (!this.enabled || !this.queryApi)
235
+ return [];
236
+ try {
237
+ const startTime = options.startTime || new Date(Date.now() - 24 * 60 * 60 * 1000);
238
+ const endTime = options.endTime || new Date();
239
+ // Validate time range
240
+ if (startTime.getTime() >= endTime.getTime()) {
241
+ logger_1.logger.debug('Invalid time range for network calls query');
242
+ return [];
243
+ }
244
+ let query = `from(bucket: "${this.config.bucket}")
245
+ |> range(start: ${startTime.toISOString()}, stop: ${endTime.toISOString()})
246
+ |> filter(fn: (r) => r._measurement == "network_call")`;
247
+ if (options.testId) {
248
+ query += `\n |> filter(fn: (r) => r.test_id == "${options.testId}")`;
249
+ }
250
+ if (options.testName) {
251
+ query += `\n |> filter(fn: (r) => r.test_name == "${options.testName}")`;
252
+ }
253
+ query += `\n |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")`;
254
+ if (options.limit) {
255
+ query += `\n |> limit(n: ${options.limit})`;
256
+ }
257
+ const rows = await this.queryApi.collectRows(query);
258
+ return this.rowsToNetworkCalls(rows);
259
+ }
260
+ catch (error) {
261
+ logger_1.logger.error(`Failed to query network calls from InfluxDB: ${error.message}`);
262
+ return [];
263
+ }
264
+ }
265
+ /**
266
+ * Get list of test runs
267
+ */
268
+ async getTestRuns(limit = 100) {
269
+ if (!this.enabled || !this.queryApi)
270
+ return [];
271
+ try {
272
+ const query = `from(bucket: "${this.config.bucket}")
273
+ |> range(start: -30d)
274
+ |> filter(fn: (r) => r._measurement == "test_summary")
275
+ |> keep(columns: ["test_id", "test_name", "_time"])
276
+ |> sort(columns: ["_time"], desc: true)
277
+ |> limit(n: ${limit})`;
278
+ const rows = await this.queryApi.collectRows(query);
279
+ return rows.map((row) => ({
280
+ testId: row.test_id,
281
+ testName: row.test_name,
282
+ timestamp: new Date(row._time)
283
+ }));
284
+ }
285
+ catch (error) {
286
+ logger_1.logger.error(`Failed to get test runs from InfluxDB: ${error.message}`);
287
+ return [];
288
+ }
289
+ }
290
+ /**
291
+ * Export test data for a specific test run
292
+ */
293
+ async exportTestData(testId, format = 'json') {
294
+ const results = await this.queryResults({ testId });
295
+ const networkCalls = await this.queryNetworkCalls({ testId });
296
+ const data = {
297
+ testId,
298
+ results,
299
+ networkCalls,
300
+ exportedAt: new Date().toISOString()
301
+ };
302
+ if (format === 'csv') {
303
+ return this.resultsToCSV(results);
304
+ }
305
+ return JSON.stringify(data, null, 2);
306
+ }
307
+ /**
308
+ * Finalize the current test - flush all pending writes
309
+ */
310
+ async finalize() {
311
+ if (this.writeApi) {
312
+ try {
313
+ await this.writeApi.flush();
314
+ logger_1.logger.debug('InfluxDB writer finalized for test: ' + this.currentTestName);
315
+ }
316
+ catch (error) {
317
+ logger_1.logger.error(`Failed to finalize InfluxDB writes: ${error.message}`);
318
+ }
319
+ }
320
+ }
321
+ /**
322
+ * Close the connection
323
+ */
324
+ async close() {
325
+ if (this.writeApi) {
326
+ await this.writeApi.close();
327
+ }
328
+ }
329
+ rowsToResults(rows) {
330
+ return rows.map(row => ({
331
+ id: `${row.test_id}-${row._time}`,
332
+ vu_id: row.vu_id || 0,
333
+ iteration: row.iteration || 0,
334
+ scenario: row.scenario || 'default',
335
+ action: row.action || 'unknown',
336
+ timestamp: new Date(row._time).getTime(),
337
+ duration: row.duration || 0,
338
+ success: row.success === 'true',
339
+ status: row.status || 0,
340
+ response_size: row.response_size,
341
+ connect_time: row.connect_time,
342
+ latency: row.latency,
343
+ error: row.error,
344
+ request_url: row.request_url,
345
+ request_method: row.method
346
+ }));
347
+ }
348
+ rowsToNetworkCalls(rows) {
349
+ return rows.map(row => ({
350
+ id: `${row.test_id}-${row._time}`,
351
+ vu_id: row.vu_id || 0,
352
+ timestamp: new Date(row._time).getTime(),
353
+ request_url: row.url || '',
354
+ request_method: row.method || 'GET',
355
+ response_status: row.status || 0,
356
+ response_size: row.response_size || 0,
357
+ start_time: new Date(row._time).getTime(),
358
+ duration: row.duration || 0,
359
+ resource_type: row.resource_type || 'other',
360
+ success: row.success === 'true',
361
+ error: row.error,
362
+ scenario: row.scenario,
363
+ step_name: row.step_name
364
+ }));
365
+ }
366
+ resultsToCSV(results) {
367
+ const headers = [
368
+ 'timestamp', 'vu_id', 'iteration', 'scenario', 'action', 'success',
369
+ 'duration', 'status', 'response_size', 'connect_time', 'latency',
370
+ 'request_url', 'request_method', 'error'
371
+ ];
372
+ const rows = results.map(r => [
373
+ new Date(r.timestamp).toISOString(),
374
+ r.vu_id,
375
+ r.iteration || 0,
376
+ r.scenario,
377
+ r.action || '',
378
+ r.success,
379
+ r.duration || 0,
380
+ r.status || 0,
381
+ r.response_size || 0,
382
+ r.connect_time || 0,
383
+ r.latency || 0,
384
+ (r.request_url || '').replace(/,/g, ';'),
385
+ r.request_method || '',
386
+ (r.error || '').replace(/,/g, ';').replace(/\n/g, ' ')
387
+ ].join(','));
388
+ return [headers.join(','), ...rows].join('\n');
389
+ }
390
+ }
391
+ exports.InfluxDBWriter = InfluxDBWriter;
392
+ // Singleton instance for global access
393
+ let influxDBWriter = null;
394
+ function getInfluxDBWriter() {
395
+ return influxDBWriter;
396
+ }
397
+ function setInfluxDBWriter(writer) {
398
+ influxDBWriter = writer;
399
+ }
400
+ async function initInfluxDBWriter(config) {
401
+ influxDBWriter = new InfluxDBWriter(config);
402
+ await influxDBWriter.connect();
403
+ return influxDBWriter;
404
+ }
@@ -0,0 +1,18 @@
1
+ import { TestResult } from '../types';
2
+ export interface RealtimeEndpoint {
3
+ type: 'graphite' | 'webhook' | 'influxdb' | 'websocket';
4
+ url?: string;
5
+ host?: string;
6
+ port?: number;
7
+ database?: string;
8
+ token?: string;
9
+ headers?: Record<string, string>;
10
+ }
11
+ export declare class RealtimeDispatcher {
12
+ private endpoints;
13
+ private startTime;
14
+ setEndpoints(endpoints: RealtimeEndpoint[]): void;
15
+ setStartTime(time: number): void;
16
+ dispatch(batch: TestResult[], batchNumber: number): Promise<void>;
17
+ private sendToEndpoint;
18
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RealtimeDispatcher = void 0;
4
+ const logger_1 = require("../../utils/logger");
5
+ const graphite_1 = require("./endpoints/graphite");
6
+ const webhook_1 = require("./endpoints/webhook");
7
+ const influxdb_1 = require("./endpoints/influxdb");
8
+ const websocket_1 = require("./endpoints/websocket");
9
+ class RealtimeDispatcher {
10
+ constructor() {
11
+ this.endpoints = [];
12
+ this.startTime = 0;
13
+ }
14
+ setEndpoints(endpoints) {
15
+ this.endpoints = endpoints;
16
+ }
17
+ setStartTime(time) {
18
+ this.startTime = time;
19
+ }
20
+ async dispatch(batch, batchNumber) {
21
+ if (this.endpoints.length === 0)
22
+ return;
23
+ const promises = this.endpoints.map(endpoint => this.sendToEndpoint(batch, endpoint, batchNumber).catch(error => logger_1.logger.warn(`Failed to send to ${endpoint.type} endpoint:`, error)));
24
+ await Promise.allSettled(promises);
25
+ }
26
+ async sendToEndpoint(batch, endpoint, batchNumber) {
27
+ switch (endpoint.type) {
28
+ case 'graphite':
29
+ await (0, graphite_1.sendToGraphite)(batch, endpoint);
30
+ break;
31
+ case 'webhook':
32
+ await (0, webhook_1.sendToWebhook)(batch, endpoint, batchNumber, this.startTime);
33
+ break;
34
+ case 'influxdb':
35
+ await (0, influxdb_1.sendToInfluxDB)(batch, endpoint, batchNumber);
36
+ break;
37
+ case 'websocket':
38
+ await (0, websocket_1.sendToWebSocket)(batch, endpoint, batchNumber, this.startTime);
39
+ break;
40
+ default:
41
+ logger_1.logger.warn(`Unknown endpoint type: ${endpoint.type}`);
42
+ }
43
+ }
44
+ }
45
+ exports.RealtimeDispatcher = RealtimeDispatcher;
@@ -0,0 +1,3 @@
1
+ import { TestResult } from '../../types';
2
+ import { RealtimeEndpoint } from '../dispatcher';
3
+ export declare function sendToGraphite(batch: TestResult[], config: RealtimeEndpoint): Promise<void>;
@@ -0,0 +1,61 @@
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.sendToGraphite = sendToGraphite;
37
+ const net = __importStar(require("net"));
38
+ async function sendToGraphite(batch, config) {
39
+ return new Promise((resolve, reject) => {
40
+ const client = net.createConnection(config.port, config.host);
41
+ client.on('connect', () => {
42
+ const metrics = batch.map(result => {
43
+ const timestamp = Math.floor(result.timestamp / 1000);
44
+ const metricName = `loadtest.${result.scenario}.${result.step_name || result.action}`;
45
+ return [
46
+ `${metricName}.duration ${result.duration} ${timestamp}`,
47
+ `${metricName}.success ${result.success ? 1 : 0} ${timestamp}`,
48
+ `${metricName}.count 1 ${timestamp}`
49
+ ].join('\n');
50
+ }).join('\n') + '\n';
51
+ client.write(metrics);
52
+ client.end();
53
+ });
54
+ client.on('close', () => resolve());
55
+ client.on('error', reject);
56
+ setTimeout(() => {
57
+ client.destroy();
58
+ reject(new Error('Graphite connection timeout'));
59
+ }, 5000);
60
+ });
61
+ }
@@ -0,0 +1,3 @@
1
+ import { TestResult } from '../../types';
2
+ import { RealtimeEndpoint } from '../dispatcher';
3
+ export declare function sendToInfluxDB(batch: TestResult[], config: RealtimeEndpoint, batchNumber: number): Promise<void>;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sendToInfluxDB = sendToInfluxDB;
4
+ async function sendToInfluxDB(batch, config, batchNumber) {
5
+ const lines = batch.map(result => {
6
+ const tags = [
7
+ `scenario=${result.scenario}`,
8
+ `step=${result.step_name || result.action}`,
9
+ `vu_id=${result.vu_id}`,
10
+ `success=${result.success}`
11
+ ].join(',');
12
+ const fields = [
13
+ `duration=${result.duration}`,
14
+ `success=${result.success ? 'true' : 'false'}`,
15
+ `batch_number=${batchNumber}i`
16
+ ];
17
+ if (result.status) {
18
+ fields.push(`status=${result.status}i`);
19
+ }
20
+ const timestamp = result.timestamp * 1000000; // Convert to nanoseconds
21
+ return `loadtest,${tags} ${fields.join(',')} ${timestamp}`;
22
+ }).join('\n');
23
+ const response = await fetch(`${config.url}/write?db=${config.database}`, {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Authorization': `Bearer ${config.token}`,
27
+ 'Content-Type': 'text/plain'
28
+ },
29
+ body: lines
30
+ });
31
+ if (!response.ok) {
32
+ const errorText = await response.text();
33
+ throw new Error(`InfluxDB write failed: ${response.status} ${errorText}`);
34
+ }
35
+ }
@@ -0,0 +1,3 @@
1
+ import { TestResult } from '../../types';
2
+ import { RealtimeEndpoint } from '../dispatcher';
3
+ export declare function sendToWebhook(batch: TestResult[], config: RealtimeEndpoint, batchNumber: number, startTime: number): Promise<void>;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sendToWebhook = sendToWebhook;
4
+ async function sendToWebhook(batch, config, batchNumber, startTime) {
5
+ const response = await fetch(config.url, {
6
+ method: 'POST',
7
+ headers: {
8
+ 'Content-Type': 'application/json',
9
+ ...config.headers
10
+ },
11
+ body: JSON.stringify({
12
+ timestamp: new Date().toISOString(),
13
+ batch_number: batchNumber,
14
+ batch_size: batch.length,
15
+ test_start_time: new Date(startTime).toISOString(),
16
+ results: batch
17
+ })
18
+ });
19
+ if (!response.ok) {
20
+ throw new Error(`Webhook failed: ${response.status} ${response.statusText}`);
21
+ }
22
+ }
@@ -0,0 +1,3 @@
1
+ import { TestResult } from '../../types';
2
+ import { RealtimeEndpoint } from '../dispatcher';
3
+ export declare function sendToWebSocket(batch: TestResult[], config: RealtimeEndpoint, batchNumber: number, startTime: number): Promise<void>;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sendToWebSocket = sendToWebSocket;
4
+ const ws_1 = require("ws");
5
+ async function sendToWebSocket(batch, config, batchNumber, startTime) {
6
+ return new Promise((resolve, reject) => {
7
+ const ws = new ws_1.WebSocket(config.url);
8
+ ws.on('open', () => {
9
+ ws.send(JSON.stringify({
10
+ type: 'metrics_batch',
11
+ timestamp: new Date().toISOString(),
12
+ batch_number: batchNumber,
13
+ test_start_time: new Date(startTime).toISOString(),
14
+ data: batch
15
+ }));
16
+ ws.close();
17
+ resolve();
18
+ });
19
+ ws.on('error', reject);
20
+ setTimeout(() => {
21
+ ws.close();
22
+ reject(new Error('WebSocket connection timeout'));
23
+ }, 5000);
24
+ });
25
+ }
@@ -0,0 +1,5 @@
1
+ export { RealtimeDispatcher, RealtimeEndpoint } from './dispatcher';
2
+ export { sendToGraphite } from './endpoints/graphite';
3
+ export { sendToWebhook } from './endpoints/webhook';
4
+ export { sendToInfluxDB } from './endpoints/influxdb';
5
+ export { sendToWebSocket } from './endpoints/websocket';
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sendToWebSocket = exports.sendToInfluxDB = exports.sendToWebhook = exports.sendToGraphite = exports.RealtimeDispatcher = void 0;
4
+ var dispatcher_1 = require("./dispatcher");
5
+ Object.defineProperty(exports, "RealtimeDispatcher", { enumerable: true, get: function () { return dispatcher_1.RealtimeDispatcher; } });
6
+ var graphite_1 = require("./endpoints/graphite");
7
+ Object.defineProperty(exports, "sendToGraphite", { enumerable: true, get: function () { return graphite_1.sendToGraphite; } });
8
+ var webhook_1 = require("./endpoints/webhook");
9
+ Object.defineProperty(exports, "sendToWebhook", { enumerable: true, get: function () { return webhook_1.sendToWebhook; } });
10
+ var influxdb_1 = require("./endpoints/influxdb");
11
+ Object.defineProperty(exports, "sendToInfluxDB", { enumerable: true, get: function () { return influxdb_1.sendToInfluxDB; } });
12
+ var websocket_1 = require("./endpoints/websocket");
13
+ Object.defineProperty(exports, "sendToWebSocket", { enumerable: true, get: function () { return websocket_1.sendToWebSocket; } });
@@ -0,0 +1,3 @@
1
+ export { SummaryGenerator, SummaryGeneratorDependencies } from './summary-generator';
2
+ export { StepStatisticsCalculator } from './step-statistics';
3
+ export { TimelineCalculator } from './timeline-calculator';
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TimelineCalculator = exports.StepStatisticsCalculator = exports.SummaryGenerator = void 0;
4
+ var summary_generator_1 = require("./summary-generator");
5
+ Object.defineProperty(exports, "SummaryGenerator", { enumerable: true, get: function () { return summary_generator_1.SummaryGenerator; } });
6
+ var step_statistics_1 = require("./step-statistics");
7
+ Object.defineProperty(exports, "StepStatisticsCalculator", { enumerable: true, get: function () { return step_statistics_1.StepStatisticsCalculator; } });
8
+ var timeline_calculator_1 = require("./timeline-calculator");
9
+ Object.defineProperty(exports, "TimelineCalculator", { enumerable: true, get: function () { return timeline_calculator_1.TimelineCalculator; } });