k6-perf-reporter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/.claude/settings.local.json +54 -0
  2. package/.config.example.json +8 -0
  3. package/.config.json +8 -0
  4. package/.eslintrc.json +18 -0
  5. package/.github/workflows/build.yml +30 -0
  6. package/.github/workflows/release.yml +44 -0
  7. package/README.md +449 -0
  8. package/dist/cli-reporter.d.ts +5 -0
  9. package/dist/cli-reporter.d.ts.map +1 -0
  10. package/dist/cli-reporter.js +71 -0
  11. package/dist/cli-reporter.js.map +1 -0
  12. package/dist/cli.d.ts +2 -0
  13. package/dist/cli.d.ts.map +1 -0
  14. package/dist/cli.js +43 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/config.d.ts +14 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +26 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/data-collector.d.ts +14 -0
  21. package/dist/data-collector.d.ts.map +1 -0
  22. package/dist/data-collector.js +51 -0
  23. package/dist/data-collector.js.map +1 -0
  24. package/dist/data-extractor.d.ts +31 -0
  25. package/dist/data-extractor.d.ts.map +1 -0
  26. package/dist/data-extractor.js +250 -0
  27. package/dist/data-extractor.js.map +1 -0
  28. package/dist/explore.d.ts +2 -0
  29. package/dist/explore.d.ts.map +1 -0
  30. package/dist/explore.js +27 -0
  31. package/dist/explore.js.map +1 -0
  32. package/dist/index.d.ts +7 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +43 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/influx-client.d.ts +16 -0
  37. package/dist/influx-client.d.ts.map +1 -0
  38. package/dist/influx-client.interface.d.ts +21 -0
  39. package/dist/influx-client.interface.d.ts.map +1 -0
  40. package/dist/influx-client.interface.js +3 -0
  41. package/dist/influx-client.interface.js.map +1 -0
  42. package/dist/influx-client.js +35 -0
  43. package/dist/influx-client.js.map +1 -0
  44. package/dist/influx-data-extractor.d.ts +112 -0
  45. package/dist/influx-data-extractor.d.ts.map +1 -0
  46. package/dist/influx-data-extractor.interface.d.ts +21 -0
  47. package/dist/influx-data-extractor.interface.d.ts.map +1 -0
  48. package/dist/influx-data-extractor.interface.js +3 -0
  49. package/dist/influx-data-extractor.interface.js.map +1 -0
  50. package/dist/influx-data-extractor.js +445 -0
  51. package/dist/influx-data-extractor.js.map +1 -0
  52. package/dist/influx.d.ts +122 -0
  53. package/dist/influx.d.ts.map +1 -0
  54. package/dist/influx.js +899 -0
  55. package/dist/influx.js.map +1 -0
  56. package/dist/logger.d.ts +7 -0
  57. package/dist/logger.d.ts.map +1 -0
  58. package/dist/logger.js +12 -0
  59. package/dist/logger.js.map +1 -0
  60. package/dist/metrics-collector.d.ts +10 -0
  61. package/dist/metrics-collector.d.ts.map +1 -0
  62. package/dist/metrics-collector.js +99 -0
  63. package/dist/metrics-collector.js.map +1 -0
  64. package/dist/metrics-collector.test.d.ts +2 -0
  65. package/dist/metrics-collector.test.d.ts.map +1 -0
  66. package/dist/metrics-collector.test.js +235 -0
  67. package/dist/metrics-collector.test.js.map +1 -0
  68. package/dist/reporters/cli-reporter.d.ts +9 -0
  69. package/dist/reporters/cli-reporter.d.ts.map +1 -0
  70. package/dist/reporters/cli-reporter.js +141 -0
  71. package/dist/reporters/cli-reporter.js.map +1 -0
  72. package/dist/reporters/cli.d.ts +5 -0
  73. package/dist/reporters/cli.d.ts.map +1 -0
  74. package/dist/reporters/cli.js +150 -0
  75. package/dist/reporters/cli.js.map +1 -0
  76. package/dist/reporters/index.d.ts +3 -0
  77. package/dist/reporters/index.d.ts.map +1 -0
  78. package/dist/reporters/index.js +8 -0
  79. package/dist/reporters/index.js.map +1 -0
  80. package/dist/reporters/json-reporter.d.ts +5 -0
  81. package/dist/reporters/json-reporter.d.ts.map +1 -0
  82. package/dist/reporters/json-reporter.js +18 -0
  83. package/dist/reporters/json-reporter.js.map +1 -0
  84. package/dist/reporters/json.d.ts +5 -0
  85. package/dist/reporters/json.d.ts.map +1 -0
  86. package/dist/reporters/json.js +70 -0
  87. package/dist/reporters/json.js.map +1 -0
  88. package/dist/reporters/markdown.d.ts +5 -0
  89. package/dist/reporters/markdown.d.ts.map +1 -0
  90. package/dist/reporters/markdown.js +87 -0
  91. package/dist/reporters/markdown.js.map +1 -0
  92. package/dist/reports.d.ts +11 -0
  93. package/dist/reports.d.ts.map +1 -0
  94. package/dist/reports.js +325 -0
  95. package/dist/reports.js.map +1 -0
  96. package/dist/types.d.ts +113 -0
  97. package/dist/types.d.ts.map +1 -0
  98. package/dist/types.js +4 -0
  99. package/dist/types.js.map +1 -0
  100. package/dist/utils.d.ts +2 -0
  101. package/dist/utils.d.ts.map +1 -0
  102. package/dist/utils.js +19 -0
  103. package/dist/utils.js.map +1 -0
  104. package/package.json +33 -0
  105. package/src/cli.ts +47 -0
  106. package/src/config.ts +31 -0
  107. package/src/data-collector.ts +66 -0
  108. package/src/influx-client.ts +48 -0
  109. package/src/influx-data-extractor.ts +722 -0
  110. package/src/reporters/cli-reporter.ts +169 -0
  111. package/src/reporters/index.ts +2 -0
  112. package/src/reporters/json-reporter.ts +15 -0
  113. package/tsconfig.json +20 -0
