@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,1217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.HTMLReportGenerator = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const statistics_1 = require("./statistics");
|
|
40
|
+
const logger_1 = require("../utils/logger");
|
|
41
|
+
// Import handlebars correctly
|
|
42
|
+
const Handlebars = __importStar(require("handlebars"));
|
|
43
|
+
class HTMLReportGenerator {
|
|
44
|
+
constructor() {
|
|
45
|
+
// Register Handlebars helpers
|
|
46
|
+
this.registerHelpers();
|
|
47
|
+
}
|
|
48
|
+
registerHelpers() {
|
|
49
|
+
// Helper for comparing numbers
|
|
50
|
+
Handlebars.registerHelper('gt', function (a, b) {
|
|
51
|
+
return a > b;
|
|
52
|
+
});
|
|
53
|
+
// Helper for formatting numbers
|
|
54
|
+
Handlebars.registerHelper('toFixed', function (num, digits) {
|
|
55
|
+
return typeof num === 'number' ? num.toFixed(digits) : '0';
|
|
56
|
+
});
|
|
57
|
+
// Helper for conditional classes
|
|
58
|
+
Handlebars.registerHelper('statusClass', function (successRate) {
|
|
59
|
+
if (successRate >= 95)
|
|
60
|
+
return 'metric-success';
|
|
61
|
+
if (successRate >= 90)
|
|
62
|
+
return 'metric-warning';
|
|
63
|
+
return 'metric-error';
|
|
64
|
+
});
|
|
65
|
+
// Helper for accessing numeric properties
|
|
66
|
+
Handlebars.registerHelper('percentile', function (percentiles, key) {
|
|
67
|
+
return percentiles[key] || 0;
|
|
68
|
+
});
|
|
69
|
+
// Helper for lookup with numeric keys
|
|
70
|
+
Handlebars.registerHelper('lookup', function (obj, key) {
|
|
71
|
+
return obj[key];
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async generate(data, config, outputPath) {
|
|
75
|
+
try {
|
|
76
|
+
const templatePath = config.template || this.getDefaultTemplatePath();
|
|
77
|
+
let template;
|
|
78
|
+
if (fs.existsSync(templatePath)) {
|
|
79
|
+
template = fs.readFileSync(templatePath, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// Use built-in template
|
|
83
|
+
template = this.getBuiltInTemplate();
|
|
84
|
+
}
|
|
85
|
+
const compiledTemplate = Handlebars.compile(template);
|
|
86
|
+
const chartData = this.prepareChartData(data.results);
|
|
87
|
+
const percentiles = config.percentiles || [50, 90, 95, 99, 99.9, 99.99];
|
|
88
|
+
const responseTimes = data.results
|
|
89
|
+
.filter(r => r.success)
|
|
90
|
+
.map(r => r.duration);
|
|
91
|
+
const percentileData = statistics_1.StatisticsCalculator.calculatePercentiles(responseTimes, percentiles);
|
|
92
|
+
// Convert percentiles to array format
|
|
93
|
+
const percentileArray = percentiles.map(p => ({
|
|
94
|
+
percentile: p,
|
|
95
|
+
value: percentileData[p] || 0
|
|
96
|
+
}));
|
|
97
|
+
const timeSeriesData = statistics_1.StatisticsCalculator.groupResultsByTime(data.results, 5000);
|
|
98
|
+
const scenarioStats = this.calculateScenarioStatistics(data.results);
|
|
99
|
+
// Enhanced chart data with new features
|
|
100
|
+
const stepStatistics = this.calculateStepStatistics(data.results);
|
|
101
|
+
const vuRampupData = this.calculateVURampupData(data.results, data.summary.vu_ramp_up || []);
|
|
102
|
+
const timelineData = this.calculateTimelineData(data.results);
|
|
103
|
+
const stepResponseTimes = this.calculateStepResponseTimes(data.results);
|
|
104
|
+
// NEW: Additional chart data
|
|
105
|
+
const responseTimeDistribution = statistics_1.StatisticsCalculator.calculateResponseTimeDistribution(data.results, 15);
|
|
106
|
+
const requestsPerSecondData = this.calculateRequestsPerSecondData(data.results);
|
|
107
|
+
const responsesPerSecondData = this.calculateResponsesPerSecondData(data.results);
|
|
108
|
+
// FIXED: Calculate peak VUs from timeline data
|
|
109
|
+
const peakVirtualUsers = timelineData.length > 0
|
|
110
|
+
? Math.max(...timelineData.map(d => d.active_vus))
|
|
111
|
+
: 0;
|
|
112
|
+
// Update summary with peak VUs
|
|
113
|
+
const enhancedSummary = {
|
|
114
|
+
...data.summary,
|
|
115
|
+
peak_virtual_users: peakVirtualUsers,
|
|
116
|
+
total_virtual_users: peakVirtualUsers
|
|
117
|
+
};
|
|
118
|
+
const reportContext = {
|
|
119
|
+
testName: data.testName,
|
|
120
|
+
generatedAt: new Date().toISOString(),
|
|
121
|
+
summary: enhancedSummary, // Use enhanced summary
|
|
122
|
+
percentiles: percentileData,
|
|
123
|
+
percentilesArray: percentileArray,
|
|
124
|
+
chartData: JSON.stringify(chartData),
|
|
125
|
+
timeSeriesData: JSON.stringify(timeSeriesData),
|
|
126
|
+
errorDistribution: JSON.stringify(data.summary.error_distribution),
|
|
127
|
+
scenarioStats: scenarioStats,
|
|
128
|
+
includeCharts: config.include_charts !== false,
|
|
129
|
+
includeRawData: config.include_raw_data === true,
|
|
130
|
+
rawData: config.include_raw_data ? JSON.stringify(data.results.slice(0, 1000)) : null,
|
|
131
|
+
// Enhanced chart data
|
|
132
|
+
summaryData: JSON.stringify(enhancedSummary),
|
|
133
|
+
stepStatisticsData: JSON.stringify(stepStatistics),
|
|
134
|
+
stepStatistics: stepStatistics,
|
|
135
|
+
vuRampupData: JSON.stringify(vuRampupData),
|
|
136
|
+
timelineData: JSON.stringify(timelineData),
|
|
137
|
+
stepResponseTimesData: JSON.stringify(stepResponseTimes),
|
|
138
|
+
stepResponseTimes: stepResponseTimes,
|
|
139
|
+
// NEW: Additional chart data
|
|
140
|
+
responseTimeDistributionData: JSON.stringify(responseTimeDistribution),
|
|
141
|
+
requestsPerSecondData: JSON.stringify(requestsPerSecondData),
|
|
142
|
+
responsesPerSecondData: JSON.stringify(responsesPerSecondData)
|
|
143
|
+
};
|
|
144
|
+
const html = compiledTemplate(reportContext);
|
|
145
|
+
// Ensure output directory exists
|
|
146
|
+
const dir = path.dirname(outputPath);
|
|
147
|
+
if (!fs.existsSync(dir)) {
|
|
148
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
149
|
+
}
|
|
150
|
+
fs.writeFileSync(outputPath, html);
|
|
151
|
+
logger_1.logger.debug(`📋 HTML report written to ${outputPath}`);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
logger_1.logger.error('❌ Report generation failed:', error);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Enhanced step statistics with min, max, and additional percentiles
|
|
160
|
+
* Only includes measurable results (verifications, not actions like click/fill)
|
|
161
|
+
*/
|
|
162
|
+
calculateStepStatistics(results) {
|
|
163
|
+
// Filter to only include measurable results (verifications, waits, measurements)
|
|
164
|
+
const measurableResults = results.filter(statistics_1.isMeasurableResult);
|
|
165
|
+
const stepGroups = {};
|
|
166
|
+
// Group results by step name and scenario
|
|
167
|
+
measurableResults.forEach(result => {
|
|
168
|
+
const key = `${result.scenario}-${result.step_name || 'default'}`;
|
|
169
|
+
if (!stepGroups[key]) {
|
|
170
|
+
stepGroups[key] = [];
|
|
171
|
+
}
|
|
172
|
+
stepGroups[key].push(result);
|
|
173
|
+
});
|
|
174
|
+
return Object.entries(stepGroups).map(([key, stepResults]) => {
|
|
175
|
+
const [scenario, stepName] = key.split('-');
|
|
176
|
+
const successfulResults = stepResults.filter(r => r.success);
|
|
177
|
+
const responseTimes = successfulResults.map(r => r.duration);
|
|
178
|
+
// Calculate extended percentiles including 99.9% and 99.99%
|
|
179
|
+
const percentiles = statistics_1.StatisticsCalculator.calculatePercentiles(responseTimes, [50, 90, 95, 99, 99.9, 99.99]);
|
|
180
|
+
const minResponseTime = responseTimes.length > 0 ? Math.min(...responseTimes) : 0;
|
|
181
|
+
const maxResponseTime = responseTimes.length > 0 ? Math.max(...responseTimes) : 0;
|
|
182
|
+
return {
|
|
183
|
+
step_name: stepName,
|
|
184
|
+
scenario: scenario,
|
|
185
|
+
total_requests: stepResults.length,
|
|
186
|
+
success_rate: stepResults.length > 0 ? (successfulResults.length / stepResults.length) * 100 : 0,
|
|
187
|
+
avg_response_time: responseTimes.length > 0 ?
|
|
188
|
+
responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length : 0,
|
|
189
|
+
min_response_time: minResponseTime,
|
|
190
|
+
max_response_time: maxResponseTime,
|
|
191
|
+
percentiles: percentiles,
|
|
192
|
+
response_times: responseTimes
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Calculate requests per second over time
|
|
198
|
+
*/
|
|
199
|
+
calculateRequestsPerSecondData(results) {
|
|
200
|
+
const timeGroups = statistics_1.StatisticsCalculator.groupResultsByTime(results, 1000); // 1-second intervals
|
|
201
|
+
return timeGroups.map(group => ({
|
|
202
|
+
timestamp: new Date(group.timestamp).toISOString(),
|
|
203
|
+
requests_per_second: group.count, // Total requests in this second
|
|
204
|
+
successful_requests_per_second: group.count - group.errors
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Calculate responses per second over time (successful responses)
|
|
209
|
+
*/
|
|
210
|
+
calculateResponsesPerSecondData(results) {
|
|
211
|
+
const timeGroups = statistics_1.StatisticsCalculator.groupResultsByTime(results, 1000); // 1-second intervals
|
|
212
|
+
return timeGroups.map(group => ({
|
|
213
|
+
timestamp: new Date(group.timestamp).toISOString(),
|
|
214
|
+
responses_per_second: group.count - group.errors, // Successful responses
|
|
215
|
+
total_responses_per_second: group.count, // All responses (including errors)
|
|
216
|
+
error_responses_per_second: group.errors
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Generate proper filename with timestamp and test name from config
|
|
221
|
+
*/
|
|
222
|
+
generateFilename(testName, type, configPath) {
|
|
223
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('.')[0]; // Format: YYYY-MM-DDTHH-MM-SS
|
|
224
|
+
// Extract test name from YAML config if available
|
|
225
|
+
let finalTestName = testName;
|
|
226
|
+
if (configPath) {
|
|
227
|
+
const configTestName = this.extractTestNameFromConfig(configPath);
|
|
228
|
+
if (configTestName) {
|
|
229
|
+
finalTestName = configTestName;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const sanitizedTestName = finalTestName.replace(/[^a-zA-Z0-9-_]/g, '_').toLowerCase();
|
|
233
|
+
switch (type) {
|
|
234
|
+
case 'html':
|
|
235
|
+
return `${sanitizedTestName}-${timestamp}-report.html`;
|
|
236
|
+
case 'csv':
|
|
237
|
+
return `${sanitizedTestName}-${timestamp}-results.csv`;
|
|
238
|
+
case 'json':
|
|
239
|
+
return `${sanitizedTestName}-${timestamp}-results.json`;
|
|
240
|
+
case 'summary':
|
|
241
|
+
return `${sanitizedTestName}-${timestamp}-summary.csv`;
|
|
242
|
+
default:
|
|
243
|
+
return `${sanitizedTestName}-${timestamp}.${type}`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Extract test name from YAML config file
|
|
248
|
+
*/
|
|
249
|
+
extractTestNameFromConfig(configPath) {
|
|
250
|
+
try {
|
|
251
|
+
if (!fs.existsSync(configPath)) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
255
|
+
// Simple YAML parsing for test name
|
|
256
|
+
const nameMatch = configContent.match(/^name:\s*["']?([^"'\n]+)["']?/m);
|
|
257
|
+
if (nameMatch) {
|
|
258
|
+
return nameMatch[1].trim();
|
|
259
|
+
}
|
|
260
|
+
// Fallback: look for title
|
|
261
|
+
const titleMatch = configContent.match(/^title:\s*["']?([^"'\n]+)["']?/m);
|
|
262
|
+
if (titleMatch) {
|
|
263
|
+
return titleMatch[1].trim();
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
logger_1.logger.debug(`Could not extract test name from config ${configPath}:`, error);
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
calculateVURampupData(results, vuStartEvents = []) {
|
|
273
|
+
// ONLY use real VU tracking data - no estimation
|
|
274
|
+
const vuRampupData = [];
|
|
275
|
+
// Only generate data if we have real VU start events
|
|
276
|
+
if (vuStartEvents.length === 0) {
|
|
277
|
+
return vuRampupData; // Return empty - no VU tracking available
|
|
278
|
+
}
|
|
279
|
+
const totalVUs = vuStartEvents.length;
|
|
280
|
+
const testStartTime = Math.min(...vuStartEvents.map(vu => vu.start_time));
|
|
281
|
+
const testEndTime = results.length > 0
|
|
282
|
+
? Math.max(...results.map(r => r.timestamp))
|
|
283
|
+
: testStartTime + 60000;
|
|
284
|
+
// Create time buckets for the entire test duration (1 second intervals)
|
|
285
|
+
const timeInterval = 1000;
|
|
286
|
+
const sortedVUEvents = [...vuStartEvents].sort((a, b) => a.start_time - b.start_time);
|
|
287
|
+
for (let t = testStartTime; t <= testEndTime; t += timeInterval) {
|
|
288
|
+
// Count VUs that have started by this time (real data)
|
|
289
|
+
const activeVUs = sortedVUEvents.filter(vu => vu.start_time <= t).length;
|
|
290
|
+
vuRampupData.push({
|
|
291
|
+
time: (t - testStartTime) / 1000,
|
|
292
|
+
timestamp: t,
|
|
293
|
+
count: Math.min(activeVUs, totalVUs)
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return vuRampupData;
|
|
297
|
+
}
|
|
298
|
+
calculateTimelineData(results) {
|
|
299
|
+
const timeGroups = statistics_1.StatisticsCalculator.groupResultsByTime(results, 5000); // 5 second intervals
|
|
300
|
+
return timeGroups.map(group => {
|
|
301
|
+
const groupResults = results.filter(r => Math.abs(new Date(r.timestamp).getTime() - new Date(group.timestamp).getTime()) < 5000);
|
|
302
|
+
const successfulResults = groupResults.filter(r => r.success);
|
|
303
|
+
const avgResponseTime = successfulResults.length > 0 ?
|
|
304
|
+
successfulResults.reduce((sum, r) => sum + r.duration, 0) / successfulResults.length : 0;
|
|
305
|
+
// Use only real concurrent_users from tracked data, default to 0 if not available
|
|
306
|
+
const activeVUs = group.concurrent_users || 0;
|
|
307
|
+
return {
|
|
308
|
+
timestamp: group.timestamp,
|
|
309
|
+
active_vus: activeVUs,
|
|
310
|
+
avg_response_time: avgResponseTime,
|
|
311
|
+
success_rate: groupResults.length > 0 ? (successfulResults.length / groupResults.length) * 100 : 0,
|
|
312
|
+
throughput: group.requests_per_second || (groupResults.length / 5) // requests per second
|
|
313
|
+
};
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
// Removed estimateActiveVUs - only use real tracked VU data
|
|
317
|
+
prepareChartData(results) {
|
|
318
|
+
const responseTimes = results
|
|
319
|
+
.filter(r => r.success)
|
|
320
|
+
.map(r => ({
|
|
321
|
+
timestamp: r.timestamp,
|
|
322
|
+
duration: r.duration,
|
|
323
|
+
scenario: r.scenario
|
|
324
|
+
}));
|
|
325
|
+
const errors = results
|
|
326
|
+
.filter(r => !r.success)
|
|
327
|
+
.map(r => ({
|
|
328
|
+
timestamp: r.timestamp,
|
|
329
|
+
error: r.error || 'Unknown error',
|
|
330
|
+
scenario: r.scenario
|
|
331
|
+
}));
|
|
332
|
+
return {
|
|
333
|
+
responseTimes,
|
|
334
|
+
errors,
|
|
335
|
+
scenarios: this.groupByScenario(results)
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
groupByScenario(results) {
|
|
339
|
+
const scenarios = {};
|
|
340
|
+
results.forEach(result => {
|
|
341
|
+
if (!scenarios[result.scenario]) {
|
|
342
|
+
scenarios[result.scenario] = {
|
|
343
|
+
name: result.scenario,
|
|
344
|
+
total: 0,
|
|
345
|
+
success: 0,
|
|
346
|
+
errors: 0,
|
|
347
|
+
avgResponseTime: 0,
|
|
348
|
+
responseTimes: []
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const scenario = scenarios[result.scenario];
|
|
352
|
+
scenario.total++;
|
|
353
|
+
if (result.success) {
|
|
354
|
+
scenario.success++;
|
|
355
|
+
scenario.responseTimes.push(result.duration);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
scenario.errors++;
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
// Calculate averages and success rates
|
|
362
|
+
Object.values(scenarios).forEach((scenario) => {
|
|
363
|
+
if (scenario.responseTimes.length > 0) {
|
|
364
|
+
scenario.avgResponseTime = scenario.responseTimes.reduce((a, b) => a + b, 0) / scenario.responseTimes.length;
|
|
365
|
+
}
|
|
366
|
+
scenario.successRate = scenario.total > 0 ? (scenario.success / scenario.total) * 100 : 0;
|
|
367
|
+
});
|
|
368
|
+
return Object.values(scenarios);
|
|
369
|
+
}
|
|
370
|
+
calculateStepResponseTimes(results) {
|
|
371
|
+
const stepGroups = {};
|
|
372
|
+
// Group ALL response data by step name (including timestamps)
|
|
373
|
+
results.forEach(result => {
|
|
374
|
+
if (result.success && result.step_name) {
|
|
375
|
+
const stepName = result.step_name;
|
|
376
|
+
if (!stepGroups[stepName]) {
|
|
377
|
+
stepGroups[stepName] = [];
|
|
378
|
+
}
|
|
379
|
+
stepGroups[stepName].push({
|
|
380
|
+
duration: result.duration,
|
|
381
|
+
timestamp: result.timestamp,
|
|
382
|
+
vu_id: result.vu_id,
|
|
383
|
+
iteration: result.iteration
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
// Calculate statistics for each step
|
|
388
|
+
return Object.entries(stepGroups).map(([stepName, stepData]) => {
|
|
389
|
+
// Extract just response times for statistics
|
|
390
|
+
const responseTimes = stepData.map(item => item.duration);
|
|
391
|
+
const avg = responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length;
|
|
392
|
+
const min = Math.min(...responseTimes);
|
|
393
|
+
const max = Math.max(...responseTimes);
|
|
394
|
+
// Calculate percentiles
|
|
395
|
+
const percentiles = statistics_1.StatisticsCalculator.calculatePercentiles(responseTimes, [50, 90, 95, 99]);
|
|
396
|
+
// Sort timeline data by timestamp
|
|
397
|
+
const timelineData = stepData.sort((a, b) => a.timestamp - b.timestamp);
|
|
398
|
+
return {
|
|
399
|
+
step_name: stepName,
|
|
400
|
+
count: responseTimes.length,
|
|
401
|
+
avg: Math.round(avg * 100) / 100,
|
|
402
|
+
min: min,
|
|
403
|
+
max: max,
|
|
404
|
+
p50: percentiles[50] || 0,
|
|
405
|
+
p90: percentiles[90] || 0,
|
|
406
|
+
p95: percentiles[95] || 0,
|
|
407
|
+
p99: percentiles[99] || 0,
|
|
408
|
+
response_times: responseTimes, // Raw response times for box plots
|
|
409
|
+
timeline_data: timelineData // Individual data points with timestamps for line charts
|
|
410
|
+
};
|
|
411
|
+
}).sort((a, b) => a.step_name.localeCompare(b.step_name));
|
|
412
|
+
}
|
|
413
|
+
calculateScenarioStatistics(results) {
|
|
414
|
+
const scenarioGroups = this.groupByScenario(results);
|
|
415
|
+
return scenarioGroups.map((scenario) => {
|
|
416
|
+
const percentiles = statistics_1.StatisticsCalculator.calculatePercentiles(scenario.responseTimes, [50, 90, 95, 99]);
|
|
417
|
+
return {
|
|
418
|
+
...scenario,
|
|
419
|
+
percentiles,
|
|
420
|
+
minResponseTime: scenario.responseTimes.length > 0 ? Math.min(...scenario.responseTimes) : 0,
|
|
421
|
+
maxResponseTime: scenario.responseTimes.length > 0 ? Math.max(...scenario.responseTimes) : 0,
|
|
422
|
+
// Add individual percentile values
|
|
423
|
+
p50: percentiles[50] || 0,
|
|
424
|
+
p90: percentiles[90] || 0,
|
|
425
|
+
p95: percentiles[95] || 0,
|
|
426
|
+
p99: percentiles[99] || 0
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
getDefaultTemplatePath() {
|
|
431
|
+
return path.join(__dirname, 'templates', 'html.hbs');
|
|
432
|
+
}
|
|
433
|
+
getBuiltInTemplate() {
|
|
434
|
+
// Use the enhanced template with all new features
|
|
435
|
+
return this.getEnhancedTemplate();
|
|
436
|
+
}
|
|
437
|
+
getEnhancedTemplate() {
|
|
438
|
+
return `<!DOCTYPE html>
|
|
439
|
+
<html lang="en">
|
|
440
|
+
<head>
|
|
441
|
+
<meta charset="UTF-8">
|
|
442
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
443
|
+
<title>{{testName}} - Enhanced Performance Report</title>
|
|
444
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
|
445
|
+
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
|
|
446
|
+
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.1/dist/chartjs-plugin-zoom.min.js"></script>
|
|
447
|
+
<style>
|
|
448
|
+
:root {
|
|
449
|
+
--primary-color: #2563eb;
|
|
450
|
+
--success-color: #10b981;
|
|
451
|
+
--warning-color: #f59e0b;
|
|
452
|
+
--error-color: #ef4444;
|
|
453
|
+
--background-color: #f8fafc;
|
|
454
|
+
--card-background: #ffffff;
|
|
455
|
+
--text-primary: #1f2937;
|
|
456
|
+
--text-secondary: #6b7280;
|
|
457
|
+
--border-color: #e5e7eb;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
* {
|
|
461
|
+
margin: 0;
|
|
462
|
+
padding: 0;
|
|
463
|
+
box-sizing: border-box;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
body {
|
|
467
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
468
|
+
background-color: var(--background-color);
|
|
469
|
+
color: var(--text-primary);
|
|
470
|
+
line-height: 1.6;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.container {
|
|
474
|
+
max-width: 1400px;
|
|
475
|
+
margin: 0 auto;
|
|
476
|
+
padding: 20px;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.header {
|
|
480
|
+
background: linear-gradient(135deg, var(--primary-color) 0%, #1d4ed8 100%);
|
|
481
|
+
color: white;
|
|
482
|
+
padding: 40px 20px;
|
|
483
|
+
border-radius: 12px;
|
|
484
|
+
margin-bottom: 30px;
|
|
485
|
+
text-align: center;
|
|
486
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.header h1 {
|
|
490
|
+
font-size: 2.5rem;
|
|
491
|
+
font-weight: 700;
|
|
492
|
+
margin-bottom: 10px;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.header p {
|
|
496
|
+
font-size: 1.1rem;
|
|
497
|
+
opacity: 0.9;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.summary-grid {
|
|
501
|
+
display: grid;
|
|
502
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
503
|
+
gap: 20px;
|
|
504
|
+
margin-bottom: 40px;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.metric-card {
|
|
508
|
+
background: var(--card-background);
|
|
509
|
+
padding: 24px;
|
|
510
|
+
border-radius: 12px;
|
|
511
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
512
|
+
border: 1px solid var(--border-color);
|
|
513
|
+
text-align: center;
|
|
514
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.metric-card:hover {
|
|
518
|
+
transform: translateY(-2px);
|
|
519
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.metric-value {
|
|
523
|
+
font-size: 2.5rem;
|
|
524
|
+
font-weight: 700;
|
|
525
|
+
margin-bottom: 8px;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.metric-label {
|
|
529
|
+
color: var(--text-secondary);
|
|
530
|
+
font-size: 0.9rem;
|
|
531
|
+
text-transform: uppercase;
|
|
532
|
+
letter-spacing: 0.5px;
|
|
533
|
+
font-weight: 500;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.metric-success {
|
|
537
|
+
color: var(--success-color);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.metric-warning {
|
|
541
|
+
color: var(--warning-color);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.metric-error {
|
|
545
|
+
color: var(--error-color);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.section {
|
|
549
|
+
background: var(--card-background);
|
|
550
|
+
margin-bottom: 30px;
|
|
551
|
+
border-radius: 12px;
|
|
552
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
553
|
+
border: 1px solid var(--border-color);
|
|
554
|
+
overflow: hidden;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.section-header {
|
|
558
|
+
padding: 20px 24px;
|
|
559
|
+
border-bottom: 1px solid var(--border-color);
|
|
560
|
+
background: #f9fafb;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.section-title {
|
|
564
|
+
font-size: 1.5rem;
|
|
565
|
+
font-weight: 600;
|
|
566
|
+
color: var(--text-primary);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.section-content {
|
|
570
|
+
padding: 24px;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.chart-container {
|
|
574
|
+
margin-bottom: 30px;
|
|
575
|
+
height: 400px;
|
|
576
|
+
position: relative;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.chart-container.large {
|
|
580
|
+
height: 500px;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.chart-container.small {
|
|
584
|
+
height: 300px;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.chart-container canvas {
|
|
588
|
+
max-height: 100%;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.step-stats-table {
|
|
592
|
+
width: 100%;
|
|
593
|
+
border-collapse: collapse;
|
|
594
|
+
margin-top: 20px;
|
|
595
|
+
font-size: 0.9rem;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.step-stats-table th,
|
|
599
|
+
.step-stats-table td {
|
|
600
|
+
padding: 12px 8px;
|
|
601
|
+
text-align: left;
|
|
602
|
+
border-bottom: 1px solid var(--border-color);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.step-stats-table th {
|
|
606
|
+
background: #f9fafb;
|
|
607
|
+
font-weight: 600;
|
|
608
|
+
color: var(--text-primary);
|
|
609
|
+
position: sticky;
|
|
610
|
+
top: 0;
|
|
611
|
+
font-size: 0.8rem;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.step-stats-table tr:hover {
|
|
615
|
+
background: #f9fafb;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.status-badge {
|
|
619
|
+
padding: 4px 8px;
|
|
620
|
+
border-radius: 4px;
|
|
621
|
+
font-size: 0.75rem;
|
|
622
|
+
font-weight: 500;
|
|
623
|
+
text-transform: uppercase;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.status-success {
|
|
627
|
+
background: #dcfce7;
|
|
628
|
+
color: #166534;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.status-warning {
|
|
632
|
+
background: #fef3c7;
|
|
633
|
+
color: #92400e;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.status-error {
|
|
637
|
+
background: #fee2e2;
|
|
638
|
+
color: #991b1b;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.tabs {
|
|
642
|
+
display: flex;
|
|
643
|
+
border-bottom: 1px solid var(--border-color);
|
|
644
|
+
margin-bottom: 20px;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.tab {
|
|
648
|
+
padding: 12px 24px;
|
|
649
|
+
background: none;
|
|
650
|
+
border: none;
|
|
651
|
+
cursor: pointer;
|
|
652
|
+
font-size: 1rem;
|
|
653
|
+
font-weight: 500;
|
|
654
|
+
color: var(--text-secondary);
|
|
655
|
+
border-bottom: 2px solid transparent;
|
|
656
|
+
transition: all 0.2s ease;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.tab.active {
|
|
660
|
+
color: var(--primary-color);
|
|
661
|
+
border-bottom-color: var(--primary-color);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.tab:hover {
|
|
665
|
+
color: var(--primary-color);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.tab-content {
|
|
669
|
+
display: none;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.tab-content.active {
|
|
673
|
+
display: block;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.grid-2 {
|
|
677
|
+
display: grid;
|
|
678
|
+
grid-template-columns: 1fr 1fr;
|
|
679
|
+
gap: 30px;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.grid-3 {
|
|
683
|
+
display: grid;
|
|
684
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
685
|
+
gap: 20px;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
@media (max-width: 768px) {
|
|
689
|
+
.grid-2, .grid-3 {
|
|
690
|
+
grid-template-columns: 1fr;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.container {
|
|
694
|
+
padding: 10px;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.header h1 {
|
|
698
|
+
font-size: 2rem;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.chart-container {
|
|
702
|
+
height: 300px;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.footer {
|
|
707
|
+
text-align: center;
|
|
708
|
+
padding: 20px;
|
|
709
|
+
color: var(--text-secondary);
|
|
710
|
+
font-size: 0.9rem;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.chart-controls {
|
|
714
|
+
margin-bottom: 15px;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.chart-controls button {
|
|
718
|
+
padding: 8px 16px;
|
|
719
|
+
background: var(--primary-color);
|
|
720
|
+
color: white;
|
|
721
|
+
border: none;
|
|
722
|
+
border-radius: 6px;
|
|
723
|
+
cursor: pointer;
|
|
724
|
+
margin-right: 10px;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.chart-controls button:hover {
|
|
728
|
+
background: #1d4ed8;
|
|
729
|
+
}
|
|
730
|
+
</style>
|
|
731
|
+
</head>
|
|
732
|
+
|
|
733
|
+
<body>
|
|
734
|
+
<div class="container">
|
|
735
|
+
<!-- Header -->
|
|
736
|
+
<div class="header">
|
|
737
|
+
<h1>{{testName}}</h1>
|
|
738
|
+
<p>Enhanced Performance Test Report • Generated on {{generatedAt}}</p>
|
|
739
|
+
</div>
|
|
740
|
+
|
|
741
|
+
<!-- Summary Metrics -->
|
|
742
|
+
<div class="summary-grid">
|
|
743
|
+
<div class="metric-card">
|
|
744
|
+
<div class="metric-value">{{summary.total_requests}}</div>
|
|
745
|
+
<div class="metric-label">Total Requests</div>
|
|
746
|
+
</div>
|
|
747
|
+
<div class="metric-card">
|
|
748
|
+
<div class="metric-value {{#if (gt summary.success_rate 95)}}metric-success{{else}}{{#if (gt summary.success_rate 90)}}metric-warning{{else}}metric-error{{/if}}{{/if}}">
|
|
749
|
+
{{toFixed summary.success_rate 2}}%
|
|
750
|
+
</div>
|
|
751
|
+
<div class="metric-label">Success Rate</div>
|
|
752
|
+
</div>
|
|
753
|
+
<div class="metric-card">
|
|
754
|
+
<div class="metric-value">{{toFixed summary.avg_response_time 2}}ms</div>
|
|
755
|
+
<div class="metric-label">Avg Response Time</div>
|
|
756
|
+
</div>
|
|
757
|
+
<div class="metric-card">
|
|
758
|
+
<div class="metric-value">{{toFixed summary.requests_per_second 2}}</div>
|
|
759
|
+
<div class="metric-label">Requests/sec</div>
|
|
760
|
+
</div>
|
|
761
|
+
<div class="metric-card">
|
|
762
|
+
<div class="metric-value">{{#if summary.peak_virtual_users}}{{summary.peak_virtual_users}}{{else}}{{#if summary.total_virtual_users}}{{summary.total_virtual_users}}{{else}}{{summary.vu_ramp_up.length}}{{/if}}{{/if}}</div>
|
|
763
|
+
<div class="metric-label">Virtual Users</div>
|
|
764
|
+
</div>
|
|
765
|
+
<div class="metric-card">
|
|
766
|
+
<div class="metric-value">{{toFixed summary.total_duration 0}}s</div>
|
|
767
|
+
<div class="metric-label">Total Duration</div>
|
|
768
|
+
</div>
|
|
769
|
+
</div>
|
|
770
|
+
|
|
771
|
+
<!-- NEW: Response Time Distribution -->
|
|
772
|
+
<div class="section">
|
|
773
|
+
<div class="section-header">
|
|
774
|
+
<h2 class="section-title">Response Time Distribution</h2>
|
|
775
|
+
</div>
|
|
776
|
+
<div class="section-content">
|
|
777
|
+
<div class="chart-container">
|
|
778
|
+
<canvas id="responseTimeDistributionChart"></canvas>
|
|
779
|
+
</div>
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
|
|
783
|
+
<!-- NEW: Throughput Charts -->
|
|
784
|
+
<div class="section">
|
|
785
|
+
<div class="section-header">
|
|
786
|
+
<h2 class="section-title">Throughput Analysis</h2>
|
|
787
|
+
</div>
|
|
788
|
+
<div class="section-content">
|
|
789
|
+
<div class="grid-2">
|
|
790
|
+
<div class="chart-container">
|
|
791
|
+
<canvas id="requestsPerSecondChart"></canvas>
|
|
792
|
+
</div>
|
|
793
|
+
<div class="chart-container">
|
|
794
|
+
<canvas id="responsesPerSecondChart"></canvas>
|
|
795
|
+
</div>
|
|
796
|
+
</div>
|
|
797
|
+
</div>
|
|
798
|
+
</div>
|
|
799
|
+
|
|
800
|
+
<!-- Enhanced Step Statistics Table -->
|
|
801
|
+
<div class="section">
|
|
802
|
+
<div class="section-header">
|
|
803
|
+
<h2 class="section-title">Enhanced Step Performance Statistics</h2>
|
|
804
|
+
</div>
|
|
805
|
+
<div class="section-content">
|
|
806
|
+
<table class="step-stats-table">
|
|
807
|
+
<thead>
|
|
808
|
+
<tr>
|
|
809
|
+
<th>Step Name</th>
|
|
810
|
+
<th>Scenario</th>
|
|
811
|
+
<th>Requests</th>
|
|
812
|
+
<th>Success Rate</th>
|
|
813
|
+
<th>Min</th>
|
|
814
|
+
<th>Avg</th>
|
|
815
|
+
<th>Max</th>
|
|
816
|
+
<th>P50</th>
|
|
817
|
+
<th>P90</th>
|
|
818
|
+
<th>P95</th>
|
|
819
|
+
<th>P99</th>
|
|
820
|
+
<th>P99.9</th>
|
|
821
|
+
<th>P99.99</th>
|
|
822
|
+
<th>Status</th>
|
|
823
|
+
</tr>
|
|
824
|
+
</thead>
|
|
825
|
+
<tbody>
|
|
826
|
+
{{#each stepStatistics}}
|
|
827
|
+
<tr>
|
|
828
|
+
<td><strong>{{step_name}}</strong></td>
|
|
829
|
+
<td>{{scenario}}</td>
|
|
830
|
+
<td>{{total_requests}}</td>
|
|
831
|
+
<td>{{toFixed success_rate 1}}%</td>
|
|
832
|
+
<td>{{toFixed min_response_time 1}}ms</td>
|
|
833
|
+
<td>{{toFixed avg_response_time 1}}ms</td>
|
|
834
|
+
<td>{{toFixed max_response_time 1}}ms</td>
|
|
835
|
+
<td>{{lookup percentiles 50}}ms</td>
|
|
836
|
+
<td>{{lookup percentiles 90}}ms</td>
|
|
837
|
+
<td>{{lookup percentiles 95}}ms</td>
|
|
838
|
+
<td>{{lookup percentiles 99}}ms</td>
|
|
839
|
+
<td>{{lookup percentiles 99.9}}ms</td>
|
|
840
|
+
<td>{{lookup percentiles 99.99}}ms</td>
|
|
841
|
+
<td>
|
|
842
|
+
<span class="status-badge {{#if (gt success_rate 95)}}status-success{{else}}{{#if (gt success_rate 90)}}status-warning{{else}}status-error{{/if}}{{/if}}">
|
|
843
|
+
{{#if (gt success_rate 95)}}Good{{else}}{{#if (gt success_rate 90)}}Warning{{else}}Error{{/if}}{{/if}}
|
|
844
|
+
</span>
|
|
845
|
+
</td>
|
|
846
|
+
</tr>
|
|
847
|
+
{{/each}}
|
|
848
|
+
</tbody>
|
|
849
|
+
</table>
|
|
850
|
+
</div>
|
|
851
|
+
</div>
|
|
852
|
+
|
|
853
|
+
<!-- Original Charts -->
|
|
854
|
+
<div class="section">
|
|
855
|
+
<div class="section-header">
|
|
856
|
+
<h2 class="section-title">Response Time Analysis</h2>
|
|
857
|
+
</div>
|
|
858
|
+
<div class="section-content">
|
|
859
|
+
<div class="tabs">
|
|
860
|
+
<button class="tab active" onclick="showTab('timeline')">Timeline</button>
|
|
861
|
+
<button class="tab" onclick="showTab('by-step')">By Step</button>
|
|
862
|
+
</div>
|
|
863
|
+
|
|
864
|
+
<div id="timeline" class="tab-content active">
|
|
865
|
+
<div class="chart-container large">
|
|
866
|
+
<canvas id="timelineChart"></canvas>
|
|
867
|
+
</div>
|
|
868
|
+
</div>
|
|
869
|
+
|
|
870
|
+
<div id="by-step" class="tab-content">
|
|
871
|
+
<div class="chart-container large">
|
|
872
|
+
<canvas id="stepResponseTimeChart"></canvas>
|
|
873
|
+
</div>
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
|
|
878
|
+
<!-- Footer -->
|
|
879
|
+
<div class="footer">
|
|
880
|
+
<p>Generated by Perfornium Performance Testing Framework</p>
|
|
881
|
+
</div>
|
|
882
|
+
</div>
|
|
883
|
+
|
|
884
|
+
<script>
|
|
885
|
+
// Chart data from server
|
|
886
|
+
const summaryData = {{{ summaryData }}};
|
|
887
|
+
const stepStatistics = {{{ stepStatisticsData }}};
|
|
888
|
+
const timelineData = {{{ timelineData }}};
|
|
889
|
+
const responseTimeDistributionData = {{{ responseTimeDistributionData }}};
|
|
890
|
+
const requestsPerSecondData = {{{ requestsPerSecondData }}};
|
|
891
|
+
const responsesPerSecondData = {{{ responsesPerSecondData }}};
|
|
892
|
+
|
|
893
|
+
// Tab functionality
|
|
894
|
+
function showTab(tabName) {
|
|
895
|
+
document.querySelectorAll('.tab-content').forEach(content => {
|
|
896
|
+
content.classList.remove('active');
|
|
897
|
+
});
|
|
898
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
899
|
+
tab.classList.remove('active');
|
|
900
|
+
});
|
|
901
|
+
document.getElementById(tabName).classList.add('active');
|
|
902
|
+
event.target.classList.add('active');
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// NEW: Response Time Distribution Chart
|
|
906
|
+
const responseDistCtx = document.getElementById('responseTimeDistributionChart').getContext('2d');
|
|
907
|
+
new Chart(responseDistCtx, {
|
|
908
|
+
type: 'bar',
|
|
909
|
+
data: {
|
|
910
|
+
labels: responseTimeDistributionData.map(d => d.bucket),
|
|
911
|
+
datasets: [{
|
|
912
|
+
label: 'Request Count',
|
|
913
|
+
data: responseTimeDistributionData.map(d => d.count),
|
|
914
|
+
backgroundColor: 'rgba(37, 99, 235, 0.6)',
|
|
915
|
+
borderColor: 'rgba(37, 99, 235, 1)',
|
|
916
|
+
borderWidth: 1
|
|
917
|
+
}, {
|
|
918
|
+
label: 'Percentage',
|
|
919
|
+
data: responseTimeDistributionData.map(d => d.percentage),
|
|
920
|
+
type: 'line',
|
|
921
|
+
borderColor: 'rgba(239, 68, 68, 1)',
|
|
922
|
+
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
923
|
+
yAxisID: 'y1',
|
|
924
|
+
tension: 0.4
|
|
925
|
+
}]
|
|
926
|
+
},
|
|
927
|
+
options: {
|
|
928
|
+
responsive: true,
|
|
929
|
+
maintainAspectRatio: false,
|
|
930
|
+
plugins: {
|
|
931
|
+
title: {
|
|
932
|
+
display: true,
|
|
933
|
+
text: 'Overall Response Time Distribution'
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
scales: {
|
|
937
|
+
y: {
|
|
938
|
+
beginAtZero: true,
|
|
939
|
+
title: {
|
|
940
|
+
display: true,
|
|
941
|
+
text: 'Number of Requests'
|
|
942
|
+
}
|
|
943
|
+
},
|
|
944
|
+
y1: {
|
|
945
|
+
type: 'linear',
|
|
946
|
+
display: true,
|
|
947
|
+
position: 'right',
|
|
948
|
+
title: {
|
|
949
|
+
display: true,
|
|
950
|
+
text: 'Percentage (%)'
|
|
951
|
+
},
|
|
952
|
+
grid: {
|
|
953
|
+
drawOnChartArea: false,
|
|
954
|
+
},
|
|
955
|
+
min: 0,
|
|
956
|
+
max: 100
|
|
957
|
+
},
|
|
958
|
+
x: {
|
|
959
|
+
title: {
|
|
960
|
+
display: true,
|
|
961
|
+
text: 'Response Time Range'
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
// NEW: Requests Per Second Chart
|
|
969
|
+
const requestsPerSecCtx = document.getElementById('requestsPerSecondChart').getContext('2d');
|
|
970
|
+
new Chart(requestsPerSecCtx, {
|
|
971
|
+
type: 'line',
|
|
972
|
+
data: {
|
|
973
|
+
datasets: [{
|
|
974
|
+
label: 'Total Requests/sec',
|
|
975
|
+
data: requestsPerSecondData.map(d => ({
|
|
976
|
+
x: new Date(d.timestamp),
|
|
977
|
+
y: d.requests_per_second
|
|
978
|
+
})),
|
|
979
|
+
borderColor: '#2563eb',
|
|
980
|
+
backgroundColor: 'rgba(37, 99, 235, 0.1)',
|
|
981
|
+
fill: true,
|
|
982
|
+
tension: 0.4
|
|
983
|
+
}, {
|
|
984
|
+
label: 'Successful Requests/sec',
|
|
985
|
+
data: requestsPerSecondData.map(d => ({
|
|
986
|
+
x: new Date(d.timestamp),
|
|
987
|
+
y: d.successful_requests_per_second
|
|
988
|
+
})),
|
|
989
|
+
borderColor: '#10b981',
|
|
990
|
+
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
991
|
+
fill: true,
|
|
992
|
+
tension: 0.4
|
|
993
|
+
}]
|
|
994
|
+
},
|
|
995
|
+
options: {
|
|
996
|
+
responsive: true,
|
|
997
|
+
maintainAspectRatio: false,
|
|
998
|
+
plugins: {
|
|
999
|
+
title: {
|
|
1000
|
+
display: true,
|
|
1001
|
+
text: 'Requests Per Second Over Time'
|
|
1002
|
+
}
|
|
1003
|
+
},
|
|
1004
|
+
scales: {
|
|
1005
|
+
x: {
|
|
1006
|
+
type: 'time',
|
|
1007
|
+
time: {
|
|
1008
|
+
displayFormats: {
|
|
1009
|
+
second: 'HH:mm:ss'
|
|
1010
|
+
}
|
|
1011
|
+
},
|
|
1012
|
+
title: {
|
|
1013
|
+
display: true,
|
|
1014
|
+
text: 'Time'
|
|
1015
|
+
}
|
|
1016
|
+
},
|
|
1017
|
+
y: {
|
|
1018
|
+
beginAtZero: true,
|
|
1019
|
+
title: {
|
|
1020
|
+
display: true,
|
|
1021
|
+
text: 'Requests/Second'
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
// NEW: Responses Per Second Chart
|
|
1029
|
+
const responsesPerSecCtx = document.getElementById('responsesPerSecondChart').getContext('2d');
|
|
1030
|
+
new Chart(responsesPerSecCtx, {
|
|
1031
|
+
type: 'line',
|
|
1032
|
+
data: {
|
|
1033
|
+
datasets: [{
|
|
1034
|
+
label: 'Successful Responses/sec',
|
|
1035
|
+
data: responsesPerSecondData.map(d => ({
|
|
1036
|
+
x: new Date(d.timestamp),
|
|
1037
|
+
y: d.responses_per_second
|
|
1038
|
+
})),
|
|
1039
|
+
borderColor: '#10b981',
|
|
1040
|
+
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
1041
|
+
fill: true,
|
|
1042
|
+
tension: 0.4
|
|
1043
|
+
}, {
|
|
1044
|
+
label: 'Error Responses/sec',
|
|
1045
|
+
data: responsesPerSecondData.map(d => ({
|
|
1046
|
+
x: new Date(d.timestamp),
|
|
1047
|
+
y: d.error_responses_per_second
|
|
1048
|
+
})),
|
|
1049
|
+
borderColor: '#ef4444',
|
|
1050
|
+
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
1051
|
+
fill: true,
|
|
1052
|
+
tension: 0.4
|
|
1053
|
+
}]
|
|
1054
|
+
},
|
|
1055
|
+
options: {
|
|
1056
|
+
responsive: true,
|
|
1057
|
+
maintainAspectRatio: false,
|
|
1058
|
+
plugins: {
|
|
1059
|
+
title: {
|
|
1060
|
+
display: true,
|
|
1061
|
+
text: 'Responses Per Second Over Time'
|
|
1062
|
+
}
|
|
1063
|
+
},
|
|
1064
|
+
scales: {
|
|
1065
|
+
x: {
|
|
1066
|
+
type: 'time',
|
|
1067
|
+
time: {
|
|
1068
|
+
displayFormats: {
|
|
1069
|
+
second: 'HH:mm:ss'
|
|
1070
|
+
}
|
|
1071
|
+
},
|
|
1072
|
+
title: {
|
|
1073
|
+
display: true,
|
|
1074
|
+
text: 'Time'
|
|
1075
|
+
}
|
|
1076
|
+
},
|
|
1077
|
+
y: {
|
|
1078
|
+
beginAtZero: true,
|
|
1079
|
+
title: {
|
|
1080
|
+
display: true,
|
|
1081
|
+
text: 'Responses/Second'
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
// Timeline Chart
|
|
1089
|
+
const timelineCtx = document.getElementById('timelineChart').getContext('2d');
|
|
1090
|
+
new Chart(timelineCtx, {
|
|
1091
|
+
type: 'line',
|
|
1092
|
+
data: {
|
|
1093
|
+
datasets: [
|
|
1094
|
+
{
|
|
1095
|
+
label: 'Avg Response Time (ms)',
|
|
1096
|
+
data: timelineData.map(d => ({
|
|
1097
|
+
x: new Date(d.timestamp),
|
|
1098
|
+
y: d.avg_response_time
|
|
1099
|
+
})),
|
|
1100
|
+
borderColor: '#10b981',
|
|
1101
|
+
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
1102
|
+
yAxisID: 'y',
|
|
1103
|
+
tension: 0.4
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
label: 'Success Rate (%)',
|
|
1107
|
+
data: timelineData.map(d => ({
|
|
1108
|
+
x: new Date(d.timestamp),
|
|
1109
|
+
y: d.success_rate
|
|
1110
|
+
})),
|
|
1111
|
+
borderColor: '#f59e0b',
|
|
1112
|
+
backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
|
1113
|
+
yAxisID: 'y1',
|
|
1114
|
+
tension: 0.4
|
|
1115
|
+
}
|
|
1116
|
+
]
|
|
1117
|
+
},
|
|
1118
|
+
options: {
|
|
1119
|
+
responsive: true,
|
|
1120
|
+
maintainAspectRatio: false,
|
|
1121
|
+
plugins: {
|
|
1122
|
+
title: {
|
|
1123
|
+
display: true,
|
|
1124
|
+
text: 'Performance Timeline'
|
|
1125
|
+
}
|
|
1126
|
+
},
|
|
1127
|
+
scales: {
|
|
1128
|
+
x: {
|
|
1129
|
+
type: 'time',
|
|
1130
|
+
time: {
|
|
1131
|
+
displayFormats: {
|
|
1132
|
+
second: 'HH:mm:ss'
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
},
|
|
1136
|
+
y: {
|
|
1137
|
+
type: 'linear',
|
|
1138
|
+
display: true,
|
|
1139
|
+
position: 'left',
|
|
1140
|
+
title: {
|
|
1141
|
+
display: true,
|
|
1142
|
+
text: 'Response Time (ms)'
|
|
1143
|
+
}
|
|
1144
|
+
},
|
|
1145
|
+
y1: {
|
|
1146
|
+
type: 'linear',
|
|
1147
|
+
display: true,
|
|
1148
|
+
position: 'right',
|
|
1149
|
+
title: {
|
|
1150
|
+
display: true,
|
|
1151
|
+
text: 'Success Rate (%)'
|
|
1152
|
+
},
|
|
1153
|
+
grid: {
|
|
1154
|
+
drawOnChartArea: false,
|
|
1155
|
+
},
|
|
1156
|
+
min: 0,
|
|
1157
|
+
max: 100
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
// Step Response Time Chart
|
|
1164
|
+
const stepResponseCtx = document.getElementById('stepResponseTimeChart').getContext('2d');
|
|
1165
|
+
new Chart(stepResponseCtx, {
|
|
1166
|
+
type: 'bar',
|
|
1167
|
+
data: {
|
|
1168
|
+
labels: stepStatistics.map(s => s.step_name),
|
|
1169
|
+
datasets: [
|
|
1170
|
+
{
|
|
1171
|
+
label: 'P50',
|
|
1172
|
+
data: stepStatistics.map(s => s.percentiles[50] || 0),
|
|
1173
|
+
backgroundColor: 'rgba(37, 99, 235, 0.6)'
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
label: 'P90',
|
|
1177
|
+
data: stepStatistics.map(s => s.percentiles[90] || 0),
|
|
1178
|
+
backgroundColor: 'rgba(16, 185, 129, 0.6)'
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
label: 'P95',
|
|
1182
|
+
data: stepStatistics.map(s => s.percentiles[95] || 0),
|
|
1183
|
+
backgroundColor: 'rgba(245, 158, 11, 0.6)'
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
label: 'P99',
|
|
1187
|
+
data: stepStatistics.map(s => s.percentiles[99] || 0),
|
|
1188
|
+
backgroundColor: 'rgba(239, 68, 68, 0.6)'
|
|
1189
|
+
}
|
|
1190
|
+
]
|
|
1191
|
+
},
|
|
1192
|
+
options: {
|
|
1193
|
+
responsive: true,
|
|
1194
|
+
maintainAspectRatio: false,
|
|
1195
|
+
plugins: {
|
|
1196
|
+
title: {
|
|
1197
|
+
display: true,
|
|
1198
|
+
text: 'Response Time Percentiles by Step'
|
|
1199
|
+
}
|
|
1200
|
+
},
|
|
1201
|
+
scales: {
|
|
1202
|
+
y: {
|
|
1203
|
+
beginAtZero: true,
|
|
1204
|
+
title: {
|
|
1205
|
+
display: true,
|
|
1206
|
+
text: 'Response Time (ms)'
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
</script>
|
|
1213
|
+
</body>
|
|
1214
|
+
</html>`;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
exports.HTMLReportGenerator = HTMLReportGenerator;
|