@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.
- package/README.md +360 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +192 -0
- package/dist/cli/commands/distributed.d.ts +11 -0
- package/dist/cli/commands/distributed.js +179 -0
- package/dist/cli/commands/import.d.ts +23 -0
- package/dist/cli/commands/import.js +461 -0
- package/dist/cli/commands/init.d.ts +7 -0
- package/dist/cli/commands/init.js +923 -0
- package/dist/cli/commands/mock.d.ts +7 -0
- package/dist/cli/commands/mock.js +281 -0
- package/dist/cli/commands/report.d.ts +5 -0
- package/dist/cli/commands/report.js +70 -0
- package/dist/cli/commands/run.d.ts +12 -0
- package/dist/cli/commands/run.js +260 -0
- package/dist/cli/commands/validate.d.ts +3 -0
- package/dist/cli/commands/validate.js +35 -0
- package/dist/cli/commands/worker.d.ts +27 -0
- package/dist/cli/commands/worker.js +320 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +20 -0
- package/dist/config/parser.d.ts +19 -0
- package/dist/config/parser.js +330 -0
- package/dist/config/types/global-config.d.ts +74 -0
- package/dist/config/types/global-config.js +2 -0
- package/dist/config/types/hooks.d.ts +58 -0
- package/dist/config/types/hooks.js +3 -0
- package/dist/config/types/import-types.d.ts +33 -0
- package/dist/config/types/import-types.js +2 -0
- package/dist/config/types/index.d.ts +11 -0
- package/dist/config/types/index.js +27 -0
- package/dist/config/types/load-config.d.ts +32 -0
- package/dist/config/types/load-config.js +9 -0
- package/dist/config/types/output-config.d.ts +10 -0
- package/dist/config/types/output-config.js +2 -0
- package/dist/config/types/report-config.d.ts +10 -0
- package/dist/config/types/report-config.js +2 -0
- package/dist/config/types/runtime-types.d.ts +6 -0
- package/dist/config/types/runtime-types.js +2 -0
- package/dist/config/types/scenario-config.d.ts +30 -0
- package/dist/config/types/scenario-config.js +2 -0
- package/dist/config/types/step-types.d.ts +139 -0
- package/dist/config/types/step-types.js +2 -0
- package/dist/config/types/test-configuration.d.ts +18 -0
- package/dist/config/types/test-configuration.js +2 -0
- package/dist/config/types/worker-config.d.ts +12 -0
- package/dist/config/types/worker-config.js +2 -0
- package/dist/config/validator.d.ts +19 -0
- package/dist/config/validator.js +198 -0
- package/dist/core/csv-data-provider.d.ts +47 -0
- package/dist/core/csv-data-provider.js +265 -0
- package/dist/core/hooks-manager.d.ts +33 -0
- package/dist/core/hooks-manager.js +129 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +11 -0
- package/dist/core/script-executor.d.ts +14 -0
- package/dist/core/script-executor.js +290 -0
- package/dist/core/step-executor.d.ts +41 -0
- package/dist/core/step-executor.js +680 -0
- package/dist/core/test-runner.d.ts +34 -0
- package/dist/core/test-runner.js +465 -0
- package/dist/core/threshold-evaluator.d.ts +43 -0
- package/dist/core/threshold-evaluator.js +170 -0
- package/dist/core/virtual-user-pool.d.ts +42 -0
- package/dist/core/virtual-user-pool.js +136 -0
- package/dist/core/virtual-user.d.ts +51 -0
- package/dist/core/virtual-user.js +488 -0
- package/dist/distributed/coordinator.d.ts +34 -0
- package/dist/distributed/coordinator.js +158 -0
- package/dist/distributed/health-monitor.d.ts +18 -0
- package/dist/distributed/health-monitor.js +72 -0
- package/dist/distributed/load-distributor.d.ts +17 -0
- package/dist/distributed/load-distributor.js +106 -0
- package/dist/distributed/remote-worker.d.ts +37 -0
- package/dist/distributed/remote-worker.js +241 -0
- package/dist/distributed/result-aggregator.d.ts +43 -0
- package/dist/distributed/result-aggregator.js +146 -0
- package/dist/dsl/index.d.ts +3 -0
- package/dist/dsl/index.js +11 -0
- package/dist/dsl/test-builder.d.ts +111 -0
- package/dist/dsl/test-builder.js +514 -0
- package/dist/importers/har-importer.d.ts +17 -0
- package/dist/importers/har-importer.js +172 -0
- package/dist/importers/open-api-importer.d.ts +23 -0
- package/dist/importers/open-api-importer.js +181 -0
- package/dist/importers/wsdl-importer.d.ts +42 -0
- package/dist/importers/wsdl-importer.js +440 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +17 -0
- package/dist/load-patterns/arrivals.d.ts +7 -0
- package/dist/load-patterns/arrivals.js +118 -0
- package/dist/load-patterns/base.d.ts +9 -0
- package/dist/load-patterns/base.js +2 -0
- package/dist/load-patterns/basic.d.ts +7 -0
- package/dist/load-patterns/basic.js +117 -0
- package/dist/load-patterns/stepping.d.ts +6 -0
- package/dist/load-patterns/stepping.js +122 -0
- package/dist/metrics/collector.d.ts +72 -0
- package/dist/metrics/collector.js +662 -0
- package/dist/metrics/types.d.ts +135 -0
- package/dist/metrics/types.js +2 -0
- package/dist/outputs/base.d.ts +7 -0
- package/dist/outputs/base.js +2 -0
- package/dist/outputs/csv.d.ts +13 -0
- package/dist/outputs/csv.js +163 -0
- package/dist/outputs/graphite.d.ts +13 -0
- package/dist/outputs/graphite.js +126 -0
- package/dist/outputs/influxdb.d.ts +12 -0
- package/dist/outputs/influxdb.js +82 -0
- package/dist/outputs/json.d.ts +14 -0
- package/dist/outputs/json.js +107 -0
- package/dist/outputs/streaming-csv.d.ts +37 -0
- package/dist/outputs/streaming-csv.js +254 -0
- package/dist/outputs/streaming-json.d.ts +43 -0
- package/dist/outputs/streaming-json.js +353 -0
- package/dist/outputs/webhook.d.ts +16 -0
- package/dist/outputs/webhook.js +96 -0
- package/dist/protocols/base.d.ts +33 -0
- package/dist/protocols/base.js +2 -0
- package/dist/protocols/rest/handler.d.ts +67 -0
- package/dist/protocols/rest/handler.js +776 -0
- package/dist/protocols/soap/handler.d.ts +12 -0
- package/dist/protocols/soap/handler.js +165 -0
- package/dist/protocols/web/core-web-vitals.d.ts +121 -0
- package/dist/protocols/web/core-web-vitals.js +373 -0
- package/dist/protocols/web/handler.d.ts +50 -0
- package/dist/protocols/web/handler.js +706 -0
- package/dist/recorder/native-recorder.d.ts +14 -0
- package/dist/recorder/native-recorder.js +533 -0
- package/dist/recorder/scenario-recorder.d.ts +55 -0
- package/dist/recorder/scenario-recorder.js +296 -0
- package/dist/reporting/constants.d.ts +94 -0
- package/dist/reporting/constants.js +82 -0
- package/dist/reporting/enhanced-html-generator.d.ts +55 -0
- package/dist/reporting/enhanced-html-generator.js +965 -0
- package/dist/reporting/generator.d.ts +42 -0
- package/dist/reporting/generator.js +1217 -0
- package/dist/reporting/statistics.d.ts +144 -0
- package/dist/reporting/statistics.js +742 -0
- package/dist/reporting/templates/enhanced-report.hbs +2812 -0
- package/dist/reporting/templates/html.hbs +2453 -0
- package/dist/utils/faker-manager.d.ts +55 -0
- package/dist/utils/faker-manager.js +166 -0
- package/dist/utils/file-manager.d.ts +33 -0
- package/dist/utils/file-manager.js +154 -0
- package/dist/utils/handlebars-manager.d.ts +42 -0
- package/dist/utils/handlebars-manager.js +172 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +46 -0
- package/dist/utils/template.d.ts +80 -0
- package/dist/utils/template.js +513 -0
- package/dist/utils/test-output-writer.d.ts +56 -0
- package/dist/utils/test-output-writer.js +643 -0
- package/dist/utils/time.d.ts +3 -0
- package/dist/utils/time.js +23 -0
- package/dist/utils/timestamp-helper.d.ts +17 -0
- package/dist/utils/timestamp-helper.js +53 -0
- package/dist/workers/manager.d.ts +18 -0
- package/dist/workers/manager.js +95 -0
- package/dist/workers/server.d.ts +21 -0
- package/dist/workers/server.js +205 -0
- package/dist/workers/worker.d.ts +19 -0
- package/dist/workers/worker.js +147 -0
- 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;
|