@testsmith/perfornium 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +360 -0
  2. package/dist/cli/cli.d.ts +2 -0
  3. package/dist/cli/cli.js +192 -0
  4. package/dist/cli/commands/distributed.d.ts +11 -0
  5. package/dist/cli/commands/distributed.js +179 -0
  6. package/dist/cli/commands/import.d.ts +23 -0
  7. package/dist/cli/commands/import.js +461 -0
  8. package/dist/cli/commands/init.d.ts +7 -0
  9. package/dist/cli/commands/init.js +923 -0
  10. package/dist/cli/commands/mock.d.ts +7 -0
  11. package/dist/cli/commands/mock.js +281 -0
  12. package/dist/cli/commands/report.d.ts +5 -0
  13. package/dist/cli/commands/report.js +70 -0
  14. package/dist/cli/commands/run.d.ts +12 -0
  15. package/dist/cli/commands/run.js +260 -0
  16. package/dist/cli/commands/validate.d.ts +3 -0
  17. package/dist/cli/commands/validate.js +35 -0
  18. package/dist/cli/commands/worker.d.ts +27 -0
  19. package/dist/cli/commands/worker.js +320 -0
  20. package/dist/config/index.d.ts +2 -0
  21. package/dist/config/index.js +20 -0
  22. package/dist/config/parser.d.ts +19 -0
  23. package/dist/config/parser.js +330 -0
  24. package/dist/config/types/global-config.d.ts +74 -0
  25. package/dist/config/types/global-config.js +2 -0
  26. package/dist/config/types/hooks.d.ts +58 -0
  27. package/dist/config/types/hooks.js +3 -0
  28. package/dist/config/types/import-types.d.ts +33 -0
  29. package/dist/config/types/import-types.js +2 -0
  30. package/dist/config/types/index.d.ts +11 -0
  31. package/dist/config/types/index.js +27 -0
  32. package/dist/config/types/load-config.d.ts +32 -0
  33. package/dist/config/types/load-config.js +9 -0
  34. package/dist/config/types/output-config.d.ts +10 -0
  35. package/dist/config/types/output-config.js +2 -0
  36. package/dist/config/types/report-config.d.ts +10 -0
  37. package/dist/config/types/report-config.js +2 -0
  38. package/dist/config/types/runtime-types.d.ts +6 -0
  39. package/dist/config/types/runtime-types.js +2 -0
  40. package/dist/config/types/scenario-config.d.ts +30 -0
  41. package/dist/config/types/scenario-config.js +2 -0
  42. package/dist/config/types/step-types.d.ts +139 -0
  43. package/dist/config/types/step-types.js +2 -0
  44. package/dist/config/types/test-configuration.d.ts +18 -0
  45. package/dist/config/types/test-configuration.js +2 -0
  46. package/dist/config/types/worker-config.d.ts +12 -0
  47. package/dist/config/types/worker-config.js +2 -0
  48. package/dist/config/validator.d.ts +19 -0
  49. package/dist/config/validator.js +198 -0
  50. package/dist/core/csv-data-provider.d.ts +47 -0
  51. package/dist/core/csv-data-provider.js +265 -0
  52. package/dist/core/hooks-manager.d.ts +33 -0
  53. package/dist/core/hooks-manager.js +129 -0
  54. package/dist/core/index.d.ts +5 -0
  55. package/dist/core/index.js +11 -0
  56. package/dist/core/script-executor.d.ts +14 -0
  57. package/dist/core/script-executor.js +290 -0
  58. package/dist/core/step-executor.d.ts +41 -0
  59. package/dist/core/step-executor.js +680 -0
  60. package/dist/core/test-runner.d.ts +34 -0
  61. package/dist/core/test-runner.js +465 -0
  62. package/dist/core/threshold-evaluator.d.ts +43 -0
  63. package/dist/core/threshold-evaluator.js +170 -0
  64. package/dist/core/virtual-user-pool.d.ts +42 -0
  65. package/dist/core/virtual-user-pool.js +136 -0
  66. package/dist/core/virtual-user.d.ts +51 -0
  67. package/dist/core/virtual-user.js +488 -0
  68. package/dist/distributed/coordinator.d.ts +34 -0
  69. package/dist/distributed/coordinator.js +158 -0
  70. package/dist/distributed/health-monitor.d.ts +18 -0
  71. package/dist/distributed/health-monitor.js +72 -0
  72. package/dist/distributed/load-distributor.d.ts +17 -0
  73. package/dist/distributed/load-distributor.js +106 -0
  74. package/dist/distributed/remote-worker.d.ts +37 -0
  75. package/dist/distributed/remote-worker.js +241 -0
  76. package/dist/distributed/result-aggregator.d.ts +43 -0
  77. package/dist/distributed/result-aggregator.js +146 -0
  78. package/dist/dsl/index.d.ts +3 -0
  79. package/dist/dsl/index.js +11 -0
  80. package/dist/dsl/test-builder.d.ts +111 -0
  81. package/dist/dsl/test-builder.js +514 -0
  82. package/dist/importers/har-importer.d.ts +17 -0
  83. package/dist/importers/har-importer.js +172 -0
  84. package/dist/importers/open-api-importer.d.ts +23 -0
  85. package/dist/importers/open-api-importer.js +181 -0
  86. package/dist/importers/wsdl-importer.d.ts +42 -0
  87. package/dist/importers/wsdl-importer.js +440 -0
  88. package/dist/index.d.ts +5 -0
  89. package/dist/index.js +17 -0
  90. package/dist/load-patterns/arrivals.d.ts +7 -0
  91. package/dist/load-patterns/arrivals.js +118 -0
  92. package/dist/load-patterns/base.d.ts +9 -0
  93. package/dist/load-patterns/base.js +2 -0
  94. package/dist/load-patterns/basic.d.ts +7 -0
  95. package/dist/load-patterns/basic.js +117 -0
  96. package/dist/load-patterns/stepping.d.ts +6 -0
  97. package/dist/load-patterns/stepping.js +122 -0
  98. package/dist/metrics/collector.d.ts +72 -0
  99. package/dist/metrics/collector.js +662 -0
  100. package/dist/metrics/types.d.ts +135 -0
  101. package/dist/metrics/types.js +2 -0
  102. package/dist/outputs/base.d.ts +7 -0
  103. package/dist/outputs/base.js +2 -0
  104. package/dist/outputs/csv.d.ts +13 -0
  105. package/dist/outputs/csv.js +163 -0
  106. package/dist/outputs/graphite.d.ts +13 -0
  107. package/dist/outputs/graphite.js +126 -0
  108. package/dist/outputs/influxdb.d.ts +12 -0
  109. package/dist/outputs/influxdb.js +82 -0
  110. package/dist/outputs/json.d.ts +14 -0
  111. package/dist/outputs/json.js +107 -0
  112. package/dist/outputs/streaming-csv.d.ts +37 -0
  113. package/dist/outputs/streaming-csv.js +254 -0
  114. package/dist/outputs/streaming-json.d.ts +43 -0
  115. package/dist/outputs/streaming-json.js +353 -0
  116. package/dist/outputs/webhook.d.ts +16 -0
  117. package/dist/outputs/webhook.js +96 -0
  118. package/dist/protocols/base.d.ts +33 -0
  119. package/dist/protocols/base.js +2 -0
  120. package/dist/protocols/rest/handler.d.ts +67 -0
  121. package/dist/protocols/rest/handler.js +776 -0
  122. package/dist/protocols/soap/handler.d.ts +12 -0
  123. package/dist/protocols/soap/handler.js +165 -0
  124. package/dist/protocols/web/core-web-vitals.d.ts +121 -0
  125. package/dist/protocols/web/core-web-vitals.js +373 -0
  126. package/dist/protocols/web/handler.d.ts +50 -0
  127. package/dist/protocols/web/handler.js +706 -0
  128. package/dist/recorder/native-recorder.d.ts +14 -0
  129. package/dist/recorder/native-recorder.js +533 -0
  130. package/dist/recorder/scenario-recorder.d.ts +55 -0
  131. package/dist/recorder/scenario-recorder.js +296 -0
  132. package/dist/reporting/constants.d.ts +94 -0
  133. package/dist/reporting/constants.js +82 -0
  134. package/dist/reporting/enhanced-html-generator.d.ts +55 -0
  135. package/dist/reporting/enhanced-html-generator.js +965 -0
  136. package/dist/reporting/generator.d.ts +42 -0
  137. package/dist/reporting/generator.js +1217 -0
  138. package/dist/reporting/statistics.d.ts +144 -0
  139. package/dist/reporting/statistics.js +742 -0
  140. package/dist/reporting/templates/enhanced-report.hbs +2812 -0
  141. package/dist/reporting/templates/html.hbs +2453 -0
  142. package/dist/utils/faker-manager.d.ts +55 -0
  143. package/dist/utils/faker-manager.js +166 -0
  144. package/dist/utils/file-manager.d.ts +33 -0
  145. package/dist/utils/file-manager.js +154 -0
  146. package/dist/utils/handlebars-manager.d.ts +42 -0
  147. package/dist/utils/handlebars-manager.js +172 -0
  148. package/dist/utils/logger.d.ts +16 -0
  149. package/dist/utils/logger.js +46 -0
  150. package/dist/utils/template.d.ts +80 -0
  151. package/dist/utils/template.js +513 -0
  152. package/dist/utils/test-output-writer.d.ts +56 -0
  153. package/dist/utils/test-output-writer.js +643 -0
  154. package/dist/utils/time.d.ts +3 -0
  155. package/dist/utils/time.js +23 -0
  156. package/dist/utils/timestamp-helper.d.ts +17 -0
  157. package/dist/utils/timestamp-helper.js +53 -0
  158. package/dist/workers/manager.d.ts +18 -0
  159. package/dist/workers/manager.js +95 -0
  160. package/dist/workers/server.d.ts +21 -0
  161. package/dist/workers/server.js +205 -0
  162. package/dist/workers/worker.d.ts +19 -0
  163. package/dist/workers/worker.js +147 -0
  164. package/package.json +102 -0
@@ -0,0 +1,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;