package/dist/influx.js ADDED
@@ -0,0 +1,899 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.InfluxQueryClient = void 0;
7
+ const influxdb_client_1 = require("@influxdata/influxdb-client");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ class InfluxQueryClient {
10
+ constructor(config) {
11
+ this.config = config;
12
+ this.influxDB = new influxdb_client_1.InfluxDB({
13
+ url: config.url,
14
+ token: config.token,
15
+ });
16
+ }
17
+ async queryMetrics(startTime, endTime, runId) {
18
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
19
+ const flux = `
20
+ from(bucket: "${this.config.bucket}")
21
+ |> range(start: ${startTime}, stop: ${endTime})
22
+ |> filter(fn: (r) => ${this.buildRunIdFilter(runId)})
23
+ `;
24
+ const metricsData = [];
25
+ return new Promise((resolve, reject) => {
26
+ queryApi.queryRows(flux, {
27
+ next: (row, tableMeta) => {
28
+ const o = tableMeta.toObject(row);
29
+ metricsData.push({
30
+ timestamp: new Date(o._time),
31
+ metric: o._field,
32
+ value: o._value,
33
+ tags: {
34
+ scenario: o.scenario,
35
+ },
36
+ });
37
+ },
38
+ error: (error) => reject(error),
39
+ complete: () => resolve(metricsData),
40
+ });
41
+ });
42
+ }
43
+ async getResponseTimePercentiles(startTime, endTime, runId) {
44
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
45
+ const queries = [
46
+ { name: 'p50', query: `from(bucket: "${this.config.bucket}") |> range(start: ${startTime}, stop: ${endTime}) |> filter(fn: (r) => r._measurement == "http_req_duration" and ${this.buildRunIdFilter(runId)}) |> quantile(q: 0.5)` },
47
+ { name: 'p95', query: `from(bucket: "${this.config.bucket}") |> range(start: ${startTime}, stop: ${endTime}) |> filter(fn: (r) => r._measurement == "http_req_duration" and ${this.buildRunIdFilter(runId)}) |> quantile(q: 0.95)` },
48
+ { name: 'p99', query: `from(bucket: "${this.config.bucket}") |> range(start: ${startTime}, stop: ${endTime}) |> filter(fn: (r) => r._measurement == "http_req_duration" and ${this.buildRunIdFilter(runId)}) |> quantile(q: 0.99)` },
49
+ ];
50
+ const results = { p50: 0, p95: 0, p99: 0 };
51
+ let completed = 0;
52
+ return new Promise((resolve, reject) => {
53
+ queries.forEach(({ name, query }) => {
54
+ queryApi.queryRows(query, {
55
+ next: (row, tableMeta) => {
56
+ const o = tableMeta.toObject(row);
57
+ if (typeof o._value === "number") {
58
+ results[name] = o._value;
59
+ }
60
+ },
61
+ error: (error) => {
62
+ const errorMsg = error instanceof Error ? error.message : String(error);
63
+ console.error(chalk_1.default.red(` ✗ Error querying ${name}: ${errorMsg}`));
64
+ reject(error);
65
+ },
66
+ complete: () => {
67
+ completed++;
68
+ if (completed === queries.length) {
69
+ console.log(chalk_1.default.gray(` ✓ Retrieved response time percentiles - p50=${results.p50.toFixed(2)}ms, p95=${results.p95.toFixed(2)}ms, p99=${results.p99.toFixed(2)}ms`));
70
+ resolve({
71
+ p50: results.p50,
72
+ p95: results.p95,
73
+ p99: results.p99,
74
+ });
75
+ }
76
+ },
77
+ });
78
+ });
79
+ });
80
+ }
81
+ calculatePercentile(sortedValues, percentile) {
82
+ if (sortedValues.length === 0)
83
+ return 0;
84
+ const index = Math.ceil(sortedValues.length * percentile) - 1;
85
+ return sortedValues[Math.max(0, index)] || 0;
86
+ }
87
+ buildRunIdFilter(runId) {
88
+ return runId ? `r.runId == "${runId}"` : "true";
89
+ }
90
+ async getRequestStats(startTime, endTime, runId) {
91
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
92
+ // Count total requests
93
+ const totalFlux = `
94
+ from(bucket: "${this.config.bucket}")
95
+ |> range(start: ${startTime}, stop: ${endTime})
96
+ |> filter(fn: (r) => r._measurement == "http_req_duration" and ${this.buildRunIdFilter(runId)})
97
+ |> group(columns: ["_measurement"])
98
+ |> count()
99
+ `;
100
+ let total = 0;
101
+ let failed = 0;
102
+ return new Promise((resolve, reject) => {
103
+ queryApi.queryRows(totalFlux, {
104
+ next: (row, tableMeta) => {
105
+ const o = tableMeta.toObject(row);
106
+ total = o._value || 0;
107
+ },
108
+ error: (error) => {
109
+ const errorMsg = error instanceof Error ? error.message : String(error);
110
+ console.error(chalk_1.default.red(` ✗ Error querying total requests: ${errorMsg}`));
111
+ reject(error);
112
+ },
113
+ complete: () => {
114
+ console.log(chalk_1.default.gray(` ✓ Total requests: ${total}`));
115
+ // Count failed requests (where http_req_failed value is 1)
116
+ const failedFlux = `
117
+ from(bucket: "${this.config.bucket}")
118
+ |> range(start: ${startTime}, stop: ${endTime})
119
+ |> filter(fn: (r) => r._measurement == "http_req_failed" and ${this.buildRunIdFilter(runId)} and r._value == 1)
120
+ |> group(columns: ["_measurement"])
121
+ |> count()
122
+ `;
123
+ queryApi.queryRows(failedFlux, {
124
+ next: (row, tableMeta) => {
125
+ const o = tableMeta.toObject(row);
126
+ failed = o._value || 0;
127
+ },
128
+ error: (error) => {
129
+ const errorMsg = error instanceof Error ? error.message : String(error);
130
+ console.error(chalk_1.default.red(` ✗ Error querying failed requests: ${errorMsg}`));
131
+ reject(error);
132
+ },
133
+ complete: () => {
134
+ const success = Math.max(0, total - failed);
135
+ console.log(chalk_1.default.gray(` ✓ Request breakdown: ${success} success, ${failed} failed`));
136
+ resolve({ total, success, failed });
137
+ },
138
+ });
139
+ },
140
+ });
141
+ });
142
+ }
143
+ async getErrorBreakdown(startTime, endTime, runId) {
144
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
145
+ const flux = `
146
+ from(bucket: "${this.config.bucket}")
147
+ |> range(start: ${startTime}, stop: ${endTime})
148
+ |> filter(fn: (r) => r._measurement == "error_responses" and ${this.buildRunIdFilter(runId)})
149
+ |> group(columns: ["status"])
150
+ |> count()
151
+ `;
152
+ const errors = {};
153
+ return new Promise((resolve, reject) => {
154
+ queryApi.queryRows(flux, {
155
+ next: (row, tableMeta) => {
156
+ const o = tableMeta.toObject(row);
157
+ const status = o.status || "unknown";
158
+ errors[status] = o._value || 0;
159
+ },
160
+ error: (error) => {
161
+ const errorMsg = error instanceof Error ? error.message : String(error);
162
+ console.error(chalk_1.default.red(` ✗ Error querying error breakdown: ${errorMsg}`));
163
+ reject(error);
164
+ },
165
+ complete: () => {
166
+ console.log(chalk_1.default.gray(` ✓ Error breakdown: ${Object.keys(errors).length} unique error codes found`));
167
+ resolve(errors);
168
+ },
169
+ });
170
+ });
171
+ }
172
+ async getErrorDetails(startTime, endTime, runId) {
173
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
174
+ const flux = `
175
+ from(bucket: "${this.config.bucket}")
176
+ |> range(start: ${startTime}, stop: ${endTime})
177
+ |> filter(fn: (r) => r._measurement == "error_responses" and ${this.buildRunIdFilter(runId)})
178
+ |> group(columns: ["status", "url"])
179
+ |> count()
180
+ `;
181
+ const errorMap = {};
182
+ return new Promise((resolve, reject) => {
183
+ queryApi.queryRows(flux, {
184
+ next: (row, tableMeta) => {
185
+ const o = tableMeta.toObject(row);
186
+ const status = String(o.status || "unknown");
187
+ const url = String(o.url || "unknown");
188
+ const count = o._value || 0;
189
+ if (!errorMap[status]) {
190
+ errorMap[status] = {};
191
+ }
192
+ errorMap[status][url] = count;
193
+ },
194
+ error: (error) => {
195
+ const errorMsg = error instanceof Error ? error.message : String(error);
196
+ console.error(chalk_1.default.red(` ✗ Error querying error details: ${errorMsg}`));
197
+ reject(error);
198
+ },
199
+ complete: () => {
200
+ // Convert to array and flatten
201
+ const result = [];
202
+ Object.entries(errorMap).forEach(([status, urls]) => {
203
+ Object.entries(urls).forEach(([url, count]) => {
204
+ result.push({
205
+ status: parseInt(status, 10),
206
+ url,
207
+ count,
208
+ });
209
+ });
210
+ });
211
+ // Sort by status, then by count descending
212
+ result.sort((a, b) => {
213
+ if (a.status !== b.status)
214
+ return a.status - b.status;
215
+ return b.count - a.count;
216
+ });
217
+ console.log(chalk_1.default.gray(` ✓ Error details: ${result.length} error entries found`));
218
+ resolve(result);
219
+ },
220
+ });
221
+ });
222
+ }
223
+ async getErrorRequestsSummary(startTime, endTime, runId) {
224
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
225
+ // Query all error response times (non-2xx status) grouped by endpoint
226
+ const flux = `
227
+ from(bucket: "${this.config.bucket}")
228
+ |> range(start: ${startTime}, stop: ${endTime})
229
+ |> filter(fn: (r) => r._measurement == "http_req_duration" and ${this.buildRunIdFilter(runId)})
230
+ |> filter(fn: (r) => r.status !~ /^2/)
231
+ |> keep(columns: ["_value", "url"])
232
+ `;
233
+ const endpointData = {};
234
+ return new Promise((resolve, reject) => {
235
+ queryApi.queryRows(flux, {
236
+ next: (row, tableMeta) => {
237
+ const o = tableMeta.toObject(row);
238
+ const url = o.url || "unknown";
239
+ const value = o._value || 0;
240
+ if (!endpointData[url]) {
241
+ endpointData[url] = [];
242
+ }
243
+ endpointData[url].push(value);
244
+ },
245
+ error: (error) => {
246
+ const errorMsg = error instanceof Error ? error.message : String(error);
247
+ console.error(chalk_1.default.red(` ✗ Error querying error requests summary: ${errorMsg}`));
248
+ reject(error);
249
+ },
250
+ complete: () => {
251
+ const result = {};
252
+ Object.entries(endpointData).forEach(([url, values]) => {
253
+ const sorted = values.sort((a, b) => a - b);
254
+ result[url] = {
255
+ count: values.length,
256
+ minResponseTime: sorted[0] || 0,
257
+ avgResponseTime: values.reduce((a, b) => a + b, 0) / values.length,
258
+ p95ResponseTime: this.calculatePercentile(sorted, 0.95),
259
+ p99ResponseTime: this.calculatePercentile(sorted, 0.99),
260
+ maxResponseTime: sorted[sorted.length - 1] || 0,
261
+ };
262
+ });
263
+ console.log(chalk_1.default.gray(` ✓ Error requests summary: ${Object.keys(result).length} endpoints with errors`));
264
+ resolve(result);
265
+ },
266
+ });
267
+ });
268
+ }
269
+ async getErrorRequestsDetailedSummary(startTime, endTime, runId) {
270
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
271
+ // Query error response times grouped by endpoint, method, and status
272
+ const flux = `
273
+ from(bucket: "${this.config.bucket}")
274
+ |> range(start: ${startTime}, stop: ${endTime})
275
+ |> filter(fn: (r) => r._measurement == "http_req_duration" and ${this.buildRunIdFilter(runId)})
276
+ |> filter(fn: (r) => r.status !~ /^2/)
277
+ |> keep(columns: ["_value", "url", "method", "status"])
278
+ `;
279
+ const groupKey = (url, method, status) => `${url}|${method}|${status}`;
280
+ const errorGroupData = {};
281
+ return new Promise((resolve, reject) => {
282
+ queryApi.queryRows(flux, {
283
+ next: (row, tableMeta) => {
284
+ const o = tableMeta.toObject(row);
285
+ const url = o.url || "unknown";
286
+ const method = o.method || "unknown";
287
+ const status = o.status || 0;
288
+ const value = o._value || 0;
289
+ const key = groupKey(url, method, status);
290
+ if (!errorGroupData[key]) {
291
+ errorGroupData[key] = { url, method, status, values: [] };
292
+ }
293
+ errorGroupData[key].values.push(value);
294
+ },
295
+ error: (error) => {
296
+ const errorMsg = error instanceof Error ? error.message : String(error);
297
+ console.error(chalk_1.default.red(` ✗ Error querying detailed error requests: ${errorMsg}`));
298
+ reject(error);
299
+ },
300
+ complete: () => {
301
+ const result = [];
302
+ Object.values(errorGroupData).forEach(({ url, method, status, values }) => {
303
+ const sorted = values.sort((a, b) => a - b);
304
+ result.push({
305
+ url,
306
+ method,
307
+ status,
308
+ count: values.length,
309
+ minResponseTime: sorted[0] || 0,
310
+ avgResponseTime: values.reduce((a, b) => a + b, 0) / values.length,
311
+ p95ResponseTime: this.calculatePercentile(sorted, 0.95),
312
+ p99ResponseTime: this.calculatePercentile(sorted, 0.99),
313
+ maxResponseTime: sorted[sorted.length - 1] || 0,
314
+ });
315
+ });
316
+ // Sort by count descending
317
+ result.sort((a, b) => b.count - a.count);
318
+ console.log(chalk_1.default.gray(` ✓ Error requests detailed: ${result.length} error groups found`));
319
+ resolve(result);
320
+ },
321
+ });
322
+ });
323
+ }
324
+ async getVUsStats(startTime, endTime) {
325
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
326
+ let vusMax = 0;
327
+ let vusConfiguredMax = 0;
328
+ return new Promise((resolve, reject) => {
329
+ // Query vus measurement - get max value (peak VUs during test)
330
+ const vusFlux = `
331
+ from(bucket: "${this.config.bucket}")
332
+ |> range(start: ${startTime}, stop: ${endTime})
333
+ |> filter(fn: (r) => r._measurement == "vus")
334
+ |> filter(fn: (r) => r._field == "value")
335
+ |> max()
336
+ `;
337
+ queryApi.queryRows(vusFlux, {
338
+ next: (row, tableMeta) => {
339
+ const o = tableMeta.toObject(row);
340
+ vusMax = o._value || 0;
341
+ },
342
+ error: (error) => {
343
+ const errorMsg = error instanceof Error ? error.message : String(error);
344
+ console.error(chalk_1.default.red(` ✗ Error querying VUs: ${errorMsg}`));
345
+ reject(error);
346
+ },
347
+ complete: () => {
348
+ // Query vus_max measurement - get max value (configured max)
349
+ const vusMaxFlux = `
350
+ from(bucket: "${this.config.bucket}")
351
+ |> range(start: ${startTime}, stop: ${endTime})
352
+ |> filter(fn: (r) => r._measurement == "vus_max")
353
+ |> filter(fn: (r) => r._field == "value")
354
+ |> max()
355
+ `;
356
+ queryApi.queryRows(vusMaxFlux, {
357
+ next: (row, tableMeta) => {
358
+ const o = tableMeta.toObject(row);
359
+ vusConfiguredMax = o._value || 0;
360
+ },
361
+ error: (error) => {
362
+ const errorMsg = error instanceof Error ? error.message : String(error);
363
+ console.error(chalk_1.default.red(` ✗ Error querying VUs Max: ${errorMsg}`));
364
+ reject(error);
365
+ },
366
+ complete: () => {
367
+ console.log(chalk_1.default.gray(` ✓ VUs: current=${vusMax}, max=${vusConfiguredMax}`));
368
+ resolve({ vusMax, vusConfiguredMax });
369
+ },
370
+ });
371
+ },
372
+ });
373
+ });
374
+ }
375
+ async getPodCount(startTime, endTime) {
376
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
377
+ // Query the pods measurement directly
378
+ const flux = `
379
+ from(bucket: "${this.config.bucket}")
380
+ |> range(start: ${startTime}, stop: ${endTime})
381
+ |> filter(fn: (r) => r._measurement == "pods")
382
+ |> filter(fn: (r) => r._field == "value")
383
+ |> max()
384
+ `;
385
+ return new Promise((resolve, reject) => {
386
+ queryApi.queryRows(flux, {
387
+ next: (row, tableMeta) => {
388
+ const o = tableMeta.toObject(row);
389
+ resolve(o._value || 0);
390
+ },
391
+ error: (error) => {
392
+ const errorMsg = error instanceof Error ? error.message : String(error);
393
+ console.error(chalk_1.default.red(` ✗ Error querying pods: ${errorMsg}`));
394
+ reject(error);
395
+ },
396
+ complete: () => {
397
+ resolve(0);
398
+ },
399
+ });
400
+ });
401
+ }
402
+ async getDroppedIterations(startTime, endTime, runId) {
403
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
404
+ const flux = `
405
+ from(bucket: "${this.config.bucket}")
406
+ |> range(start: ${startTime}, stop: ${endTime})
407
+ |> filter(fn: (r) => r._measurement == "dropped_iterations" and ${this.buildRunIdFilter(runId)})
408
+ |> group(columns: ["_measurement"])
409
+ |> sum()
410
+ `;
411
+ return new Promise((resolve, reject) => {
412
+ queryApi.queryRows(flux, {
413
+ next: (row, tableMeta) => {
414
+ const o = tableMeta.toObject(row);
415
+ resolve(o._value || 0);
416
+ },
417
+ error: (error) => {
418
+ const errorMsg = error instanceof Error ? error.message : String(error);
419
+ console.error(chalk_1.default.red(` ✗ Error querying dropped iterations: ${errorMsg}`));
420
+ reject(error);
421
+ },
422
+ complete: () => {
423
+ resolve(0);
424
+ },
425
+ });
426
+ });
427
+ }
428
+ async getRPSStats(startTime, endTime, runId) {
429
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
430
+ const flux = `
431
+ from(bucket: "${this.config.bucket}")
432
+ |> range(start: ${startTime}, stop: ${endTime})
433
+ |> filter(fn: (r) => r._measurement == "http_reqs" and ${this.buildRunIdFilter(runId)})
434
+ |> aggregateWindow(every: 10s, fn: sum)
435
+ |> keep(columns: ["_value"])
436
+ `;
437
+ const rpsValues = [];
438
+ return new Promise((resolve, reject) => {
439
+ queryApi.queryRows(flux, {
440
+ next: (row, tableMeta) => {
441
+ const o = tableMeta.toObject(row);
442
+ if (typeof o._value === "number") {
443
+ rpsValues.push(o._value);
444
+ }
445
+ },
446
+ error: (error) => {
447
+ const errorMsg = error instanceof Error ? error.message : String(error);
448
+ console.error(chalk_1.default.red(` ✗ Error querying RPS: ${errorMsg}`));
449
+ reject(error);
450
+ },
451
+ complete: () => {
452
+ if (rpsValues.length === 0) {
453
+ resolve({ rpsMax: 0, rpsAvg: 0, rpsP95: 0 });
454
+ return;
455
+ }
456
+ rpsValues.sort((a, b) => a - b);
457
+ const rpsMax = rpsValues[rpsValues.length - 1];
458
+ const rpsAvg = rpsValues.reduce((a, b) => a + b, 0) / rpsValues.length;
459
+ const rpsP95 = this.calculatePercentile(rpsValues, 0.95);
460
+ resolve({ rpsMax, rpsAvg, rpsP95 });
461
+ },
462
+ });
463
+ });
464
+ }
465
+ async getThroughput(startTime, endTime, runId) {
466
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
467
+ const flux = `
468
+ from(bucket: "${this.config.bucket}")
469
+ |> range(start: ${startTime}, stop: ${endTime})
470
+ |> filter(fn: (r) => r._measurement == "http_reqs" and ${this.buildRunIdFilter(runId)})
471
+ |> aggregateWindow(every: 10s, fn: sum)
472
+ |> mean()
473
+ `;
474
+ return new Promise((resolve, reject) => {
475
+ queryApi.queryRows(flux, {
476
+ next: (row, tableMeta) => {
477
+ const o = tableMeta.toObject(row);
478
+ if (typeof o._value === "number") {
479
+ resolve(o._value);
480
+ }
481
+ },
482
+ error: (error) => {
483
+ const errorMsg = error instanceof Error ? error.message : String(error);
484
+ console.error(chalk_1.default.red(` ✗ Error querying RPS: ${errorMsg}`));
485
+ reject(error);
486
+ },
487
+ complete: () => {
488
+ resolve(0);
489
+ },
490
+ });
491
+ });
492
+ }
493
+ async getSlowestRequests(startTime, endTime, topN = 10, runId) {
494
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
495
+ const flux = `
496
+ from(bucket: "${this.config.bucket}")
497
+ |> range(start: ${startTime}, stop: ${endTime})
498
+ |> filter(fn: (r) => r._measurement == "http_req_duration" and ${this.buildRunIdFilter(runId)})
499
+ |> keep(columns: ["_value", "url"])
500
+ |> group(columns: ["url"])
501
+ |> sort(columns: ["_value"], desc: true)
502
+ `;
503
+ const endpointStats = {};
504
+ return new Promise((resolve, reject) => {
505
+ queryApi.queryRows(flux, {
506
+ next: (row, tableMeta) => {
507
+ const o = tableMeta.toObject(row);
508
+ const url = o.url || "unknown";
509
+ const value = o._value || 0;
510
+ if (!endpointStats[url]) {
511
+ endpointStats[url] = [];
512
+ }
513
+ endpointStats[url].push(value);
514
+ },
515
+ error: (error) => {
516
+ const errorMsg = error instanceof Error ? error.message : String(error);
517
+ console.error(chalk_1.default.red(` ✗ Error querying slowest requests: ${errorMsg}`));
518
+ reject(error);
519
+ },
520
+ complete: () => {
521
+ // Calculate p95 and max for each endpoint
522
+ const slowestRequests = [];
523
+ Object.entries(endpointStats).forEach(([url, values]) => {
524
+ const sorted = values.sort((a, b) => a - b);
525
+ const max = sorted[sorted.length - 1] || 0;
526
+ const p95 = this.calculatePercentile(sorted, 0.95);
527
+ slowestRequests.push({ url, p95, max });
528
+ });
529
+ // Sort by p95 descending and limit to topN
530
+ slowestRequests.sort((a, b) => b.p95 - a.p95).slice(0, topN);
531
+ console.log(chalk_1.default.gray(` ✓ Retrieved slowest unique endpoints: ${slowestRequests.length}`));
532
+ resolve(slowestRequests.slice(0, topN));
533
+ },
534
+ });
535
+ });
536
+ }
537
+ async getIterationCount(startTime, endTime, runId) {
538
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
539
+ const flux = `
540
+ from(bucket: "${this.config.bucket}")
541
+ |> range(start: ${startTime}, stop: ${endTime})
542
+ |> filter(fn: (r) => r._measurement == "iterations" and ${this.buildRunIdFilter(runId)})
543
+ |> filter(fn: (r) => r._field == "value")
544
+ |> sum()
545
+ `;
546
+ return new Promise((resolve, reject) => {
547
+ queryApi.queryRows(flux, {
548
+ next: (row, tableMeta) => {
549
+ const o = tableMeta.toObject(row);
550
+ resolve(o._value || 0);
551
+ },
552
+ error: (error) => {
553
+ const errorMsg = error instanceof Error ? error.message : String(error);
554
+ console.error(chalk_1.default.red(` ✗ Error querying iterations: ${errorMsg}`));
555
+ reject(error);
556
+ },
557
+ complete: () => {
558
+ resolve(0);
559
+ },
560
+ });
561
+ });
562
+ }
563
+ async getIterationDurationPercentiles(startTime, endTime, runId) {
564
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
565
+ const flux = `
566
+ from(bucket: "${this.config.bucket}")
567
+ |> range(start: ${startTime}, stop: ${endTime})
568
+ |> filter(fn: (r) => r._measurement == "iteration_duration" and ${this.buildRunIdFilter(runId)})
569
+ |> quantile(q: 0.99)
570
+ `;
571
+ let p99 = 0;
572
+ return new Promise((resolve, reject) => {
573
+ queryApi.queryRows(flux, {
574
+ next: (row, tableMeta) => {
575
+ const o = tableMeta.toObject(row);
576
+ if (typeof o._value === "number") {
577
+ p99 = o._value;
578
+ }
579
+ },
580
+ error: (error) => {
581
+ const errorMsg = error instanceof Error ? error.message : String(error);
582
+ console.error(chalk_1.default.red(` ✗ Error querying iteration duration: ${errorMsg}`));
583
+ reject(error);
584
+ },
585
+ complete: () => {
586
+ console.log(chalk_1.default.gray(` ✓ Retrieved iteration duration percentiles - p99=${p99.toFixed(2)}ms`));
587
+ resolve({
588
+ p50: 0,
589
+ p95: 0,
590
+ p99,
591
+ });
592
+ },
593
+ });
594
+ });
595
+ }
596
+ async getChecksStats(startTime, endTime, runId) {
597
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
598
+ const flux = `
599
+ from(bucket: "${this.config.bucket}")
600
+ |> range(start: ${startTime}, stop: ${endTime})
601
+ |> filter(fn: (r) => r._measurement == "checks" and ${this.buildRunIdFilter(runId)})
602
+ |> group(columns: ["_measurement"])
603
+ |> count()
604
+ `;
605
+ return new Promise((resolve, reject) => {
606
+ let total = 0;
607
+ queryApi.queryRows(flux, {
608
+ next: (row, tableMeta) => {
609
+ const o = tableMeta.toObject(row);
610
+ total = o._value;
611
+ },
612
+ error: (error) => {
613
+ const errorMsg = error instanceof Error ? error.message : String(error);
614
+ console.error(chalk_1.default.red(` ✗ Error querying checks: ${errorMsg}`));
615
+ reject(error);
616
+ },
617
+ complete: () => {
618
+ console.log(chalk_1.default.gray(` ✓ Checks: total=${total}`));
619
+ resolve({
620
+ total,
621
+ succeeded: total,
622
+ failed: 0,
623
+ successRate: total > 0 ? 100 : 0,
624
+ });
625
+ },
626
+ });
627
+ });
628
+ }
629
+ async getHttpPhaseStats(startTime, endTime, runId) {
630
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
631
+ const phases = [
632
+ "http_req_blocked",
633
+ "http_req_connecting",
634
+ "http_req_tls_handshaking",
635
+ "http_req_sending",
636
+ "http_req_receiving",
637
+ "http_req_waiting",
638
+ ];
639
+ const result = {};
640
+ // Query all phases in parallel
641
+ const promises = phases.map((phase) => {
642
+ return new Promise((resolve, reject) => {
643
+ const flux = `
644
+ from(bucket: "${this.config.bucket}")
645
+ |> range(start: ${startTime}, stop: ${endTime})
646
+ |> filter(fn: (r) => r._measurement == "${phase}" and ${this.buildRunIdFilter(runId)})
647
+ |> keep(columns: ["_value"])
648
+ `;
649
+ const values = [];
650
+ queryApi.queryRows(flux, {
651
+ next: (row, tableMeta) => {
652
+ const o = tableMeta.toObject(row);
653
+ values.push(o._value);
654
+ },
655
+ error: (error) => reject(error),
656
+ complete: () => {
657
+ if (values.length === 0) {
658
+ result[phase] = { avg: 0, min: 0, max: 0, p90: 0, p95: 0 };
659
+ }
660
+ else {
661
+ const sorted = values.sort((a, b) => a - b);
662
+ const sum = values.reduce((a, b) => a + b, 0);
663
+ result[phase] = {
664
+ avg: sum / values.length,
665
+ min: sorted[0],
666
+ max: sorted[sorted.length - 1],
667
+ p90: this.calculatePercentile(sorted, 0.9),
668
+ p95: this.calculatePercentile(sorted, 0.95),
669
+ };
670
+ }
671
+ resolve();
672
+ },
673
+ });
674
+ });
675
+ });
676
+ await Promise.all(promises).catch((error) => {
677
+ const errorMsg = error instanceof Error ? error.message : String(error);
678
+ console.error(chalk_1.default.red(` ✗ Error querying HTTP phases: ${errorMsg}`));
679
+ throw error;
680
+ });
681
+ return result;
682
+ }
683
+ async getDataTransferStats(startTime, endTime, runId) {
684
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
685
+ // Calculate duration in seconds
686
+ const startDate = new Date(startTime);
687
+ const endDate = new Date(endTime);
688
+ const durationSeconds = (endDate.getTime() - startDate.getTime()) / 1000 || 1;
689
+ // Query data_received and data_sent totals
690
+ const getTotal = (measurement) => {
691
+ return new Promise((resolve, reject) => {
692
+ const flux = `
693
+ from(bucket: "${this.config.bucket}")
694
+ |> range(start: ${startTime}, stop: ${endTime})
695
+ |> filter(fn: (r) => r._measurement == "${measurement}" and ${this.buildRunIdFilter(runId)})
696
+ |> group(columns: ["_measurement"])
697
+ |> sum()
698
+ `;
699
+ let total = 0;
700
+ queryApi.queryRows(flux, {
701
+ next: (row, tableMeta) => {
702
+ const o = tableMeta.toObject(row);
703
+ total = o._value;
704
+ },
705
+ error: (error) => reject(error),
706
+ complete: () => resolve(total),
707
+ });
708
+ });
709
+ };
710
+ return new Promise((resolve, reject) => {
711
+ Promise.all([getTotal("data_received"), getTotal("data_sent")])
712
+ .then(([received, sent]) => {
713
+ const receivedRate = received / durationSeconds;
714
+ const sentRate = sent / durationSeconds;
715
+ resolve({
716
+ received,
717
+ sent,
718
+ receivedRate,
719
+ sentRate,
720
+ });
721
+ })
722
+ .catch((error) => {
723
+ const errorMsg = error instanceof Error ? error.message : String(error);
724
+ console.error(chalk_1.default.red(` ✗ Error querying data transfer: ${errorMsg}`));
725
+ reject(error);
726
+ });
727
+ });
728
+ }
729
+ async getRequestsByEndpoint(startTime, endTime, runId) {
730
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
731
+ const flux = `
732
+ from(bucket: "${this.config.bucket}")
733
+ |> range(start: ${startTime}, stop: ${endTime})
734
+ |> filter(fn: (r) => r._measurement == "http_req_duration" and ${this.buildRunIdFilter(runId)})
735
+ |> group(columns: ["url"])
736
+ |> count()
737
+ |> sort(columns: ["_value"], desc: true)
738
+ `;
739
+ const result = {};
740
+ return new Promise((resolve, reject) => {
741
+ queryApi.queryRows(flux, {
742
+ next: (row, tableMeta) => {
743
+ const o = tableMeta.toObject(row);
744
+ const url = o.url || "unknown";
745
+ const count = o._value || 0;
746
+ result[url] = count;
747
+ },
748
+ error: (error) => {
749
+ const errorMsg = error instanceof Error ? error.message : String(error);
750
+ console.error(chalk_1.default.red(` ✗ Error querying requests by endpoint: ${errorMsg}`));
751
+ reject(error);
752
+ },
753
+ complete: () => {
754
+ const endpointCount = Object.keys(result).length;
755
+ console.log(chalk_1.default.gray(` ✓ Requests by endpoint: ${endpointCount} unique endpoints`));
756
+ resolve(result);
757
+ },
758
+ });
759
+ });
760
+ }
761
+ async getRpsPerEndpoint(startTime, endTime, runId) {
762
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
763
+ const flux = `
764
+ from(bucket: "${this.config.bucket}")
765
+ |> range(start: ${startTime}, stop: ${endTime})
766
+ |> filter(fn: (r) => r._measurement == "http_reqs" and ${this.buildRunIdFilter(runId)})
767
+ |> group(columns: ["url"])
768
+ |> aggregateWindow(every: 10s, fn: sum)
769
+ |> keep(columns: ["_value", "url"])
770
+ `;
771
+ const rpsPerEndpoint = {};
772
+ return new Promise((resolve, reject) => {
773
+ queryApi.queryRows(flux, {
774
+ next: (row, tableMeta) => {
775
+ const o = tableMeta.toObject(row);
776
+ const url = o.url || "unknown";
777
+ const count = o._value || 0;
778
+ if (!rpsPerEndpoint[url]) {
779
+ rpsPerEndpoint[url] = [];
780
+ }
781
+ rpsPerEndpoint[url].push(count);
782
+ },
783
+ error: (error) => {
784
+ const errorMsg = error instanceof Error ? error.message : String(error);
785
+ console.error(chalk_1.default.red(` ✗ Error querying RPS per endpoint: ${errorMsg}`));
786
+ reject(error);
787
+ },
788
+ complete: () => {
789
+ const result = {};
790
+ Object.entries(rpsPerEndpoint).forEach(([url, values]) => {
791
+ const sorted = values.sort((a, b) => a - b);
792
+ const max = sorted[sorted.length - 1] || 0;
793
+ const avg = values.reduce((a, b) => a + b, 0) / values.length;
794
+ const p95 = this.calculatePercentile(sorted, 0.95);
795
+ result[url] = { avg, max, p95 };
796
+ });
797
+ console.log(chalk_1.default.gray(` ✓ RPS per endpoint: ${Object.keys(result).length} unique endpoints`));
798
+ resolve(result);
799
+ },
800
+ });
801
+ });
802
+ }
803
+ async getRpsTimeSeriesByEndpoint(startTime, endTime, interval = "10s", runId) {
804
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
805
+ const flux = `
806
+ from(bucket: "${this.config.bucket}")
807
+ |> range(start: ${startTime}, stop: ${endTime})
808
+ |> filter(fn: (r) => r._measurement == "http_reqs" and ${this.buildRunIdFilter(runId)})
809
+ |> group(columns: ["url"])
810
+ |> aggregateWindow(every: ${interval}, fn: sum)
811
+ |> keep(columns: ["_time", "_value", "url"])
812
+ `;
813
+ const result = {};
814
+ return new Promise((resolve, reject) => {
815
+ queryApi.queryRows(flux, {
816
+ next: (row, tableMeta) => {
817
+ const o = tableMeta.toObject(row);
818
+ const url = o.url || "unknown";
819
+ const timestamp = new Date(o._time);
820
+ const rps = o._value || 0;
821
+ if (!result[url]) {
822
+ result[url] = [];
823
+ }
824
+ result[url].push({ timestamp, rps });
825
+ },
826
+ error: (error) => {
827
+ const errorMsg = error instanceof Error ? error.message : String(error);
828
+ console.error(chalk_1.default.red(` ✗ Error querying RPS time series: ${errorMsg}`));
829
+ reject(error);
830
+ },
831
+ complete: () => {
832
+ const endpointCount = Object.keys(result).length;
833
+ console.log(chalk_1.default.gray(` ✓ RPS time series: ${endpointCount} endpoints`));
834
+ resolve(result);
835
+ },
836
+ });
837
+ });
838
+ }
839
+ async getRequestsSummary(startTime, endTime, runId) {
840
+ const queryApi = this.influxDB.getQueryApi(this.config.org);
841
+ // Query all response times grouped by endpoint and method
842
+ const flux = `
843
+ from(bucket: "${this.config.bucket}")
844
+ |> range(start: ${startTime}, stop: ${endTime})
845
+ |> filter(fn: (r) => r._measurement == "http_req_duration" and ${this.buildRunIdFilter(runId)})
846
+ |> keep(columns: ["_value", "url", "method", "status"])
847
+ `;
848
+ const groupKey = (url, method) => `${url}|${method}`;
849
+ const groupData = {};
850
+ return new Promise((resolve, reject) => {
851
+ queryApi.queryRows(flux, {
852
+ next: (row, tableMeta) => {
853
+ const o = tableMeta.toObject(row);
854
+ const url = o.url || "unknown";
855
+ const method = o.method || "unknown";
856
+ const status = parseInt(String(o.status) || "0", 10);
857
+ const value = o._value || 0;
858
+ const key = groupKey(url, method);
859
+ if (!groupData[key]) {
860
+ groupData[key] = { url, method, values: [], statuses: {} };
861
+ }
862
+ groupData[key].values.push(value);
863
+ groupData[key].statuses[status] = (groupData[key].statuses[status] || 0) + 1;
864
+ },
865
+ error: (error) => {
866
+ const errorMsg = error instanceof Error ? error.message : String(error);
867
+ console.error(chalk_1.default.red(` ✗ Error querying requests summary: ${errorMsg}`));
868
+ reject(error);
869
+ },
870
+ complete: () => {
871
+ const result = {};
872
+ Object.values(groupData).forEach(({ url, method, values, statuses }) => {
873
+ const sortedValues = values.sort((a, b) => a - b);
874
+ const successful = Object.entries(statuses)
875
+ .filter(([status]) => status.startsWith("2"))
876
+ .reduce((sum, [, count]) => sum + count, 0);
877
+ const failed = sortedValues.length - successful;
878
+ result[`${url}|${method}`] = {
879
+ url,
880
+ method,
881
+ count: sortedValues.length,
882
+ successful,
883
+ failed,
884
+ minResponseTime: sortedValues[0] || 0,
885
+ avgResponseTime: sortedValues.reduce((a, b) => a + b, 0) / sortedValues.length,
886
+ p95ResponseTime: this.calculatePercentile(sortedValues, 0.95),
887
+ p99ResponseTime: this.calculatePercentile(sortedValues, 0.99),
888
+ maxResponseTime: sortedValues[sortedValues.length - 1] || 0,
889
+ };
890
+ });
891
+ console.log(chalk_1.default.gray(` ✓ Requests summary: ${Object.keys(result).length} unique endpoint/method combinations`));
892
+ resolve(result);
893
+ },
894
+ });
895
+ });
896
+ }
897
+ }
898
+ exports.InfluxQueryClient = InfluxQueryClient;
899
+ //# sourceMappingURL=influx.js.map