@testsmith/perfornium 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +360 -0
  2. package/dist/cli/cli.d.ts +2 -0
  3. package/dist/cli/cli.js +192 -0
  4. package/dist/cli/commands/distributed.d.ts +11 -0
  5. package/dist/cli/commands/distributed.js +179 -0
  6. package/dist/cli/commands/import.d.ts +23 -0
  7. package/dist/cli/commands/import.js +461 -0
  8. package/dist/cli/commands/init.d.ts +7 -0
  9. package/dist/cli/commands/init.js +923 -0
  10. package/dist/cli/commands/mock.d.ts +7 -0
  11. package/dist/cli/commands/mock.js +281 -0
  12. package/dist/cli/commands/report.d.ts +5 -0
  13. package/dist/cli/commands/report.js +70 -0
  14. package/dist/cli/commands/run.d.ts +12 -0
  15. package/dist/cli/commands/run.js +260 -0
  16. package/dist/cli/commands/validate.d.ts +3 -0
  17. package/dist/cli/commands/validate.js +35 -0
  18. package/dist/cli/commands/worker.d.ts +27 -0
  19. package/dist/cli/commands/worker.js +320 -0
  20. package/dist/config/index.d.ts +2 -0
  21. package/dist/config/index.js +20 -0
  22. package/dist/config/parser.d.ts +19 -0
  23. package/dist/config/parser.js +330 -0
  24. package/dist/config/types/global-config.d.ts +74 -0
  25. package/dist/config/types/global-config.js +2 -0
  26. package/dist/config/types/hooks.d.ts +58 -0
  27. package/dist/config/types/hooks.js +3 -0
  28. package/dist/config/types/import-types.d.ts +33 -0
  29. package/dist/config/types/import-types.js +2 -0
  30. package/dist/config/types/index.d.ts +11 -0
  31. package/dist/config/types/index.js +27 -0
  32. package/dist/config/types/load-config.d.ts +32 -0
  33. package/dist/config/types/load-config.js +9 -0
  34. package/dist/config/types/output-config.d.ts +10 -0
  35. package/dist/config/types/output-config.js +2 -0
  36. package/dist/config/types/report-config.d.ts +10 -0
  37. package/dist/config/types/report-config.js +2 -0
  38. package/dist/config/types/runtime-types.d.ts +6 -0
  39. package/dist/config/types/runtime-types.js +2 -0
  40. package/dist/config/types/scenario-config.d.ts +30 -0
  41. package/dist/config/types/scenario-config.js +2 -0
  42. package/dist/config/types/step-types.d.ts +139 -0
  43. package/dist/config/types/step-types.js +2 -0
  44. package/dist/config/types/test-configuration.d.ts +18 -0
  45. package/dist/config/types/test-configuration.js +2 -0
  46. package/dist/config/types/worker-config.d.ts +12 -0
  47. package/dist/config/types/worker-config.js +2 -0
  48. package/dist/config/validator.d.ts +19 -0
  49. package/dist/config/validator.js +198 -0
  50. package/dist/core/csv-data-provider.d.ts +47 -0
  51. package/dist/core/csv-data-provider.js +265 -0
  52. package/dist/core/hooks-manager.d.ts +33 -0
  53. package/dist/core/hooks-manager.js +129 -0
  54. package/dist/core/index.d.ts +5 -0
  55. package/dist/core/index.js +11 -0
  56. package/dist/core/script-executor.d.ts +14 -0
  57. package/dist/core/script-executor.js +290 -0
  58. package/dist/core/step-executor.d.ts +41 -0
  59. package/dist/core/step-executor.js +680 -0
  60. package/dist/core/test-runner.d.ts +34 -0
  61. package/dist/core/test-runner.js +465 -0
  62. package/dist/core/threshold-evaluator.d.ts +43 -0
  63. package/dist/core/threshold-evaluator.js +170 -0
  64. package/dist/core/virtual-user-pool.d.ts +42 -0
  65. package/dist/core/virtual-user-pool.js +136 -0
  66. package/dist/core/virtual-user.d.ts +51 -0
  67. package/dist/core/virtual-user.js +488 -0
  68. package/dist/distributed/coordinator.d.ts +34 -0
  69. package/dist/distributed/coordinator.js +158 -0
  70. package/dist/distributed/health-monitor.d.ts +18 -0
  71. package/dist/distributed/health-monitor.js +72 -0
  72. package/dist/distributed/load-distributor.d.ts +17 -0
  73. package/dist/distributed/load-distributor.js +106 -0
  74. package/dist/distributed/remote-worker.d.ts +37 -0
  75. package/dist/distributed/remote-worker.js +241 -0
  76. package/dist/distributed/result-aggregator.d.ts +43 -0
  77. package/dist/distributed/result-aggregator.js +146 -0
  78. package/dist/dsl/index.d.ts +3 -0
  79. package/dist/dsl/index.js +11 -0
  80. package/dist/dsl/test-builder.d.ts +111 -0
  81. package/dist/dsl/test-builder.js +514 -0
  82. package/dist/importers/har-importer.d.ts +17 -0
  83. package/dist/importers/har-importer.js +172 -0
  84. package/dist/importers/open-api-importer.d.ts +23 -0
  85. package/dist/importers/open-api-importer.js +181 -0
  86. package/dist/importers/wsdl-importer.d.ts +42 -0
  87. package/dist/importers/wsdl-importer.js +440 -0
  88. package/dist/index.d.ts +5 -0
  89. package/dist/index.js +17 -0
  90. package/dist/load-patterns/arrivals.d.ts +7 -0
  91. package/dist/load-patterns/arrivals.js +118 -0
  92. package/dist/load-patterns/base.d.ts +9 -0
  93. package/dist/load-patterns/base.js +2 -0
  94. package/dist/load-patterns/basic.d.ts +7 -0
  95. package/dist/load-patterns/basic.js +117 -0
  96. package/dist/load-patterns/stepping.d.ts +6 -0
  97. package/dist/load-patterns/stepping.js +122 -0
  98. package/dist/metrics/collector.d.ts +72 -0
  99. package/dist/metrics/collector.js +662 -0
  100. package/dist/metrics/types.d.ts +135 -0
  101. package/dist/metrics/types.js +2 -0
  102. package/dist/outputs/base.d.ts +7 -0
  103. package/dist/outputs/base.js +2 -0
  104. package/dist/outputs/csv.d.ts +13 -0
  105. package/dist/outputs/csv.js +163 -0
  106. package/dist/outputs/graphite.d.ts +13 -0
  107. package/dist/outputs/graphite.js +126 -0
  108. package/dist/outputs/influxdb.d.ts +12 -0
  109. package/dist/outputs/influxdb.js +82 -0
  110. package/dist/outputs/json.d.ts +14 -0
  111. package/dist/outputs/json.js +107 -0
  112. package/dist/outputs/streaming-csv.d.ts +37 -0
  113. package/dist/outputs/streaming-csv.js +254 -0
  114. package/dist/outputs/streaming-json.d.ts +43 -0
  115. package/dist/outputs/streaming-json.js +353 -0
  116. package/dist/outputs/webhook.d.ts +16 -0
  117. package/dist/outputs/webhook.js +96 -0
  118. package/dist/protocols/base.d.ts +33 -0
  119. package/dist/protocols/base.js +2 -0
  120. package/dist/protocols/rest/handler.d.ts +67 -0
  121. package/dist/protocols/rest/handler.js +776 -0
  122. package/dist/protocols/soap/handler.d.ts +12 -0
  123. package/dist/protocols/soap/handler.js +165 -0
  124. package/dist/protocols/web/core-web-vitals.d.ts +121 -0
  125. package/dist/protocols/web/core-web-vitals.js +373 -0
  126. package/dist/protocols/web/handler.d.ts +50 -0
  127. package/dist/protocols/web/handler.js +706 -0
  128. package/dist/recorder/native-recorder.d.ts +14 -0
  129. package/dist/recorder/native-recorder.js +533 -0
  130. package/dist/recorder/scenario-recorder.d.ts +55 -0
  131. package/dist/recorder/scenario-recorder.js +296 -0
  132. package/dist/reporting/constants.d.ts +94 -0
  133. package/dist/reporting/constants.js +82 -0
  134. package/dist/reporting/enhanced-html-generator.d.ts +55 -0
  135. package/dist/reporting/enhanced-html-generator.js +965 -0
  136. package/dist/reporting/generator.d.ts +42 -0
  137. package/dist/reporting/generator.js +1217 -0
  138. package/dist/reporting/statistics.d.ts +144 -0
  139. package/dist/reporting/statistics.js +742 -0
  140. package/dist/reporting/templates/enhanced-report.hbs +2812 -0
  141. package/dist/reporting/templates/html.hbs +2453 -0
  142. package/dist/utils/faker-manager.d.ts +55 -0
  143. package/dist/utils/faker-manager.js +166 -0
  144. package/dist/utils/file-manager.d.ts +33 -0
  145. package/dist/utils/file-manager.js +154 -0
  146. package/dist/utils/handlebars-manager.d.ts +42 -0
  147. package/dist/utils/handlebars-manager.js +172 -0
  148. package/dist/utils/logger.d.ts +16 -0
  149. package/dist/utils/logger.js +46 -0
  150. package/dist/utils/template.d.ts +80 -0
  151. package/dist/utils/template.js +513 -0
  152. package/dist/utils/test-output-writer.d.ts +56 -0
  153. package/dist/utils/test-output-writer.js +643 -0
  154. package/dist/utils/time.d.ts +3 -0
  155. package/dist/utils/time.js +23 -0
  156. package/dist/utils/timestamp-helper.d.ts +17 -0
  157. package/dist/utils/timestamp-helper.js +53 -0
  158. package/dist/workers/manager.d.ts +18 -0
  159. package/dist/workers/manager.js +95 -0
  160. package/dist/workers/server.d.ts +21 -0
  161. package/dist/workers/server.js +205 -0
  162. package/dist/workers/worker.d.ts +19 -0
  163. package/dist/workers/worker.js +147 -0
  164. package/package.json +102 -0
@@ -0,0 +1,742 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StatisticsCalculator = void 0;
4
+ exports.isMeasurableResult = isMeasurableResult;
5
+ const constants_1 = require("./constants");
6
+ // Commands that should be measured (verifications, waits, measurements)
7
+ // Actions like click, fill, press, goto, select are NOT measured
8
+ const MEASURABLE_COMMANDS = [
9
+ 'verify_exists', 'verify_visible', 'verify_text', 'verify_contains', 'verify_not_exists',
10
+ 'wait_for_selector', 'wait_for_text', 'wait_for_load_state',
11
+ 'measure_web_vitals', 'performance_audit',
12
+ 'network_idle', 'dom_ready'
13
+ ];
14
+ /**
15
+ * Check if a result should be included in statistics (only verifications/measurements)
16
+ */
17
+ function isMeasurableResult(result) {
18
+ // If shouldRecord is explicitly set, use that
19
+ if (result.shouldRecord === true) {
20
+ return true;
21
+ }
22
+ if (result.shouldRecord === false) {
23
+ return false;
24
+ }
25
+ // Check if the action/command is measurable
26
+ const action = (result.action || result.step_name || '').toLowerCase();
27
+ return MEASURABLE_COMMANDS.some(cmd => action.includes(cmd));
28
+ }
29
+ class StatisticsCalculator {
30
+ /**
31
+ * Calculate percentiles using linear interpolation (consistent method)
32
+ */
33
+ static calculatePercentiles(values, percentiles = constants_1.PERCENTILES.EXTENDED) {
34
+ if (values.length === 0)
35
+ return {};
36
+ const sorted = [...values].sort((a, b) => a - b);
37
+ const result = {};
38
+ percentiles.forEach(p => {
39
+ if (p === 100) {
40
+ result[p] = sorted[sorted.length - 1];
41
+ }
42
+ else if (p === 0) {
43
+ result[p] = sorted[0];
44
+ }
45
+ else {
46
+ // Linear interpolation for accurate percentile calculation
47
+ const index = (p / 100) * (sorted.length - 1);
48
+ const lower = Math.floor(index);
49
+ const upper = Math.ceil(index);
50
+ if (lower === upper) {
51
+ result[p] = sorted[lower];
52
+ }
53
+ else {
54
+ const weight = index - lower;
55
+ result[p] = sorted[lower] * (1 - weight) + sorted[upper] * weight;
56
+ }
57
+ }
58
+ // Round to 2 decimal places
59
+ result[p] = Math.round(result[p] * 100) / 100;
60
+ });
61
+ return result;
62
+ }
63
+ /**
64
+ * Calculate Apdex (Application Performance Index) score
65
+ * Industry-standard metric for user satisfaction
66
+ * Only includes measurable results (verifications, not actions like click/fill)
67
+ */
68
+ static calculateApdexScore(results, satisfiedThreshold = constants_1.APDEX_DEFAULTS.SATISFIED_THRESHOLD) {
69
+ // Filter to only include measurable results (verifications)
70
+ const measurableResults = results.filter(isMeasurableResult);
71
+ const toleratingThreshold = satisfiedThreshold * constants_1.APDEX_DEFAULTS.TOLERATING_MULTIPLIER;
72
+ let satisfied = 0;
73
+ let tolerating = 0;
74
+ let frustrated = 0;
75
+ const successfulResults = measurableResults.filter(r => r.success);
76
+ successfulResults.forEach(result => {
77
+ const responseTime = (0, constants_1.getResponseTime)(result);
78
+ if (responseTime <= satisfiedThreshold) {
79
+ satisfied++;
80
+ }
81
+ else if (responseTime <= toleratingThreshold) {
82
+ tolerating++;
83
+ }
84
+ else {
85
+ frustrated++;
86
+ }
87
+ });
88
+ // Failed verifications are always frustrated
89
+ frustrated += measurableResults.filter(r => !r.success).length;
90
+ const total = measurableResults.length;
91
+ const score = total > 0 ? (satisfied + (tolerating / 2)) / total : 0;
92
+ // Rating based on Apdex score
93
+ let rating;
94
+ if (score >= 0.94)
95
+ rating = 'Excellent';
96
+ else if (score >= 0.85)
97
+ rating = 'Good';
98
+ else if (score >= 0.70)
99
+ rating = 'Fair';
100
+ else if (score >= 0.50)
101
+ rating = 'Poor';
102
+ else
103
+ rating = 'Unacceptable';
104
+ return {
105
+ score: Math.round(score * 1000) / 1000,
106
+ satisfied,
107
+ tolerating,
108
+ frustrated,
109
+ total,
110
+ rating,
111
+ };
112
+ }
113
+ /**
114
+ * Check SLA compliance against defined thresholds
115
+ * Only measures verifications, not actions like click/fill
116
+ */
117
+ static checkSLACompliance(results, slaConfig = {}) {
118
+ // Filter to only include measurable results (verifications)
119
+ const measurableResults = results.filter(isMeasurableResult);
120
+ const config = { ...constants_1.SLA_DEFAULTS, ...slaConfig };
121
+ const checks = [];
122
+ // Success rate check (based on verifications only)
123
+ const successRate = measurableResults.length > 0
124
+ ? (measurableResults.filter(r => r.success).length / measurableResults.length) * 100
125
+ : 0;
126
+ checks.push({
127
+ name: 'Success Rate',
128
+ target: config.SUCCESS_RATE,
129
+ actual: Math.round(successRate * 100) / 100,
130
+ passed: successRate >= config.SUCCESS_RATE,
131
+ unit: '%',
132
+ });
133
+ // Response time checks (based on verifications only)
134
+ const responseTimes = measurableResults.filter(r => r.success).map(r => (0, constants_1.getResponseTime)(r));
135
+ if (responseTimes.length > 0) {
136
+ const avgResponseTime = responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length;
137
+ checks.push({
138
+ name: 'Avg Response Time',
139
+ target: config.AVG_RESPONSE_TIME,
140
+ actual: Math.round(avgResponseTime * 100) / 100,
141
+ passed: avgResponseTime <= config.AVG_RESPONSE_TIME,
142
+ unit: 'ms',
143
+ });
144
+ const percentiles = this.calculatePercentiles(responseTimes, [95, 99]);
145
+ checks.push({
146
+ name: 'P95 Response Time',
147
+ target: config.P95_RESPONSE_TIME,
148
+ actual: percentiles[95] || 0,
149
+ passed: (percentiles[95] || 0) <= config.P95_RESPONSE_TIME,
150
+ unit: 'ms',
151
+ });
152
+ checks.push({
153
+ name: 'P99 Response Time',
154
+ target: config.P99_RESPONSE_TIME,
155
+ actual: percentiles[99] || 0,
156
+ passed: (percentiles[99] || 0) <= config.P99_RESPONSE_TIME,
157
+ unit: 'ms',
158
+ });
159
+ }
160
+ // Throughput check (based on measurable operations only)
161
+ if (measurableResults.length > 0) {
162
+ const timestamps = measurableResults.map(r => r.timestamp);
163
+ const duration = (Math.max(...timestamps) - Math.min(...timestamps)) / 1000;
164
+ const throughput = duration > 0 ? measurableResults.length / duration : 0;
165
+ checks.push({
166
+ name: 'Throughput',
167
+ target: config.MIN_REQUESTS_PER_SECOND,
168
+ actual: Math.round(throughput * 100) / 100,
169
+ passed: throughput >= config.MIN_REQUESTS_PER_SECOND,
170
+ unit: 'req/s',
171
+ });
172
+ }
173
+ const passed = checks.every(c => c.passed);
174
+ const failedChecks = checks.filter(c => !c.passed);
175
+ let summary;
176
+ if (passed) {
177
+ summary = 'All SLA targets met';
178
+ }
179
+ else {
180
+ summary = `${failedChecks.length} SLA violation(s): ${failedChecks.map(c => c.name).join(', ')}`;
181
+ }
182
+ return { passed, checks, summary };
183
+ }
184
+ /**
185
+ * Detect outliers using IQR method
186
+ * Only analyzes measurable results (verifications, not actions like click/fill)
187
+ */
188
+ static detectOutliers(results) {
189
+ // Filter to only measurable results, then filter to successful ones
190
+ const measurableResults = results.filter(isMeasurableResult);
191
+ const successfulResults = measurableResults.filter(r => r.success);
192
+ if (successfulResults.length < 4) {
193
+ return {
194
+ outliers: [],
195
+ outlierCount: 0,
196
+ outlierPercentage: 0,
197
+ lowerBound: 0,
198
+ upperBound: 0,
199
+ method: 'IQR',
200
+ };
201
+ }
202
+ const responseTimes = successfulResults.map(r => (0, constants_1.getResponseTime)(r));
203
+ const sorted = [...responseTimes].sort((a, b) => a - b);
204
+ // Calculate Q1, Q3, and IQR
205
+ const q1Index = Math.floor(sorted.length * 0.25);
206
+ const q3Index = Math.floor(sorted.length * 0.75);
207
+ const q1 = sorted[q1Index];
208
+ const q3 = sorted[q3Index];
209
+ const iqr = q3 - q1;
210
+ // Calculate bounds
211
+ const lowerBound = q1 - (constants_1.OUTLIER_DETECTION.IQR_MULTIPLIER * iqr);
212
+ const upperBound = q3 + (constants_1.OUTLIER_DETECTION.IQR_MULTIPLIER * iqr);
213
+ const extremeUpperBound = q3 + (constants_1.OUTLIER_DETECTION.IQR_EXTREME_MULTIPLIER * iqr);
214
+ const outliers = [];
215
+ successfulResults.forEach(result => {
216
+ const responseTime = (0, constants_1.getResponseTime)(result);
217
+ if (responseTime < lowerBound || responseTime > upperBound) {
218
+ outliers.push({
219
+ value: responseTime,
220
+ timestamp: result.timestamp,
221
+ vu_id: result.vu_id,
222
+ step_name: result.step_name,
223
+ severity: responseTime > extremeUpperBound ? 'extreme' : 'mild',
224
+ });
225
+ }
226
+ });
227
+ return {
228
+ outliers: outliers.sort((a, b) => b.value - a.value), // Sort by value descending
229
+ outlierCount: outliers.length,
230
+ outlierPercentage: Math.round((outliers.length / successfulResults.length) * 10000) / 100,
231
+ lowerBound: Math.max(0, Math.round(lowerBound * 100) / 100),
232
+ upperBound: Math.round(upperBound * 100) / 100,
233
+ method: 'IQR',
234
+ };
235
+ }
236
+ /**
237
+ * Calculate confidence interval for mean response time
238
+ */
239
+ static calculateConfidenceInterval(values, confidenceLevel = constants_1.CONFIDENCE_INTERVALS.DEFAULT_LEVEL) {
240
+ if (values.length < 2) {
241
+ const mean = values.length === 1 ? values[0] : 0;
242
+ return {
243
+ mean,
244
+ lower: mean,
245
+ upper: mean,
246
+ confidenceLevel,
247
+ standardError: 0,
248
+ marginOfError: 0,
249
+ };
250
+ }
251
+ const n = values.length;
252
+ const mean = values.reduce((a, b) => a + b, 0) / n;
253
+ // Calculate standard deviation
254
+ const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / (n - 1);
255
+ const stdDev = Math.sqrt(variance);
256
+ // Standard error
257
+ const standardError = stdDev / Math.sqrt(n);
258
+ // Z-score for confidence level (approximation for large samples)
259
+ // For 90%: 1.645, 95%: 1.96, 99%: 2.576
260
+ let zScore;
261
+ if (confidenceLevel >= 0.99)
262
+ zScore = 2.576;
263
+ else if (confidenceLevel >= 0.95)
264
+ zScore = 1.96;
265
+ else if (confidenceLevel >= 0.90)
266
+ zScore = 1.645;
267
+ else
268
+ zScore = 1.96; // Default to 95%
269
+ const marginOfError = zScore * standardError;
270
+ return {
271
+ mean: Math.round(mean * 100) / 100,
272
+ lower: Math.round((mean - marginOfError) * 100) / 100,
273
+ upper: Math.round((mean + marginOfError) * 100) / 100,
274
+ confidenceLevel,
275
+ standardError: Math.round(standardError * 100) / 100,
276
+ marginOfError: Math.round(marginOfError * 100) / 100,
277
+ };
278
+ }
279
+ /**
280
+ * Generate heatmap data for response time over time visualization
281
+ * Only includes measurable results (verifications, not actions like click/fill)
282
+ */
283
+ static generateHeatmapData(results, timeBuckets = constants_1.HEATMAP.TIME_BUCKETS, responseTimeBuckets = constants_1.HEATMAP.RESPONSE_TIME_BUCKETS) {
284
+ // Filter to only measurable results, then filter to successful ones
285
+ const measurableResults = results.filter(isMeasurableResult);
286
+ const successfulResults = measurableResults.filter(r => r.success);
287
+ if (successfulResults.length === 0) {
288
+ return {
289
+ data: [],
290
+ timeLabels: [],
291
+ responseTimeLabels: [],
292
+ maxValue: 0,
293
+ };
294
+ }
295
+ const timestamps = successfulResults.map(r => r.timestamp);
296
+ const responseTimes = successfulResults.map(r => (0, constants_1.getResponseTime)(r));
297
+ const minTime = Math.min(...timestamps);
298
+ const maxTime = Math.max(...timestamps);
299
+ const minRT = Math.min(...responseTimes);
300
+ const maxRT = Math.max(...responseTimes);
301
+ const timeRange = maxTime - minTime || 1;
302
+ const rtRange = maxRT - minRT || 1;
303
+ const timeBucketSize = timeRange / timeBuckets;
304
+ const rtBucketSize = rtRange / responseTimeBuckets;
305
+ // Initialize 2D array
306
+ const data = Array(responseTimeBuckets)
307
+ .fill(null)
308
+ .map(() => Array(timeBuckets).fill(0));
309
+ // Populate heatmap
310
+ successfulResults.forEach(result => {
311
+ const timeBucket = Math.min(Math.floor((result.timestamp - minTime) / timeBucketSize), timeBuckets - 1);
312
+ const rtBucket = Math.min(Math.floor(((0, constants_1.getResponseTime)(result) - minRT) / rtBucketSize), responseTimeBuckets - 1);
313
+ data[rtBucket][timeBucket]++;
314
+ });
315
+ // Generate labels
316
+ const timeLabels = [];
317
+ for (let i = 0; i < timeBuckets; i++) {
318
+ const time = new Date(minTime + (i * timeBucketSize));
319
+ timeLabels.push(time.toISOString().substr(11, 8)); // HH:MM:SS
320
+ }
321
+ const responseTimeLabels = [];
322
+ for (let i = 0; i < responseTimeBuckets; i++) {
323
+ const rt = minRT + (i * rtBucketSize);
324
+ responseTimeLabels.push(`${Math.round(rt)}ms`);
325
+ }
326
+ const maxValue = Math.max(...data.flat());
327
+ return { data, timeLabels, responseTimeLabels, maxValue };
328
+ }
329
+ /**
330
+ * O(n) time-based grouping using Map-based bucketing
331
+ * FIXED: Replaces O(n²) filter-based implementation
332
+ */
333
+ static groupResultsByTime(results, intervalMs = constants_1.TIME_BUCKETS.MEDIUM) {
334
+ if (results.length === 0)
335
+ return [];
336
+ // Find time range
337
+ let minTime = Infinity;
338
+ let maxTime = -Infinity;
339
+ for (const r of results) {
340
+ if (r.timestamp < minTime)
341
+ minTime = r.timestamp;
342
+ if (r.timestamp > maxTime)
343
+ maxTime = r.timestamp;
344
+ }
345
+ // Single pass: bucket all results
346
+ const buckets = new Map();
347
+ for (const result of results) {
348
+ const bucketKey = Math.floor((result.timestamp - minTime) / intervalMs) * intervalMs + minTime;
349
+ if (!buckets.has(bucketKey)) {
350
+ buckets.set(bucketKey, []);
351
+ }
352
+ buckets.get(bucketKey).push(result);
353
+ }
354
+ // Convert buckets to output format
355
+ const groups = [];
356
+ // Sort bucket keys for chronological order
357
+ const sortedKeys = Array.from(buckets.keys()).sort((a, b) => a - b);
358
+ for (const timestamp of sortedKeys) {
359
+ const intervalResults = buckets.get(timestamp);
360
+ const successfulResults = intervalResults.filter(r => r.success);
361
+ const errorResults = intervalResults.filter(r => !r.success);
362
+ // Calculate average response time using standardized field
363
+ const avgResponseTime = successfulResults.length > 0
364
+ ? successfulResults.reduce((sum, r) => sum + (0, constants_1.getResponseTime)(r), 0) / successfulResults.length
365
+ : 0;
366
+ // Count unique virtual users
367
+ const uniqueVUs = new Set(intervalResults.map(r => r.vu_id)).size;
368
+ groups.push({
369
+ timestamp,
370
+ time_label: new Date(timestamp).toISOString(),
371
+ count: intervalResults.length,
372
+ successful_count: successfulResults.length,
373
+ error_count: errorResults.length,
374
+ errors: errorResults.length,
375
+ success_rate: intervalResults.length > 0
376
+ ? (successfulResults.length / intervalResults.length) * 100
377
+ : 0,
378
+ avg_response_time: Math.round(avgResponseTime * 100) / 100,
379
+ throughput: intervalResults.length / (intervalMs / 1000),
380
+ requests_per_second: intervalResults.length / (intervalMs / 1000),
381
+ concurrent_users: uniqueVUs,
382
+ response_times: successfulResults.map(r => (0, constants_1.getResponseTime)(r)),
383
+ });
384
+ }
385
+ return groups;
386
+ }
387
+ /**
388
+ * Calculate enhanced statistics including all metrics
389
+ */
390
+ static calculateEnhancedStatistics(values) {
391
+ if (values.length === 0) {
392
+ return {
393
+ count: 0,
394
+ min: 0,
395
+ max: 0,
396
+ mean: 0,
397
+ median: 0,
398
+ stdDev: 0,
399
+ percentiles: {},
400
+ confidenceInterval: {
401
+ mean: 0,
402
+ lower: 0,
403
+ upper: 0,
404
+ confidenceLevel: 0.95,
405
+ standardError: 0,
406
+ marginOfError: 0,
407
+ },
408
+ };
409
+ }
410
+ const sorted = [...values].sort((a, b) => a - b);
411
+ const count = values.length;
412
+ const min = sorted[0];
413
+ const max = sorted[count - 1];
414
+ const mean = values.reduce((sum, val) => sum + val, 0) / count;
415
+ const median = count % 2 === 0
416
+ ? (sorted[count / 2 - 1] + sorted[count / 2]) / 2
417
+ : sorted[Math.floor(count / 2)];
418
+ // Calculate standard deviation
419
+ const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / count;
420
+ const stdDev = Math.sqrt(variance);
421
+ // Calculate percentiles using consistent method
422
+ const percentiles = this.calculatePercentiles(values, constants_1.PERCENTILES.EXTENDED);
423
+ // Calculate confidence interval
424
+ const confidenceInterval = this.calculateConfidenceInterval(values);
425
+ return {
426
+ count,
427
+ min: Math.round(min * 100) / 100,
428
+ max: Math.round(max * 100) / 100,
429
+ mean: Math.round(mean * 100) / 100,
430
+ median: Math.round(median * 100) / 100,
431
+ stdDev: Math.round(stdDev * 100) / 100,
432
+ percentiles,
433
+ confidenceInterval,
434
+ };
435
+ }
436
+ static calculateThroughput(results, totalDurationMs) {
437
+ if (totalDurationMs <= 0)
438
+ return 0;
439
+ return results.length / (totalDurationMs / 1000);
440
+ }
441
+ static calculateErrorRate(results) {
442
+ if (results.length === 0)
443
+ return 0;
444
+ const errors = results.filter(r => !r.success).length;
445
+ return (errors / results.length) * 100;
446
+ }
447
+ /**
448
+ * Calculate aggregated Core Web Vitals statistics
449
+ */
450
+ static calculateWebVitalsStatistics(results) {
451
+ const webVitalsResults = results.filter(r => r.custom_metrics?.web_vitals);
452
+ if (webVitalsResults.length === 0) {
453
+ return {};
454
+ }
455
+ const allVitals = {
456
+ lcp: [],
457
+ fid: [],
458
+ cls: [],
459
+ fcp: [],
460
+ ttfb: [],
461
+ tti: [],
462
+ tbt: [],
463
+ speedIndex: [],
464
+ inp: [],
465
+ };
466
+ webVitalsResults.forEach(result => {
467
+ const vitals = result.custom_metrics.web_vitals;
468
+ Object.keys(allVitals).forEach(key => {
469
+ if (vitals[key] !== undefined) {
470
+ allVitals[key].push(vitals[key]);
471
+ }
472
+ });
473
+ });
474
+ const avgVitals = {};
475
+ const vitalsDetails = {};
476
+ Object.entries(allVitals).forEach(([metric, values]) => {
477
+ if (values.length > 0) {
478
+ const avg = values.reduce((sum, val) => sum + val, 0) / values.length;
479
+ avgVitals[metric] = Math.round(avg * 100) / 100;
480
+ const thresholdKey = metric.toUpperCase().replace('SPEEDINDEX', 'SPEED_INDEX');
481
+ const thresholds = constants_1.WEB_VITALS_THRESHOLDS[thresholdKey];
482
+ let score = 'good';
483
+ if (thresholds) {
484
+ if (avg <= thresholds.good) {
485
+ score = 'good';
486
+ }
487
+ else if (avg <= thresholds.poor) {
488
+ score = 'needs-improvement';
489
+ }
490
+ else {
491
+ score = 'poor';
492
+ }
493
+ }
494
+ vitalsDetails[metric] = {
495
+ value: avgVitals[metric],
496
+ score,
497
+ p50: this.calculatePercentiles(values, [50])[50],
498
+ p95: this.calculatePercentiles(values, [95])[95],
499
+ };
500
+ }
501
+ });
502
+ const scores = Object.values(vitalsDetails).map((d) => d.score);
503
+ const goodCount = scores.filter(s => s === 'good').length;
504
+ const poorCount = scores.filter(s => s === 'poor').length;
505
+ const totalCount = scores.length;
506
+ let overallScore = 'needs-improvement';
507
+ if (totalCount === 0) {
508
+ overallScore = 'needs-improvement';
509
+ }
510
+ else if (goodCount >= totalCount * 0.75) {
511
+ overallScore = 'good';
512
+ }
513
+ else if (poorCount > totalCount * 0.25) {
514
+ overallScore = 'poor';
515
+ }
516
+ return {
517
+ web_vitals_data: avgVitals,
518
+ vitals_score: overallScore,
519
+ vitals_details: vitalsDetails,
520
+ };
521
+ }
522
+ /**
523
+ * Enhanced response time distribution with adaptive bucketing
524
+ * Only includes measurable results (verifications, not actions like click/fill)
525
+ */
526
+ static calculateResponseTimeDistribution(results, targetBuckets = 10) {
527
+ // Filter to only measurable results, then filter to successful ones
528
+ const measurableResults = results.filter(isMeasurableResult);
529
+ const successfulResults = measurableResults.filter(r => r.success);
530
+ if (successfulResults.length === 0)
531
+ return [];
532
+ const responseTimes = successfulResults.map(r => (0, constants_1.getResponseTime)(r));
533
+ const min = Math.min(...responseTimes);
534
+ const max = Math.max(...responseTimes);
535
+ const range = max - min;
536
+ if (range < 1) {
537
+ return [{
538
+ bucket: `${Math.round(min)}ms`,
539
+ bucket_start: min,
540
+ bucket_end: max,
541
+ count: responseTimes.length,
542
+ percentage: 100,
543
+ }];
544
+ }
545
+ // Calculate ideal bucket size based on range
546
+ let bucketSize = range / targetBuckets;
547
+ // Round to nice numbers
548
+ if (bucketSize < 1)
549
+ bucketSize = 1;
550
+ else if (bucketSize < 2)
551
+ bucketSize = 2;
552
+ else if (bucketSize < 5)
553
+ bucketSize = 5;
554
+ else if (bucketSize < 10)
555
+ bucketSize = 10;
556
+ else if (bucketSize < 25)
557
+ bucketSize = 25;
558
+ else if (bucketSize < 50)
559
+ bucketSize = 50;
560
+ else if (bucketSize < 100)
561
+ bucketSize = 100;
562
+ else if (bucketSize < 250)
563
+ bucketSize = 250;
564
+ else if (bucketSize < 500)
565
+ bucketSize = 500;
566
+ else if (bucketSize < 1000)
567
+ bucketSize = 1000;
568
+ else
569
+ bucketSize = Math.ceil(bucketSize / 1000) * 1000;
570
+ const bucketStart = Math.floor(min / bucketSize) * bucketSize;
571
+ const bucketEnd = Math.ceil(max / bucketSize) * bucketSize;
572
+ const numBuckets = Math.max(1, Math.round((bucketEnd - bucketStart) / bucketSize));
573
+ const distribution = [];
574
+ for (let i = 0; i < numBuckets; i++) {
575
+ const start = bucketStart + (i * bucketSize);
576
+ const end = start + bucketSize;
577
+ const count = responseTimes.filter(time => time >= start && (i === numBuckets - 1 ? time <= end : time < end)).length;
578
+ distribution.push({
579
+ bucket: `${Math.round(start)}-${Math.round(end)}ms`,
580
+ bucket_start: start,
581
+ bucket_end: end,
582
+ count,
583
+ percentage: Math.round((count / responseTimes.length) * 10000) / 100,
584
+ });
585
+ }
586
+ return distribution;
587
+ }
588
+ /**
589
+ * Calculate detailed step statistics (single source of truth)
590
+ * Only includes measurable results (verifications, waits, measurements)
591
+ * Excludes actions like click, fill, press, goto, select
592
+ */
593
+ static calculateDetailedStepStatistics(results) {
594
+ // Filter to only include measurable results
595
+ const measurableResults = results.filter(isMeasurableResult);
596
+ // O(n) grouping using Map
597
+ const stepGroups = new Map();
598
+ for (const result of measurableResults) {
599
+ const key = `${result.scenario}_${result.step_name || 'default'}`;
600
+ if (!stepGroups.has(key)) {
601
+ stepGroups.set(key, []);
602
+ }
603
+ stepGroups.get(key).push(result);
604
+ }
605
+ return Array.from(stepGroups.entries()).map(([key, stepResults]) => {
606
+ const parts = key.split('_');
607
+ const scenario = parts[0];
608
+ const stepName = parts.slice(1).join('_') || 'default';
609
+ const successfulResults = stepResults.filter(r => r.success);
610
+ const responseTimes = successfulResults.map(r => (0, constants_1.getResponseTime)(r));
611
+ const stats = this.calculateEnhancedStatistics(responseTimes);
612
+ // Error type distribution
613
+ const errorTypes = {};
614
+ stepResults.filter(r => !r.success).forEach(r => {
615
+ const errorType = r.error || 'Unknown error';
616
+ errorTypes[errorType] = (errorTypes[errorType] || 0) + 1;
617
+ });
618
+ return {
619
+ step_name: stepName,
620
+ scenario,
621
+ total_requests: stepResults.length,
622
+ successful_requests: successfulResults.length,
623
+ failed_requests: stepResults.length - successfulResults.length,
624
+ success_rate: stepResults.length > 0
625
+ ? Math.round((successfulResults.length / stepResults.length) * 10000) / 100
626
+ : 0,
627
+ min_response_time: stats.min,
628
+ max_response_time: stats.max,
629
+ avg_response_time: stats.mean,
630
+ median_response_time: stats.median,
631
+ std_dev_response_time: stats.stdDev,
632
+ percentiles: stats.percentiles,
633
+ confidence_interval: stats.confidenceInterval,
634
+ response_times: responseTimes,
635
+ error_types: errorTypes,
636
+ };
637
+ }).sort((a, b) => a.step_name.localeCompare(b.step_name));
638
+ }
639
+ /**
640
+ * Calculate error distribution and patterns
641
+ */
642
+ static calculateErrorDistribution(results) {
643
+ const errorResults = results.filter(r => !r.success);
644
+ const totalErrors = errorResults.length;
645
+ if (totalErrors === 0) {
646
+ return {
647
+ total_errors: 0,
648
+ error_rate: 0,
649
+ error_types: [],
650
+ errors_over_time: [],
651
+ };
652
+ }
653
+ // Group errors by type using Map for O(n)
654
+ const errorCounts = new Map();
655
+ for (const result of errorResults) {
656
+ const errorType = result.error || 'Unknown error';
657
+ errorCounts.set(errorType, (errorCounts.get(errorType) || 0) + 1);
658
+ }
659
+ const errorTypesWithPercentage = Array.from(errorCounts.entries()).map(([type, count]) => ({
660
+ type,
661
+ count,
662
+ percentage: Math.round((count / totalErrors) * 10000) / 100,
663
+ }));
664
+ // Errors over time
665
+ const errorsOverTime = this.groupResultsByTime(errorResults, constants_1.TIME_BUCKETS.MEDIUM).map(group => ({
666
+ timestamp: new Date(group.timestamp).toISOString(),
667
+ error_count: group.count,
668
+ error_rate: group.count / (constants_1.TIME_BUCKETS.MEDIUM / 1000),
669
+ }));
670
+ return {
671
+ total_errors: totalErrors,
672
+ error_rate: Math.round((totalErrors / results.length) * 10000) / 100,
673
+ error_types: errorTypesWithPercentage,
674
+ errors_over_time: errorsOverTime,
675
+ };
676
+ }
677
+ /**
678
+ * Calculate performance trends using linear regression
679
+ * Only analyzes measurable results (verifications, not actions like click/fill)
680
+ */
681
+ static calculatePerformanceTrends(results) {
682
+ // Filter to only measurable results
683
+ const measurableResults = results.filter(isMeasurableResult);
684
+ const timeGroups = this.groupResultsByTime(measurableResults, constants_1.TIME_BUCKETS.COARSE);
685
+ if (timeGroups.length < 2) {
686
+ return {
687
+ trend: 'insufficient_data',
688
+ response_time_trend: 0,
689
+ throughput_trend: 0,
690
+ success_rate_trend: 0,
691
+ };
692
+ }
693
+ const calculateTrend = (values) => {
694
+ if (values.length < 2)
695
+ return 0;
696
+ const n = values.length;
697
+ const sumX = values.reduce((sum, _, i) => sum + i, 0);
698
+ const sumY = values.reduce((sum, val) => sum + val, 0);
699
+ const sumXY = values.reduce((sum, val, i) => sum + (i * val), 0);
700
+ const sumXX = values.reduce((sum, _, i) => sum + (i * i), 0);
701
+ const denominator = n * sumXX - sumX * sumX;
702
+ if (denominator === 0)
703
+ return 0;
704
+ return (n * sumXY - sumX * sumY) / denominator;
705
+ };
706
+ const responseTimes = timeGroups.map(g => g.avg_response_time);
707
+ const throughputs = timeGroups.map(g => g.throughput);
708
+ const successRates = timeGroups.map(g => g.success_rate);
709
+ const rtTrend = calculateTrend(responseTimes);
710
+ const tpTrend = calculateTrend(throughputs);
711
+ const srTrend = calculateTrend(successRates);
712
+ // Determine overall trend direction
713
+ let trend;
714
+ if (rtTrend > 0.1)
715
+ trend = 'degrading';
716
+ else if (rtTrend < -0.1)
717
+ trend = 'improving';
718
+ else
719
+ trend = 'stable';
720
+ return {
721
+ trend,
722
+ response_time_trend: Math.round(rtTrend * 1000) / 1000,
723
+ throughput_trend: Math.round(tpTrend * 1000) / 1000,
724
+ success_rate_trend: Math.round(srTrend * 1000) / 1000,
725
+ data_points: timeGroups.length,
726
+ analysis_period_ms: timeGroups[timeGroups.length - 1].timestamp - timeGroups[0].timestamp,
727
+ };
728
+ }
729
+ /**
730
+ * Get comprehensive analysis of test results
731
+ */
732
+ static getComprehensiveAnalysis(results, slaConfig) {
733
+ return {
734
+ apdex: this.calculateApdexScore(results),
735
+ sla: this.checkSLACompliance(results, slaConfig),
736
+ outliers: this.detectOutliers(results),
737
+ trends: this.calculatePerformanceTrends(results),
738
+ heatmap: this.generateHeatmapData(results),
739
+ };
740
+ }
741
+ }
742
+ exports.StatisticsCalculator = StatisticsCalculator;