@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,2453 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>{{testName}} - Enhanced Performance Report</title>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
9
+ <script
10
+ src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
11
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.1/dist/chartjs-plugin-zoom.min.js"></script>
12
+
13
+ <style>
14
+ :root {
15
+ --primary-color: #2563eb;
16
+ --success-color: #10b981;
17
+ --warning-color: #f59e0b;
18
+ --error-color: #ef4444;
19
+ --background-color: #f8fafc;
20
+ --card-background: #ffffff;
21
+ --text-primary: #1f2937;
22
+ --text-secondary: #6b7280;
23
+ --border-color: #e5e7eb;
24
+ }
25
+
26
+ [data-theme="dark"] {
27
+ --primary-color: #3b82f6;
28
+ --success-color: #10b981;
29
+ --warning-color: #fbbf24;
30
+ --error-color: #f87171;
31
+ --background-color: #111827;
32
+ --card-background: #1f2937;
33
+ --text-primary: #f9fafb;
34
+ --text-secondary: #9ca3af;
35
+ --border-color: #374151;
36
+ }
37
+
38
+ * {
39
+ margin: 0;
40
+ padding: 0;
41
+ box-sizing: border-box;
42
+ }
43
+
44
+ body {
45
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
46
+ background-color: var(--background-color);
47
+ color: var(--text-primary);
48
+ line-height: 1.6;
49
+ }
50
+
51
+ .container {
52
+ max-width: 1400px;
53
+ margin: 0 auto;
54
+ padding: 20px;
55
+ }
56
+
57
+ .header {
58
+ background: linear-gradient(135deg, var(--primary-color) 0%, #1d4ed8 100%);
59
+ color: white;
60
+ padding: 40px 20px;
61
+ border-radius: 12px;
62
+ margin-bottom: 30px;
63
+ text-align: center;
64
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
65
+ position: relative;
66
+ }
67
+
68
+ [data-theme="dark"] .header {
69
+ background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%);
70
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
71
+ }
72
+
73
+ .header h1 {
74
+ font-size: 2.5rem;
75
+ font-weight: 700;
76
+ margin-bottom: 10px;
77
+ }
78
+
79
+ .header p {
80
+ font-size: 1.1rem;
81
+ opacity: 0.9;
82
+ }
83
+
84
+ .theme-toggle {
85
+ position: absolute;
86
+ top: 20px;
87
+ right: 20px;
88
+ background: rgba(255, 255, 255, 0.2);
89
+ border: 2px solid rgba(255, 255, 255, 0.3);
90
+ border-radius: 50px;
91
+ padding: 8px 16px;
92
+ cursor: pointer;
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 8px;
96
+ color: white;
97
+ font-size: 0.9rem;
98
+ font-weight: 500;
99
+ transition: all 0.3s ease;
100
+ }
101
+
102
+ .theme-toggle:hover {
103
+ background: rgba(255, 255, 255, 0.3);
104
+ border-color: rgba(255, 255, 255, 0.5);
105
+ }
106
+
107
+ .theme-icon {
108
+ font-size: 1.2rem;
109
+ }
110
+
111
+ .summary-grid {
112
+ display: grid;
113
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
114
+ gap: 20px;
115
+ margin-bottom: 40px;
116
+ }
117
+
118
+ .metric-card {
119
+ background: var(--card-background);
120
+ padding: 24px;
121
+ border-radius: 12px;
122
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
123
+ border: 1px solid var(--border-color);
124
+ text-align: center;
125
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
126
+ }
127
+
128
+ .metric-card:hover {
129
+ transform: translateY(-2px);
130
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
131
+ }
132
+
133
+ .metric-value {
134
+ font-size: 2.5rem;
135
+ font-weight: 700;
136
+ margin-bottom: 8px;
137
+ }
138
+
139
+ .metric-label {
140
+ color: var(--text-secondary);
141
+ font-size: 0.9rem;
142
+ text-transform: uppercase;
143
+ letter-spacing: 1px;
144
+ font-weight: 500;
145
+ }
146
+
147
+ .metric-success {
148
+ color: var(--success-color);
149
+ }
150
+
151
+ .metric-warning {
152
+ color: var(--warning-color);
153
+ }
154
+
155
+ .metric-error {
156
+ color: var(--error-color);
157
+ }
158
+
159
+ .section {
160
+ background: var(--card-background);
161
+ margin-bottom: 30px;
162
+ border-radius: 12px;
163
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
164
+ border: 1px solid var(--border-color);
165
+ overflow: hidden;
166
+ }
167
+
168
+ .section-header {
169
+ padding: 20px 24px;
170
+ border-bottom: 1px solid var(--border-color);
171
+ background: #f9fafb;
172
+ }
173
+
174
+ [data-theme="dark"] .section-header {
175
+ background: #1f2937;
176
+ }
177
+
178
+ .section-title {
179
+ font-size: 1.5rem;
180
+ font-weight: 600;
181
+ color: var(--text-primary);
182
+ }
183
+
184
+ .section-content {
185
+ padding: 24px;
186
+ }
187
+
188
+ .chart-container {
189
+ margin-bottom: 30px;
190
+ height: 400px;
191
+ position: relative;
192
+ }
193
+
194
+ .chart-container.large {
195
+ height: 500px;
196
+ }
197
+
198
+ .chart-container.small {
199
+ height: 300px;
200
+ }
201
+
202
+ .chart-container canvas {
203
+ max-height: 100%;
204
+ }
205
+
206
+ .step-stats-table {
207
+ width: 100%;
208
+ border-collapse: collapse;
209
+ margin-top: 20px;
210
+ font-size: 0.85rem;
211
+ }
212
+
213
+ .step-stats-table th,
214
+ .step-stats-table td {
215
+ padding: 8px 6px;
216
+ text-align: left;
217
+ border-bottom: 1px solid var(--border-color);
218
+ }
219
+
220
+ .step-stats-table th {
221
+ background: #f9fafb;
222
+ font-weight: 600;
223
+ color: var(--text-primary);
224
+ position: sticky;
225
+ top: 0;
226
+ font-size: 0.75rem;
227
+ }
228
+
229
+ [data-theme="dark"] .step-stats-table th {
230
+ background: #1f2937;
231
+ }
232
+
233
+ .step-stats-table tr:hover {
234
+ background: #f9fafb;
235
+ }
236
+
237
+ [data-theme="dark"] .step-stats-table tr:hover {
238
+ background: #374151;
239
+ }
240
+
241
+ /* Error Details Table Styles */
242
+ .data-table {
243
+ width: 100%;
244
+ border-collapse: collapse;
245
+ margin-top: 20px;
246
+ font-size: 0.85rem;
247
+ }
248
+
249
+ .data-table th,
250
+ .data-table td {
251
+ padding: 10px 8px;
252
+ text-align: left;
253
+ border-bottom: 1px solid var(--border-color);
254
+ }
255
+
256
+ .data-table th {
257
+ background: #f9fafb;
258
+ font-weight: 600;
259
+ color: var(--text-primary);
260
+ position: sticky;
261
+ top: 0;
262
+ font-size: 0.8rem;
263
+ white-space: nowrap;
264
+ }
265
+
266
+ [data-theme="dark"] .data-table th {
267
+ background: #1f2937;
268
+ }
269
+
270
+ .data-table tr:hover {
271
+ background: #fef9f9;
272
+ }
273
+
274
+ [data-theme="dark"] .data-table tr:hover {
275
+ background: #374151;
276
+ }
277
+
278
+ .data-table .url-cell,
279
+ .data-table .error-cell,
280
+ .data-table .response-body-cell {
281
+ max-width: 250px;
282
+ overflow: hidden;
283
+ text-overflow: ellipsis;
284
+ white-space: nowrap;
285
+ cursor: help;
286
+ }
287
+
288
+ .data-table .error-cell {
289
+ color: var(--error-color);
290
+ font-weight: 500;
291
+ }
292
+
293
+ .data-table .response-body-cell {
294
+ font-family: monospace;
295
+ font-size: 0.75rem;
296
+ color: var(--text-secondary);
297
+ }
298
+
299
+ .table-container {
300
+ overflow-x: auto;
301
+ margin-top: 15px;
302
+ }
303
+
304
+ .status-badge {
305
+ padding: 4px 8px;
306
+ border-radius: 4px;
307
+ font-size: 0.7rem;
308
+ font-weight: 500;
309
+ text-transform: uppercase;
310
+ }
311
+
312
+ .status-success {
313
+ background: #dcfce7;
314
+ color: #166534;
315
+ }
316
+
317
+ .status-warning {
318
+ background: #fef3c7;
319
+ color: #92400e;
320
+ }
321
+
322
+ .status-error {
323
+ background: #fee2e2;
324
+ color: #991b1b;
325
+ }
326
+
327
+ .tabs {
328
+ display: flex;
329
+ border-bottom: 1px solid var(--border-color);
330
+ margin-bottom: 20px;
331
+ }
332
+
333
+ .tab {
334
+ padding: 12px 24px;
335
+ background: none;
336
+ border: none;
337
+ cursor: pointer;
338
+ font-size: 1rem;
339
+ font-weight: 500;
340
+ color: var(--text-secondary);
341
+ border-bottom: 2px solid transparent;
342
+ transition: all 0.2s ease;
343
+ }
344
+
345
+ .tab.active {
346
+ color: var(--primary-color);
347
+ border-bottom-color: var(--primary-color);
348
+ }
349
+
350
+ .tab:hover {
351
+ color: var(--primary-color);
352
+ }
353
+
354
+ .tab-content {
355
+ display: none;
356
+ }
357
+
358
+ .tab-content.active {
359
+ display: block;
360
+ }
361
+
362
+ .grid-2 {
363
+ display: grid;
364
+ grid-template-columns: 1fr 1fr;
365
+ gap: 30px;
366
+ }
367
+
368
+ .grid-3 {
369
+ display: grid;
370
+ grid-template-columns: 1fr 1fr 1fr;
371
+ gap: 20px;
372
+ }
373
+
374
+ @media (max-width: 768px) {
375
+ .grid-2, .grid-3 {
376
+ grid-template-columns: 1fr;
377
+ }
378
+
379
+ .container {
380
+ padding: 10px;
381
+ }
382
+
383
+ .header h1 {
384
+ font-size: 2rem;
385
+ }
386
+
387
+ .chart-container {
388
+ height: 300px;
389
+ }
390
+
391
+ .step-stats-table {
392
+ font-size: 0.7rem;
393
+ }
394
+
395
+ .step-stats-table th,
396
+ .step-stats-table td {
397
+ padding: 6px 4px;
398
+ }
399
+ }
400
+
401
+ .footer {
402
+ text-align: center;
403
+ padding: 20px;
404
+ color: var(--text-secondary);
405
+ font-size: 0.9rem;
406
+ }
407
+
408
+ .chart-controls {
409
+ margin-bottom: 15px;
410
+ }
411
+
412
+ .chart-controls button {
413
+ padding: 8px 16px;
414
+ background: var(--primary-color);
415
+ color: white;
416
+ border: none;
417
+ border-radius: 6px;
418
+ cursor: pointer;
419
+ margin-right: 10px;
420
+ }
421
+
422
+ .chart-controls button:hover {
423
+ background: #1d4ed8;
424
+ }
425
+
426
+ /* Enhanced table styles for better readability */
427
+ .step-stats-table td:nth-child(n+5):nth-child(-n+13) {
428
+ text-align: right;
429
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
430
+ }
431
+
432
+ .step-stats-table th:nth-child(n+5):nth-child(-n+13) {
433
+ text-align: right;
434
+ }
435
+
436
+ /* Core Web Vitals Styles */
437
+ .vitals-section {
438
+ margin-bottom: 30px;
439
+ }
440
+
441
+ .vitals-cards {
442
+ display: grid;
443
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
444
+ gap: 20px;
445
+ margin-bottom: 20px;
446
+ }
447
+
448
+ .vitals-card {
449
+ background: var(--card-background);
450
+ border-radius: 12px;
451
+ padding: 20px;
452
+ text-align: center;
453
+ border: 2px solid var(--border-color);
454
+ transition: transform 0.2s ease;
455
+ }
456
+
457
+ .vitals-card:hover {
458
+ transform: translateY(-2px);
459
+ }
460
+
461
+ .vitals-card h3 {
462
+ margin-bottom: 10px;
463
+ font-size: 0.9rem;
464
+ color: var(--text-secondary);
465
+ text-transform: uppercase;
466
+ letter-spacing: 0.5px;
467
+ }
468
+
469
+ .vitals-value {
470
+ font-size: 2rem;
471
+ font-weight: bold;
472
+ margin-bottom: 5px;
473
+ }
474
+
475
+ .vitals-good {
476
+ border-color: var(--success-color);
477
+ }
478
+
479
+ .vitals-good .vitals-value {
480
+ color: var(--success-color);
481
+ }
482
+
483
+ .vitals-warning {
484
+ border-color: var(--warning-color);
485
+ }
486
+
487
+ .vitals-warning .vitals-value {
488
+ color: var(--warning-color);
489
+ }
490
+
491
+ .vitals-poor {
492
+ border-color: var(--error-color);
493
+ }
494
+
495
+ .vitals-poor .vitals-value {
496
+ color: var(--error-color);
497
+ }
498
+
499
+ .vitals-unknown {
500
+ border-color: var(--text-secondary);
501
+ }
502
+
503
+ .vitals-unknown .vitals-value {
504
+ color: var(--text-secondary);
505
+ }
506
+
507
+ .vitals-score {
508
+ display: inline-block;
509
+ padding: 4px 12px;
510
+ border-radius: 20px;
511
+ font-size: 0.8rem;
512
+ font-weight: 600;
513
+ text-transform: uppercase;
514
+ }
515
+
516
+ .vitals-score.vitals-good {
517
+ background: #d1fae5;
518
+ color: #065f46;
519
+ }
520
+
521
+ .vitals-score.vitals-warning {
522
+ background: #fef3c7;
523
+ color: #92400e;
524
+ }
525
+
526
+ .vitals-score.vitals-poor {
527
+ background: #fee2e2;
528
+ color: #991b1b;
529
+ }
530
+
531
+ .verification-metrics {
532
+ background: var(--card-background);
533
+ border-radius: 12px;
534
+ padding: 20px;
535
+ margin-top: 20px;
536
+ }
537
+
538
+ .verification-stats {
539
+ display: grid;
540
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
541
+ gap: 15px;
542
+ margin-top: 15px;
543
+ }
544
+
545
+ .verification-stat {
546
+ text-align: center;
547
+ }
548
+
549
+ .verification-stat-value {
550
+ font-size: 1.5rem;
551
+ font-weight: bold;
552
+ color: var(--primary-color);
553
+ }
554
+
555
+ .verification-stat-label {
556
+ font-size: 0.9rem;
557
+ color: var(--text-secondary);
558
+ margin-top: 5px;
559
+ }
560
+
561
+ @media (max-width: 768px) {
562
+ .vitals-cards {
563
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
564
+ gap: 15px;
565
+ }
566
+
567
+ .vitals-value {
568
+ font-size: 1.5rem;
569
+ }
570
+
571
+ .verification-stats {
572
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
573
+ gap: 10px;
574
+ }
575
+ }
576
+ </style>
577
+ </head>
578
+
579
+ <body>
580
+ <div class="container">
581
+ <!-- Header -->
582
+ <div class="header">
583
+ <button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
584
+ <span class="theme-icon" id="themeIcon">🌙</span>
585
+ <span id="themeText">Dark Mode</span>
586
+ </button>
587
+ <h1>{{testName}}</h1>
588
+ <p>Enhanced Performance Test Report • Generated on {{generatedAt}}</p>
589
+ </div>
590
+
591
+ <!-- Summary Metrics -->
592
+ <div class="summary-grid">
593
+ <div class="metric-card">
594
+ <div class="metric-value">{{summary.total_requests}}</div>
595
+ <div class="metric-label">Total Requests</div>
596
+ </div>
597
+ <div class="metric-card">
598
+ <div
599
+ 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}}">
600
+ {{toFixed summary.success_rate 2}}%
601
+ </div>
602
+ <div class="metric-label">Success Rate</div>
603
+ </div>
604
+ <div class="metric-card">
605
+ <div class="metric-value">{{toFixed summary.avg_response_time 2}}ms</div>
606
+ <div class="metric-label">Avg Response Time</div>
607
+ </div>
608
+ <div class="metric-card">
609
+ <div class="metric-value">{{toFixed summary.requests_per_second 2}}</div>
610
+ <div class="metric-label">Requests/sec</div>
611
+ </div>
612
+ <div class="metric-card">
613
+ <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>
614
+ <div class="metric-label">Virtual Users</div>
615
+ </div>
616
+ <div class="metric-card">
617
+ <div class="metric-value">{{toFixed summary.total_duration 0}}s</div>
618
+ <div class="metric-label">Total Duration</div>
619
+ </div>
620
+ </div>
621
+
622
+ <!-- NEW: Response Time Distribution -->
623
+ <div class="section">
624
+ <div class="section-header">
625
+ <h2 class="section-title">Response Time Distribution</h2>
626
+ </div>
627
+ <div class="section-content">
628
+ <div class="chart-container">
629
+ <canvas id="responseTimeDistributionChart"></canvas>
630
+ </div>
631
+ </div>
632
+ </div>
633
+
634
+ <!-- NEW: Throughput Charts -->
635
+ <div class="section">
636
+ <div class="section-header">
637
+ <h2 class="section-title">Throughput Analysis</h2>
638
+ </div>
639
+ <div class="section-content">
640
+ <div class="grid-2">
641
+ <div class="chart-container">
642
+ <canvas id="requestsPerSecondChart"></canvas>
643
+ </div>
644
+ <div class="chart-container">
645
+ <canvas id="responsesPerSecondChart"></canvas>
646
+ </div>
647
+ </div>
648
+ </div>
649
+ </div>
650
+
651
+ <!-- VU Ramp-up Chart -->
652
+ <div class="section">
653
+ <div class="section-header">
654
+ <h2 class="section-title">Virtual User Ramp-up</h2>
655
+ </div>
656
+ <div class="section-content">
657
+ <div class="chart-container">
658
+ <canvas id="vuRampupChart"></canvas>
659
+ </div>
660
+ </div>
661
+ </div>
662
+
663
+ <!-- Network Timing Analysis -->
664
+ <div class="section">
665
+ <div class="section-header">
666
+ <h2 class="section-title">Network Timing Analysis</h2>
667
+ </div>
668
+ <div class="section-content">
669
+ <div class="grid-2">
670
+ <div class="chart-container">
671
+ <canvas id="connectTimeChart"></canvas>
672
+ </div>
673
+ <div class="chart-container">
674
+ <canvas id="latencyChart"></canvas>
675
+ </div>
676
+ </div>
677
+ </div>
678
+ </div>
679
+
680
+ {{#if errorAnalysis.errorDetails}}
681
+ {{#if errorAnalysis.errorDetails.length}}
682
+ <!-- Error Details Table -->
683
+ <div class="section">
684
+ <div class="section-header">
685
+ <h2 class="section-title">Errors by Sample/Request</h2>
686
+ <div class="section-subtitle">
687
+ Total Errors: <span class="metric-error">{{errorAnalysis.totalErrors}}</span>
688
+ ({{errorAnalysis.errorRate}}% of all requests)
689
+ </div>
690
+ </div>
691
+ <div class="section-content">
692
+ <div class="table-container">
693
+ <table class="data-table">
694
+ <thead>
695
+ <tr>
696
+ <th>Sample/Request</th>
697
+ <th>Method</th>
698
+ <th>URL</th>
699
+ <th>Status</th>
700
+ <th>Error Type</th>
701
+ <th>Error Count</th>
702
+ <th>Total Requests</th>
703
+ <th>Error %</th>
704
+ </tr>
705
+ </thead>
706
+ <tbody>
707
+ {{#each errorAnalysis.errorDetails}}
708
+ <tr>
709
+ <td><strong>{{sample_name}}</strong></td>
710
+ <td>{{request_method}}</td>
711
+ <td class="url-cell" title="{{request_url}}">{{request_url}}</td>
712
+ <td><span class="status-badge status-error">{{status}}</span></td>
713
+ <td class="error-cell" title="{{error_type}}">{{error_type}}</td>
714
+ <td>{{error_count}}</td>
715
+ <td>{{total_sample_requests}}</td>
716
+ <td><strong>{{percentage}}%</strong></td>
717
+ </tr>
718
+ {{/each}}
719
+ </tbody>
720
+ </table>
721
+ </div>
722
+ </div>
723
+ </div>
724
+ {{/if}}
725
+ {{/if}}
726
+
727
+ {{#if summary.web_vitals_data}}
728
+ <!-- Core Web Vitals Section -->
729
+ <div class="section vitals-section">
730
+ <div class="section-header">
731
+ <h2 class="section-title">Core Web Vitals Performance</h2>
732
+ <span class="vitals-score {{vitalsScoreClass summary.vitals_score}}">{{summary.vitals_score}}</span>
733
+ </div>
734
+ <div class="section-content">
735
+ <div class="vitals-cards">
736
+ {{#if summary.web_vitals_data.lcp}}
737
+ <div class="vitals-card {{vitalsScoreClass summary.vitals_details.lcp.score}}">
738
+ <h3>LCP</h3>
739
+ <div class="vitals-value">{{formatVitalsMetric summary.web_vitals_data.lcp 'lcp'}}</div>
740
+ <div class="vitals-description">Largest Contentful Paint</div>
741
+ </div>
742
+ {{/if}}
743
+
744
+ {{#if summary.web_vitals_data.cls}}
745
+ <div class="vitals-card {{vitalsScoreClass summary.vitals_details.cls.score}}">
746
+ <h3>CLS</h3>
747
+ <div class="vitals-value">{{formatVitalsMetric summary.web_vitals_data.cls 'cls'}}</div>
748
+ <div class="vitals-description">Cumulative Layout Shift</div>
749
+ </div>
750
+ {{/if}}
751
+
752
+ {{#if summary.web_vitals_data.inp}}
753
+ <div class="vitals-card {{vitalsScoreClass summary.vitals_details.inp.score}}">
754
+ <h3>INP</h3>
755
+ <div class="vitals-value">{{formatVitalsMetric summary.web_vitals_data.inp 'inp'}}</div>
756
+ <div class="vitals-description">Interaction to Next Paint</div>
757
+ </div>
758
+ {{/if}}
759
+
760
+ {{#if summary.web_vitals_data.ttfb}}
761
+ <div class="vitals-card {{vitalsScoreClass summary.vitals_details.ttfb.score}}">
762
+ <h3>TTFB</h3>
763
+ <div class="vitals-value">{{formatVitalsMetric summary.web_vitals_data.ttfb 'ttfb'}}</div>
764
+ <div class="vitals-description">Time to First Byte</div>
765
+ </div>
766
+ {{/if}}
767
+
768
+ {{#if summary.web_vitals_data.fcp}}
769
+ <div class="vitals-card {{vitalsScoreClass summary.vitals_details.fcp.score}}">
770
+ <h3>FCP</h3>
771
+ <div class="vitals-value">{{formatVitalsMetric summary.web_vitals_data.fcp 'fcp'}}</div>
772
+ <div class="vitals-description">First Contentful Paint</div>
773
+ </div>
774
+ {{/if}}
775
+ </div>
776
+
777
+ <!-- Web Vitals Chart -->
778
+ <div class="chart-container">
779
+ <canvas id="webVitalsChart"></canvas>
780
+ </div>
781
+ </div>
782
+ </div>
783
+ {{/if}}
784
+
785
+ {{#if webVitalsCharts}}
786
+ <!-- Enhanced Web Vitals Analysis (Playwright Tests) -->
787
+ <div class="section">
788
+ <div class="section-header">
789
+ <h2 class="section-title">Web Vitals Detailed Analysis</h2>
790
+ </div>
791
+ <div class="section-content">
792
+ <!-- Score Distribution -->
793
+ <div class="metrics-grid">
794
+ <div class="metric-card">
795
+ <h3>Performance Score Distribution</h3>
796
+ <div class="chart-container small">
797
+ <canvas id="webVitalsScoreChart"></canvas>
798
+ </div>
799
+ </div>
800
+ </div>
801
+
802
+ <!-- Combined Web Vitals Timeline -->
803
+ <div class="section-header" style="margin-top: 30px;">
804
+ <h3>Web Vitals Timeline (All Metrics)</h3>
805
+ <p style="color: #666; font-size: 14px;">Interactive timeline showing LCP, CLS, INP, TTFB, FCP, and FID values across all URLs</p>
806
+ </div>
807
+ <div class="chart-container">
808
+ <canvas id="webVitalsTimelineChart"></canvas>
809
+ </div>
810
+
811
+ <!-- Individual Metric Charts -->
812
+ <div class="metrics-grid" style="margin-top: 30px;">
813
+ <!-- Time Series for Each Metric -->
814
+ {{#each webVitalsCharts.metrics}}
815
+ <div class="metric-card">
816
+ <h3>{{this}} Over Time</h3>
817
+ <div class="chart-container small">
818
+ <canvas id="webVitals{{this}}Chart"></canvas>
819
+ </div>
820
+ </div>
821
+ {{/each}}
822
+ </div>
823
+
824
+ <!-- Percentile Distribution -->
825
+ <div class="section-header" style="margin-top: 30px;">
826
+ <h3>Web Vitals Percentile Distribution</h3>
827
+ </div>
828
+ <div class="chart-container">
829
+ <canvas id="webVitalsPercentilesChart"></canvas>
830
+ </div>
831
+
832
+ <!-- Page-by-Page Analysis -->
833
+ {{#if webVitalsCharts.pageAnalysis}}
834
+ <div class="section-header" style="margin-top: 30px;">
835
+ <h3>Performance by Page</h3>
836
+ </div>
837
+ <div class="table-container">
838
+ <table>
839
+ <thead>
840
+ <tr>
841
+ <th>Page URL</th>
842
+ <th>Measurements</th>
843
+ <th>Avg Score</th>
844
+ <th>LCP (ms)</th>
845
+ <th>CLS</th>
846
+ <th>INP (ms)</th>
847
+ <th>TTFB (ms)</th>
848
+ <th>FCP (ms)</th>
849
+ </tr>
850
+ </thead>
851
+ <tbody>
852
+ {{#each webVitalsCharts.pageAnalysis}}
853
+ <tr>
854
+ <td>{{this.url}}</td>
855
+ <td>{{this.measurements}}</td>
856
+ <td><span class="{{vitalsScoreClass this.avgScore}}">{{this.avgScore}}</span></td>
857
+ <td>{{#if this.metrics.lcp}}{{formatVitalsMetric this.metrics.lcp.avg 'lcp'}}{{else}}-{{/if}}</td>
858
+ <td>{{#if this.metrics.cls}}{{formatVitalsMetric this.metrics.cls.avg 'cls'}}{{else}}-{{/if}}</td>
859
+ <td>{{#if this.metrics.inp}}{{formatVitalsMetric this.metrics.inp.avg 'inp'}}{{else}}-{{/if}}</td>
860
+ <td>{{#if this.metrics.ttfb}}{{formatVitalsMetric this.metrics.ttfb.avg 'ttfb'}}{{else}}-{{/if}}</td>
861
+ <td>{{#if this.metrics.fcp}}{{formatVitalsMetric this.metrics.fcp.avg 'fcp'}}{{else}}-{{/if}}</td>
862
+ </tr>
863
+ {{/each}}
864
+ </tbody>
865
+ </table>
866
+ </div>
867
+ {{/if}}
868
+ </div>
869
+ </div>
870
+ {{/if}}
871
+
872
+ {{#if summary.verification_metrics}}
873
+ <!-- Verification Metrics Section -->
874
+ <div class="section">
875
+ <div class="section-header">
876
+ <h2 class="section-title">Verification Performance</h2>
877
+ </div>
878
+ <div class="section-content">
879
+ <div class="verification-metrics">
880
+ <div class="verification-stats">
881
+ <div class="verification-stat">
882
+ <div class="verification-stat-value">{{summary.verification_metrics.total_verifications}}</div>
883
+ <div class="verification-stat-label">Total Verifications</div>
884
+ </div>
885
+ <div class="verification-stat">
886
+ <div class="verification-stat-value">{{percent summary.verification_metrics.success_rate 1}}</div>
887
+ <div class="verification-stat-label">Success Rate</div>
888
+ </div>
889
+ <div class="verification-stat">
890
+ <div class="verification-stat-value">{{formatVerificationDuration summary.verification_metrics.average_duration}}</div>
891
+ <div class="verification-stat-label">Avg Duration</div>
892
+ </div>
893
+ <div class="verification-stat">
894
+ <div class="verification-stat-value">{{formatVerificationDuration summary.verification_metrics.p95_duration}}</div>
895
+ <div class="verification-stat-label">95th Percentile</div>
896
+ </div>
897
+ </div>
898
+
899
+ <!-- Verification Performance Chart -->
900
+ <div class="chart-container" style="margin-top: 20px;">
901
+ <canvas id="verificationChart"></canvas>
902
+ </div>
903
+ </div>
904
+ </div>
905
+ </div>
906
+ {{/if}}
907
+
908
+ <!-- Response Time Analysis -->
909
+ <div class="section">
910
+ <div class="section-header">
911
+ <h2 class="section-title">Response Time Analysis</h2>
912
+ </div>
913
+ <div class="section-content">
914
+ <div class="tabs">
915
+ <button class="tab active" onclick="showTab('timeline')">Timeline</button>
916
+ <button class="tab" onclick="showTab('by-step')">By Step</button>
917
+ <button class="tab" onclick="showTab('distribution')">Distribution</button>
918
+ </div>
919
+
920
+ <div id="timeline" class="tab-content active">
921
+ <div class="chart-container large">
922
+ <canvas id="timelineChart"></canvas>
923
+ </div>
924
+ </div>
925
+
926
+ <div id="by-step" class="tab-content">
927
+ <div class="chart-container large">
928
+ <canvas id="stepResponseTimeChart"></canvas>
929
+ </div>
930
+ </div>
931
+
932
+ <div id="distribution" class="tab-content">
933
+ <div class="chart-container">
934
+ <canvas id="responseDistributionChart"></canvas>
935
+ </div>
936
+ </div>
937
+ </div>
938
+ </div>
939
+
940
+ <!-- Enhanced Step Statistics -->
941
+ <div class="section">
942
+ <div class="chart-controls" style="margin-bottom: 15px;">
943
+ <button onclick="resetZoom()" style="padding: 8px 16px; background: #2563eb; color: white; border: none; border-radius: 6px; cursor: pointer;">
944
+ Reset Zoom
945
+ </button>
946
+ </div>
947
+ <div class="chart-container">
948
+ <canvas id="stepLinesChart"></canvas>
949
+ </div>
950
+ <div class="section-header">
951
+ <h2 class="section-title">Step Performance Statistics</h2>
952
+ </div>
953
+ <div class="section-content">
954
+ <table class="step-stats-table">
955
+ <thead>
956
+ <tr>
957
+ <th>Step Name</th>
958
+ <th>Scenario</th>
959
+ <th>Requests</th>
960
+ <th>Success Rate</th>
961
+ <th>Min (ms)</th>
962
+ <th>Avg (ms)</th>
963
+ <th>Median (ms)</th>
964
+ <th>Max (ms)</th>
965
+ <th>P90</th>
966
+ <th>P95</th>
967
+ <th>P99</th>
968
+ <th>P99.9</th>
969
+ <th>P99.99</th>
970
+ <th>Status</th>
971
+ </tr>
972
+ </thead>
973
+ <tbody>
974
+ {{#each stepStatistics}}
975
+ <tr>
976
+ <td><strong>{{step_name}}</strong></td>
977
+ <td>{{scenario}}</td>
978
+ <td>{{total_requests}}</td>
979
+ <td>{{toFixed success_rate 1}}%</td>
980
+ <td>{{toFixed min_response_time 1}}</td>
981
+ <td>{{toFixed avg_response_time 1}}</td>
982
+ <td>{{lookup percentiles 50}}</td>
983
+ <td>{{toFixed max_response_time 1}}</td>
984
+ <td>{{lookup percentiles 90}}</td>
985
+ <td>{{lookup percentiles 95}}</td>
986
+ <td>{{lookup percentiles 99}}</td>
987
+ <td>{{lookup percentiles 99.9}}</td>
988
+ <td>{{lookup percentiles 99.99}}</td>
989
+ <td>
990
+ <span
991
+ class="status-badge {{#if (gt success_rate 95)}}status-success{{else}}{{#if (gt success_rate 90)}}status-warning{{else}}status-error{{/if}}{{/if}}">
992
+ {{#if (gt success_rate 95)}}Good{{else}}{{#if (gt success_rate
993
+ 90)}}Warning{{else}}Error{{/if}}{{/if}}
994
+ </span>
995
+ </td>
996
+ </tr>
997
+ {{/each}}
998
+ </tbody>
999
+ </table>
1000
+ </div>
1001
+ </div>
1002
+
1003
+ <!-- Step Percentiles Comparison -->
1004
+ <div class="section">
1005
+ <div class="section-header">
1006
+ <h2 class="section-title">Step Percentile Comparison</h2>
1007
+ </div>
1008
+ <div class="section-content">
1009
+ <div class="grid-2">
1010
+ <div class="chart-container">
1011
+ <canvas id="stepPercentilesChart"></canvas>
1012
+ </div>
1013
+ <div class="chart-container">
1014
+ <canvas id="stepThroughputChart"></canvas>
1015
+ </div>
1016
+ </div>
1017
+ </div>
1018
+ </div>
1019
+
1020
+ <!-- Performance Timeline -->
1021
+ <div class="section">
1022
+ <div class="section-header">
1023
+ <h2 class="section-title">Performance Timeline</h2>
1024
+ </div>
1025
+ <div class="section-content">
1026
+ <div class="chart-container large">
1027
+ <canvas id="performanceTimelineChart"></canvas>
1028
+ </div>
1029
+ </div>
1030
+ </div>
1031
+
1032
+ <!-- Footer -->
1033
+ <div class="footer">
1034
+ <p>Generated by Perfornium Performance Testing Framework</p>
1035
+ </div>
1036
+ </div>
1037
+
1038
+ <script>
1039
+ // Theme toggle functionality
1040
+ function toggleTheme() {
1041
+ const html = document.documentElement;
1042
+ const currentTheme = html.getAttribute('data-theme');
1043
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
1044
+
1045
+ html.setAttribute('data-theme', newTheme);
1046
+ localStorage.setItem('theme', newTheme);
1047
+
1048
+ // Update button text and icon
1049
+ const themeIcon = document.getElementById('themeIcon');
1050
+ const themeText = document.getElementById('themeText');
1051
+
1052
+ if (newTheme === 'dark') {
1053
+ themeIcon.textContent = '☀️';
1054
+ themeText.textContent = 'Light Mode';
1055
+ } else {
1056
+ themeIcon.textContent = '🌙';
1057
+ themeText.textContent = 'Dark Mode';
1058
+ }
1059
+ }
1060
+
1061
+ // Load theme from localStorage on page load
1062
+ (function() {
1063
+ const savedTheme = localStorage.getItem('theme') || 'light';
1064
+ const html = document.documentElement;
1065
+ html.setAttribute('data-theme', savedTheme);
1066
+
1067
+ // Update button on load
1068
+ const themeIcon = document.getElementById('themeIcon');
1069
+ const themeText = document.getElementById('themeText');
1070
+
1071
+ if (savedTheme === 'dark') {
1072
+ themeIcon.textContent = '☀️';
1073
+ themeText.textContent = 'Light Mode';
1074
+ }
1075
+ })();
1076
+
1077
+ // Chart data from server
1078
+ const summaryData = {{{ summaryData }}};
1079
+ const stepStatistics = {{{ stepStatisticsData }}};
1080
+ const vuRampupData = {{{ vuRampupData }}};
1081
+ const timelineData = {{{ timelineData }}};
1082
+ const responseTimeDistributionData = {{{ responseTimeDistributionData }}};
1083
+ const requestsPerSecondData = {{{ requestsPerSecondData }}};
1084
+ const responsesPerSecondData = {{{ responsesPerSecondData }}};
1085
+ const connectTimeData = {{{ connectTimeData }}};
1086
+ const latencyData = {{{ latencyData }}};
1087
+
1088
+ // Tab functionality
1089
+ function showTab(tabName) {
1090
+ // Hide all tab contents
1091
+ document.querySelectorAll('.tab-content').forEach(content => {
1092
+ content.classList.remove('active');
1093
+ });
1094
+
1095
+ // Remove active from all tabs
1096
+ document.querySelectorAll('.tab').forEach(tab => {
1097
+ tab.classList.remove('active');
1098
+ });
1099
+
1100
+ // Show selected tab content
1101
+ document.getElementById(tabName).classList.add('active');
1102
+
1103
+ // Add active to clicked tab
1104
+ event.target.classList.add('active');
1105
+ }
1106
+
1107
+ // NEW: Response Time Distribution Chart
1108
+ const responseDistCtx = document.getElementById('responseTimeDistributionChart').getContext('2d');
1109
+ new Chart(responseDistCtx, {
1110
+ type: 'bar',
1111
+ data: {
1112
+ labels: responseTimeDistributionData.map(d => d.bucket),
1113
+ datasets: [{
1114
+ label: 'Request Count',
1115
+ data: responseTimeDistributionData.map(d => d.count),
1116
+ backgroundColor: 'rgba(37, 99, 235, 0.6)',
1117
+ borderColor: 'rgba(37, 99, 235, 1)',
1118
+ borderWidth: 1
1119
+ }, {
1120
+ label: 'Percentage',
1121
+ data: responseTimeDistributionData.map(d => d.percentage),
1122
+ type: 'line',
1123
+ borderColor: 'rgba(239, 68, 68, 1)',
1124
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
1125
+ yAxisID: 'y1',
1126
+ tension: 0.4
1127
+ }]
1128
+ },
1129
+ options: {
1130
+ responsive: true,
1131
+ maintainAspectRatio: false,
1132
+ plugins: {
1133
+ title: {
1134
+ display: true,
1135
+ text: 'Overall Response Time Distribution'
1136
+ }
1137
+ },
1138
+ scales: {
1139
+ y: {
1140
+ beginAtZero: true,
1141
+ title: {
1142
+ display: true,
1143
+ text: 'Number of Requests'
1144
+ }
1145
+ },
1146
+ y1: {
1147
+ type: 'linear',
1148
+ display: true,
1149
+ position: 'right',
1150
+ title: {
1151
+ display: true,
1152
+ text: 'Percentage (%)'
1153
+ },
1154
+ grid: {
1155
+ drawOnChartArea: false,
1156
+ },
1157
+ min: 0,
1158
+ max: 100
1159
+ },
1160
+ x: {
1161
+ title: {
1162
+ display: true,
1163
+ text: 'Response Time Range'
1164
+ }
1165
+ }
1166
+ }
1167
+ }
1168
+ });
1169
+
1170
+ // NEW: Requests Per Second Chart
1171
+ const requestsPerSecCtx = document.getElementById('requestsPerSecondChart').getContext('2d');
1172
+ new Chart(requestsPerSecCtx, {
1173
+ type: 'line',
1174
+ data: {
1175
+ datasets: [{
1176
+ label: 'Total Requests/sec',
1177
+ data: requestsPerSecondData.map(d => ({
1178
+ x: new Date(d.timestamp),
1179
+ y: d.requests_per_second
1180
+ })),
1181
+ borderColor: '#2563eb',
1182
+ backgroundColor: 'rgba(37, 99, 235, 0.1)',
1183
+ fill: true,
1184
+ tension: 0.4
1185
+ }, {
1186
+ label: 'Successful Requests/sec',
1187
+ data: requestsPerSecondData.map(d => ({
1188
+ x: new Date(d.timestamp),
1189
+ y: d.successful_requests_per_second
1190
+ })),
1191
+ borderColor: '#10b981',
1192
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
1193
+ fill: true,
1194
+ tension: 0.4
1195
+ }]
1196
+ },
1197
+ options: {
1198
+ responsive: true,
1199
+ maintainAspectRatio: false,
1200
+ plugins: {
1201
+ title: {
1202
+ display: true,
1203
+ text: 'Requests Per Second Over Time'
1204
+ }
1205
+ },
1206
+ scales: {
1207
+ x: {
1208
+ type: 'time',
1209
+ time: {
1210
+ displayFormats: {
1211
+ second: 'HH:mm:ss'
1212
+ }
1213
+ },
1214
+ title: {
1215
+ display: true,
1216
+ text: 'Time'
1217
+ }
1218
+ },
1219
+ y: {
1220
+ beginAtZero: true,
1221
+ title: {
1222
+ display: true,
1223
+ text: 'Requests/Second'
1224
+ }
1225
+ }
1226
+ }
1227
+ }
1228
+ });
1229
+
1230
+ // NEW: Responses Per Second Chart
1231
+ const responsesPerSecCtx = document.getElementById('responsesPerSecondChart').getContext('2d');
1232
+ new Chart(responsesPerSecCtx, {
1233
+ type: 'line',
1234
+ data: {
1235
+ datasets: [{
1236
+ label: 'Successful Responses/sec',
1237
+ data: responsesPerSecondData.map(d => ({
1238
+ x: new Date(d.timestamp),
1239
+ y: d.responses_per_second
1240
+ })),
1241
+ borderColor: '#10b981',
1242
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
1243
+ fill: true,
1244
+ tension: 0.4
1245
+ }, {
1246
+ label: 'Error Responses/sec',
1247
+ data: responsesPerSecondData.map(d => ({
1248
+ x: new Date(d.timestamp),
1249
+ y: d.error_responses_per_second
1250
+ })),
1251
+ borderColor: '#ef4444',
1252
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
1253
+ fill: true,
1254
+ tension: 0.4
1255
+ }]
1256
+ },
1257
+ options: {
1258
+ responsive: true,
1259
+ maintainAspectRatio: false,
1260
+ plugins: {
1261
+ title: {
1262
+ display: true,
1263
+ text: 'Responses Per Second Over Time'
1264
+ }
1265
+ },
1266
+ scales: {
1267
+ x: {
1268
+ type: 'time',
1269
+ time: {
1270
+ displayFormats: {
1271
+ second: 'HH:mm:ss'
1272
+ }
1273
+ },
1274
+ title: {
1275
+ display: true,
1276
+ text: 'Time'
1277
+ }
1278
+ },
1279
+ y: {
1280
+ beginAtZero: true,
1281
+ title: {
1282
+ display: true,
1283
+ text: 'Responses/Second'
1284
+ }
1285
+ }
1286
+ }
1287
+ }
1288
+ });
1289
+
1290
+ // VU Ramp-up Chart - uses vuRampupData for accurate ramp-up visualization
1291
+ const vuRampupCtx = document.getElementById('vuRampupChart').getContext('2d');
1292
+ new Chart(vuRampupCtx, {
1293
+ type: 'line',
1294
+ data: {
1295
+ datasets: [{
1296
+ label: 'Active Virtual Users',
1297
+ data: vuRampupData.map(d => ({
1298
+ x: new Date(d.timestamp),
1299
+ y: d.count
1300
+ })),
1301
+ borderColor: '#2563eb',
1302
+ backgroundColor: 'rgba(37, 99, 235, 0.1)',
1303
+ fill: true,
1304
+ tension: 0.1,
1305
+ stepped: true
1306
+ }]
1307
+ },
1308
+ options: {
1309
+ responsive: true,
1310
+ maintainAspectRatio: false,
1311
+ plugins: {
1312
+ title: {
1313
+ display: true,
1314
+ text: 'Virtual User Ramp-up Pattern'
1315
+ }
1316
+ },
1317
+ scales: {
1318
+ x: {
1319
+ type: 'time',
1320
+ time: {
1321
+ displayFormats: {
1322
+ second: 'HH:mm:ss'
1323
+ }
1324
+ },
1325
+ title: {
1326
+ display: true,
1327
+ text: 'Time'
1328
+ }
1329
+ },
1330
+ y: {
1331
+ beginAtZero: true,
1332
+ title: {
1333
+ display: true,
1334
+ text: 'Active Virtual Users'
1335
+ }
1336
+ }
1337
+ }
1338
+ }
1339
+ });
1340
+
1341
+ // Timeline Chart
1342
+ const timelineCtx = document.getElementById('timelineChart').getContext('2d');
1343
+ new Chart(timelineCtx, {
1344
+ type: 'line',
1345
+ data: {
1346
+ datasets: [
1347
+ {
1348
+ label: 'Avg Response Time (ms)',
1349
+ data: timelineData.map(d => ({
1350
+ x: new Date(d.timestamp),
1351
+ y: d.avg_response_time
1352
+ })),
1353
+ borderColor: '#10b981',
1354
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
1355
+ yAxisID: 'y',
1356
+ tension: 0.4
1357
+ },
1358
+ {
1359
+ label: 'Success Rate (%)',
1360
+ data: timelineData.map(d => ({
1361
+ x: new Date(d.timestamp),
1362
+ y: d.success_rate
1363
+ })),
1364
+ borderColor: '#f59e0b',
1365
+ backgroundColor: 'rgba(245, 158, 11, 0.1)',
1366
+ yAxisID: 'y1',
1367
+ tension: 0.4
1368
+ }
1369
+ ]
1370
+ },
1371
+ options: {
1372
+ responsive: true,
1373
+ maintainAspectRatio: false,
1374
+ plugins: {
1375
+ title: {
1376
+ display: true,
1377
+ text: 'Performance Timeline'
1378
+ }
1379
+ },
1380
+ scales: {
1381
+ x: {
1382
+ type: 'time',
1383
+ time: {
1384
+ displayFormats: {
1385
+ second: 'HH:mm:ss'
1386
+ }
1387
+ }
1388
+ },
1389
+ y: {
1390
+ type: 'linear',
1391
+ display: true,
1392
+ position: 'left',
1393
+ title: {
1394
+ display: true,
1395
+ text: 'Response Time (ms)'
1396
+ }
1397
+ },
1398
+ y1: {
1399
+ type: 'linear',
1400
+ display: true,
1401
+ position: 'right',
1402
+ title: {
1403
+ display: true,
1404
+ text: 'Success Rate (%)'
1405
+ },
1406
+ grid: {
1407
+ drawOnChartArea: false,
1408
+ },
1409
+ min: 0,
1410
+ max: 100
1411
+ }
1412
+ }
1413
+ }
1414
+ });
1415
+
1416
+ // Step Response Time Chart
1417
+ const stepResponseCtx = document.getElementById('stepResponseTimeChart').getContext('2d');
1418
+ new Chart(stepResponseCtx, {
1419
+ type: 'bar',
1420
+ data: {
1421
+ labels: stepStatistics.map(s => s.step_name),
1422
+ datasets: [
1423
+ {
1424
+ label: 'P50',
1425
+ data: stepStatistics.map(s => s.percentiles[50] || 0),
1426
+ backgroundColor: 'rgba(37, 99, 235, 0.6)'
1427
+ },
1428
+ {
1429
+ label: 'P90',
1430
+ data: stepStatistics.map(s => s.percentiles[90] || 0),
1431
+ backgroundColor: 'rgba(16, 185, 129, 0.6)'
1432
+ },
1433
+ {
1434
+ label: 'P95',
1435
+ data: stepStatistics.map(s => s.percentiles[95] || 0),
1436
+ backgroundColor: 'rgba(245, 158, 11, 0.6)'
1437
+ },
1438
+ {
1439
+ label: 'P99',
1440
+ data: stepStatistics.map(s => s.percentiles[99] || 0),
1441
+ backgroundColor: 'rgba(239, 68, 68, 0.6)'
1442
+ }
1443
+ ]
1444
+ },
1445
+ options: {
1446
+ responsive: true,
1447
+ maintainAspectRatio: false,
1448
+ plugins: {
1449
+ title: {
1450
+ display: true,
1451
+ text: 'Response Time Percentiles by Step'
1452
+ }
1453
+ },
1454
+ scales: {
1455
+ y: {
1456
+ beginAtZero: true,
1457
+ title: {
1458
+ display: true,
1459
+ text: 'Response Time (ms)'
1460
+ }
1461
+ }
1462
+ }
1463
+ }
1464
+ });
1465
+
1466
+ // Step Percentiles Chart - use horizontal bar for better readability
1467
+ const stepPercentilesCtx = document.getElementById('stepPercentilesChart').getContext('2d');
1468
+ if (stepStatistics && stepStatistics.length > 0) {
1469
+ // Sort by P95 to show slowest steps first
1470
+ const sortedSteps = [...stepStatistics].sort((a, b) =>
1471
+ (b.percentiles[95] || 0) - (a.percentiles[95] || 0)
1472
+ ).slice(0, 8); // Top 8 slowest steps
1473
+
1474
+ new Chart(stepPercentilesCtx, {
1475
+ type: 'bar',
1476
+ data: {
1477
+ labels: sortedSteps.map(s => s.step_name),
1478
+ datasets: [
1479
+ {
1480
+ label: 'P50',
1481
+ data: sortedSteps.map(s => s.percentiles[50] || 0),
1482
+ backgroundColor: 'rgba(37, 99, 235, 0.7)'
1483
+ },
1484
+ {
1485
+ label: 'P95',
1486
+ data: sortedSteps.map(s => s.percentiles[95] || 0),
1487
+ backgroundColor: 'rgba(245, 158, 11, 0.7)'
1488
+ },
1489
+ {
1490
+ label: 'P99',
1491
+ data: sortedSteps.map(s => s.percentiles[99] || 0),
1492
+ backgroundColor: 'rgba(239, 68, 68, 0.7)'
1493
+ }
1494
+ ]
1495
+ },
1496
+ options: {
1497
+ indexAxis: 'y',
1498
+ responsive: true,
1499
+ maintainAspectRatio: false,
1500
+ plugins: {
1501
+ title: {
1502
+ display: true,
1503
+ text: 'Response Time Percentiles (Slowest Steps)'
1504
+ }
1505
+ },
1506
+ scales: {
1507
+ x: {
1508
+ beginAtZero: true,
1509
+ title: {
1510
+ display: true,
1511
+ text: 'Response Time (ms)'
1512
+ }
1513
+ }
1514
+ }
1515
+ }
1516
+ });
1517
+ }
1518
+
1519
+ // Step Throughput Chart
1520
+ const stepThroughputCtx = document.getElementById('stepThroughputChart').getContext('2d');
1521
+ if (stepStatistics && stepStatistics.length > 0) {
1522
+ new Chart(stepThroughputCtx, {
1523
+ type: 'doughnut',
1524
+ data: {
1525
+ labels: stepStatistics.map(s => s.step_name),
1526
+ datasets: [{
1527
+ data: stepStatistics.map(s => s.total_requests),
1528
+ backgroundColor: stepStatistics.map((_, index) =>
1529
+ `hsl(${index * 137.5 % 360}, 70%, 50%)`
1530
+ )
1531
+ }]
1532
+ },
1533
+ options: {
1534
+ responsive: true,
1535
+ maintainAspectRatio: false,
1536
+ plugins: {
1537
+ title: {
1538
+ display: true,
1539
+ text: 'Request Distribution by Step'
1540
+ },
1541
+ tooltip: {
1542
+ callbacks: {
1543
+ label: function(context) {
1544
+ const total = context.dataset.data.reduce((a, b) => a + b, 0);
1545
+ const value = context.raw;
1546
+ const percentage = ((value / total) * 100).toFixed(1);
1547
+ return `${context.label}: ${value} requests (${percentage}%)`;
1548
+ }
1549
+ }
1550
+ }
1551
+ }
1552
+ }
1553
+ });
1554
+ }
1555
+
1556
+ let stepLinesChart;
1557
+ const stepResponseTimes = {{{stepResponseTimesData}}};
1558
+ const stepLinesCtx = document.getElementById('stepLinesChart').getContext('2d');
1559
+
1560
+ function resetZoom() {
1561
+ if (stepLinesChart) {
1562
+ stepLinesChart.resetZoom();
1563
+ }
1564
+ }
1565
+
1566
+ // Create datasets - one line per step with ALL individual response times
1567
+ // Use test start time from timeline data for fallback timestamps
1568
+ const testStartTime = timelineData.length > 0 ? new Date(timelineData[0].timestamp).getTime() : Date.now();
1569
+
1570
+ const datasets = (stepResponseTimes || []).filter(step => step.response_times && step.response_times.length > 0).map((step, index) => {
1571
+ // Use timeline_data if available, otherwise create timestamps relative to test start
1572
+ const timelineDataPoints = step.timeline_data || step.response_times.map((responseTime, idx) => ({
1573
+ duration: responseTime,
1574
+ timestamp: testStartTime + (idx * 100), // 100ms intervals from test start
1575
+ vu_id: Math.floor(idx / 10) + 1,
1576
+ iteration: (idx % 10) + 1
1577
+ }));
1578
+
1579
+ return {
1580
+ label: step.step_name,
1581
+ data: timelineDataPoints.map(point => ({
1582
+ x: new Date(point.timestamp),
1583
+ y: point.duration
1584
+ })),
1585
+ borderColor: `hsl(${index * 360 / Math.max(stepResponseTimes.length, 1)}, 70%, 50%)`,
1586
+ backgroundColor: `hsla(${index * 360 / Math.max(stepResponseTimes.length, 1)}, 70%, 50%, 0.1)`,
1587
+ fill: false,
1588
+ tension: 0,
1589
+ pointRadius: 2,
1590
+ pointHoverRadius: 4,
1591
+ borderWidth: 2,
1592
+ pointBackgroundColor: `hsl(${index * 360 / Math.max(stepResponseTimes.length, 1)}, 70%, 50%)`,
1593
+ pointBorderColor: `hsl(${index * 360 / Math.max(stepResponseTimes.length, 1)}, 70%, 40%)`
1594
+ };
1595
+ });
1596
+
1597
+ if (datasets.length > 0) {
1598
+ stepLinesChart = new Chart(stepLinesCtx, {
1599
+ type: 'line',
1600
+ data: {
1601
+ datasets: datasets
1602
+ },
1603
+ options: {
1604
+ responsive: true,
1605
+ maintainAspectRatio: false,
1606
+ plugins: {
1607
+ title: {
1608
+ display: true,
1609
+ text: 'Response Times by Step - All Individual Results'
1610
+ },
1611
+ legend: {
1612
+ position: 'top',
1613
+ labels: {
1614
+ usePointStyle: true,
1615
+ boxWidth: 6
1616
+ }
1617
+ },
1618
+ tooltip: {
1619
+ mode: 'point',
1620
+ intersect: false,
1621
+ callbacks: {
1622
+ label: function(context) {
1623
+ const step = stepResponseTimes[context.datasetIndex];
1624
+ const timelineData = step.timeline_data || [];
1625
+ const dataPoint = timelineData[context.dataIndex];
1626
+
1627
+ if (dataPoint) {
1628
+ return [
1629
+ `${step.step_name}: ${context.parsed.y}ms`,
1630
+ `VU: ${dataPoint.vu_id}`,
1631
+ `Iteration: ${dataPoint.iteration}`
1632
+ ];
1633
+ } else {
1634
+ return `${step.step_name}: ${context.parsed.y}ms`;
1635
+ }
1636
+ },
1637
+ title: function(context) {
1638
+ return new Date(context[0].parsed.x).toLocaleTimeString();
1639
+ }
1640
+ }
1641
+ },
1642
+ zoom: {
1643
+ limits: {
1644
+ x: {min: 'original', max: 'original'},
1645
+ y: {min: 0, max: 'original'}
1646
+ },
1647
+ pan: {
1648
+ enabled: true,
1649
+ mode: 'xy',
1650
+ modifierKey: null,
1651
+ threshold: 10
1652
+ },
1653
+ zoom: {
1654
+ wheel: {
1655
+ enabled: true,
1656
+ speed: 0.1,
1657
+ modifierKey: null
1658
+ },
1659
+ pinch: {
1660
+ enabled: true
1661
+ },
1662
+ drag: {
1663
+ enabled: true,
1664
+ backgroundColor: 'rgba(37, 99, 235, 0.2)',
1665
+ borderColor: 'rgba(37, 99, 235, 0.8)',
1666
+ borderWidth: 2,
1667
+ threshold: 10,
1668
+ modifierKey: null
1669
+ },
1670
+ mode: 'xy'
1671
+ }
1672
+ }
1673
+ },
1674
+ scales: {
1675
+ x: {
1676
+ type: 'time',
1677
+ time: {
1678
+ displayFormats: {
1679
+ second: 'HH:mm:ss',
1680
+ minute: 'HH:mm'
1681
+ }
1682
+ },
1683
+ title: {
1684
+ display: true,
1685
+ text: 'Test Timeline'
1686
+ }
1687
+ },
1688
+ y: {
1689
+ beginAtZero: true,
1690
+ title: {
1691
+ display: true,
1692
+ text: 'Response Time (ms)'
1693
+ }
1694
+ }
1695
+ },
1696
+ interaction: {
1697
+ mode: 'point',
1698
+ intersect: false,
1699
+ },
1700
+ elements: {
1701
+ point: {
1702
+ hoverRadius: 6
1703
+ }
1704
+ },
1705
+ animation: {
1706
+ duration: 0 // Disable animation for better performance with many points
1707
+ }
1708
+ }
1709
+ });
1710
+ }
1711
+
1712
+ // Performance Timeline Chart
1713
+ const performanceTimelineCtx = document.getElementById('performanceTimelineChart').getContext('2d');
1714
+ if (timelineData && timelineData.length > 0) {
1715
+ new Chart(performanceTimelineCtx, {
1716
+ type: 'line',
1717
+ data: {
1718
+ datasets: [
1719
+ {
1720
+ label: 'Active VUs',
1721
+ data: timelineData.map(d => ({
1722
+ x: new Date(d.timestamp),
1723
+ y: d.active_vus || 0
1724
+ })),
1725
+ borderColor: '#8b5cf6',
1726
+ backgroundColor: 'rgba(139, 92, 246, 0.1)',
1727
+ yAxisID: 'y2',
1728
+ tension: 0.1
1729
+ },
1730
+ {
1731
+ label: 'Throughput (req/s)',
1732
+ data: timelineData.map(d => ({
1733
+ x: new Date(d.timestamp),
1734
+ y: d.throughput
1735
+ })),
1736
+ borderColor: '#06b6d4',
1737
+ backgroundColor: 'rgba(6, 182, 212, 0.1)',
1738
+ yAxisID: 'y',
1739
+ tension: 0.4
1740
+ },
1741
+ {
1742
+ label: 'Avg Response Time',
1743
+ data: timelineData.map(d => ({
1744
+ x: new Date(d.timestamp),
1745
+ y: d.avg_response_time
1746
+ })),
1747
+ borderColor: '#ef4444',
1748
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
1749
+ yAxisID: 'y1',
1750
+ tension: 0.4
1751
+ }
1752
+ ]
1753
+ },
1754
+ options: {
1755
+ responsive: true,
1756
+ maintainAspectRatio: false,
1757
+ plugins: {
1758
+ title: {
1759
+ display: true,
1760
+ text: 'Complete Performance Timeline'
1761
+ }
1762
+ },
1763
+ scales: {
1764
+ x: {
1765
+ type: 'time',
1766
+ time: {
1767
+ displayFormats: {
1768
+ second: 'HH:mm:ss'
1769
+ }
1770
+ }
1771
+ },
1772
+ y: {
1773
+ type: 'linear',
1774
+ display: true,
1775
+ position: 'left',
1776
+ title: {
1777
+ display: true,
1778
+ text: 'Throughput (req/s)'
1779
+ }
1780
+ },
1781
+ y1: {
1782
+ type: 'linear',
1783
+ display: true,
1784
+ position: 'right',
1785
+ title: {
1786
+ display: true,
1787
+ text: 'Response Time (ms)'
1788
+ },
1789
+ grid: {
1790
+ drawOnChartArea: false,
1791
+ }
1792
+ },
1793
+ y2: {
1794
+ type: 'linear',
1795
+ display: true,
1796
+ position: 'right',
1797
+ title: {
1798
+ display: true,
1799
+ text: 'Active VUs'
1800
+ },
1801
+ grid: {
1802
+ drawOnChartArea: false,
1803
+ },
1804
+ beginAtZero: true
1805
+ }
1806
+ }
1807
+ }
1808
+ });
1809
+ }
1810
+
1811
+ // Response Distribution Chart (in tab)
1812
+ const responseDistributionCtx = document.getElementById('responseDistributionChart').getContext('2d');
1813
+
1814
+ // Calculate response time distribution
1815
+ const responseTimes = [];
1816
+ stepStatistics.forEach(step => {
1817
+ step.response_times.forEach(time => {
1818
+ responseTimes.push(time);
1819
+ });
1820
+ });
1821
+
1822
+ // Create buckets for histogram
1823
+ const buckets = 10;
1824
+ const min = Math.min(...responseTimes);
1825
+ const max = Math.max(...responseTimes);
1826
+ const bucketSize = (max - min) / buckets;
1827
+ const distribution = [];
1828
+
1829
+ for (let i = 0; i < buckets; i++) {
1830
+ const bucketStart = min + (i * bucketSize);
1831
+ const bucketEnd = min + ((i + 1) * bucketSize);
1832
+ const count = responseTimes.filter(time =>
1833
+ time >= bucketStart && (i === buckets - 1 ? time <= bucketEnd : time < bucketEnd)
1834
+ ).length;
1835
+
1836
+ distribution.push({
1837
+ bucket: `${bucketStart.toFixed(0)}-${bucketEnd.toFixed(0)}ms`,
1838
+ count
1839
+ });
1840
+ }
1841
+
1842
+ new Chart(responseDistributionCtx, {
1843
+ type: 'bar',
1844
+ data: {
1845
+ labels: distribution.map(d => d.bucket),
1846
+ datasets: [{
1847
+ label: 'Request Count',
1848
+ data: distribution.map(d => d.count),
1849
+ backgroundColor: 'rgba(37, 99, 235, 0.6)',
1850
+ borderColor: 'rgba(37, 99, 235, 1)',
1851
+ borderWidth: 1
1852
+ }]
1853
+ },
1854
+ options: {
1855
+ responsive: true,
1856
+ maintainAspectRatio: false,
1857
+ plugins: {
1858
+ title: {
1859
+ display: true,
1860
+ text: 'Response Time Distribution'
1861
+ }
1862
+ },
1863
+ scales: {
1864
+ y: {
1865
+ beginAtZero: true,
1866
+ title: {
1867
+ display: true,
1868
+ text: 'Number of Requests'
1869
+ }
1870
+ },
1871
+ x: {
1872
+ title: {
1873
+ display: true,
1874
+ text: 'Response Time Range'
1875
+ }
1876
+ }
1877
+ }
1878
+ }
1879
+ });
1880
+
1881
+ // Core Web Vitals Chart
1882
+ {{#if summary.web_vitals_data}}
1883
+ const webVitalsCtx = document.getElementById('webVitalsChart')?.getContext('2d');
1884
+ if (webVitalsCtx) {
1885
+ const vitalsData = {
1886
+ labels: [],
1887
+ values: [],
1888
+ thresholds: [],
1889
+ colors: []
1890
+ };
1891
+
1892
+ {{#if summary.web_vitals_data.lcp}}
1893
+ vitalsData.labels.push('LCP');
1894
+ vitalsData.values.push({{summary.web_vitals_data.lcp}});
1895
+ vitalsData.thresholds.push([2500, 4000]);
1896
+ vitalsData.colors.push('{{#ifEquals summary.vitals_details.lcp.score "good"}}#10b981{{else}}{{#ifEquals summary.vitals_details.lcp.score "needs-improvement"}}#f59e0b{{else}}#ef4444{{/ifEquals}}{{/ifEquals}}');
1897
+ {{/if}}
1898
+
1899
+ {{#if summary.web_vitals_data.cls}}
1900
+ vitalsData.labels.push('CLS');
1901
+ vitalsData.values.push({{summary.web_vitals_data.cls}});
1902
+ vitalsData.thresholds.push([0.1, 0.25]);
1903
+ vitalsData.colors.push('{{#ifEquals summary.vitals_details.cls.score "good"}}#10b981{{else}}{{#ifEquals summary.vitals_details.cls.score "needs-improvement"}}#f59e0b{{else}}#ef4444{{/ifEquals}}{{/ifEquals}}');
1904
+ {{/if}}
1905
+
1906
+ {{#if summary.web_vitals_data.inp}}
1907
+ vitalsData.labels.push('INP');
1908
+ vitalsData.values.push({{summary.web_vitals_data.inp}});
1909
+ vitalsData.thresholds.push([200, 500]);
1910
+ vitalsData.colors.push('{{#ifEquals summary.vitals_details.inp.score "good"}}#10b981{{else}}{{#ifEquals summary.vitals_details.inp.score "needs-improvement"}}#f59e0b{{else}}#ef4444{{/ifEquals}}{{/ifEquals}}');
1911
+ {{/if}}
1912
+
1913
+ {{#if summary.web_vitals_data.fid}}
1914
+ vitalsData.labels.push('FID (deprecated)');
1915
+ vitalsData.values.push({{summary.web_vitals_data.fid}});
1916
+ vitalsData.thresholds.push([100, 300]);
1917
+ vitalsData.colors.push('{{#ifEquals summary.vitals_details.fid.score "good"}}#10b981{{else}}{{#ifEquals summary.vitals_details.fid.score "needs-improvement"}}#f59e0b{{else}}#ef4444{{/ifEquals}}{{/ifEquals}}');
1918
+ {{/if}}
1919
+
1920
+ {{#if summary.web_vitals_data.fcp}}
1921
+ vitalsData.labels.push('FCP');
1922
+ vitalsData.values.push({{summary.web_vitals_data.fcp}});
1923
+ vitalsData.thresholds.push([1800, 3000]);
1924
+ vitalsData.colors.push('{{#ifEquals summary.vitals_details.fcp.score "good"}}#10b981{{else}}{{#ifEquals summary.vitals_details.fcp.score "needs-improvement"}}#f59e0b{{else}}#ef4444{{/ifEquals}}{{/ifEquals}}');
1925
+ {{/if}}
1926
+
1927
+ {{#if summary.web_vitals_data.ttfb}}
1928
+ vitalsData.labels.push('TTFB');
1929
+ vitalsData.values.push({{summary.web_vitals_data.ttfb}});
1930
+ vitalsData.thresholds.push([800, 1800]);
1931
+ vitalsData.colors.push('{{#ifEquals summary.vitals_details.ttfb.score "good"}}#10b981{{else}}{{#ifEquals summary.vitals_details.ttfb.score "needs-improvement"}}#f59e0b{{else}}#ef4444{{/ifEquals}}{{/ifEquals}}');
1932
+ {{/if}}
1933
+
1934
+ new Chart(webVitalsCtx, {
1935
+ type: 'bar',
1936
+ data: {
1937
+ labels: vitalsData.labels,
1938
+ datasets: [{
1939
+ label: 'Measured Value',
1940
+ data: vitalsData.values,
1941
+ backgroundColor: vitalsData.colors,
1942
+ borderColor: vitalsData.colors,
1943
+ borderWidth: 2
1944
+ }]
1945
+ },
1946
+ options: {
1947
+ responsive: true,
1948
+ maintainAspectRatio: false,
1949
+ plugins: {
1950
+ title: {
1951
+ display: true,
1952
+ text: 'Core Web Vitals Performance',
1953
+ font: { size: 16 }
1954
+ },
1955
+ tooltip: {
1956
+ callbacks: {
1957
+ afterLabel: function(context) {
1958
+ const thresholds = vitalsData.thresholds[context.dataIndex];
1959
+ if (thresholds) {
1960
+ return [
1961
+ `Good: ≤ ${thresholds[0]}${context.label === 'CLS' ? '' : 'ms'}`,
1962
+ `Poor: > ${thresholds[1]}${context.label === 'CLS' ? '' : 'ms'}`
1963
+ ];
1964
+ }
1965
+ return '';
1966
+ }
1967
+ }
1968
+ }
1969
+ },
1970
+ scales: {
1971
+ y: {
1972
+ beginAtZero: true,
1973
+ title: {
1974
+ display: true,
1975
+ text: 'Value (ms / ratio)'
1976
+ }
1977
+ },
1978
+ x: {
1979
+ title: {
1980
+ display: true,
1981
+ text: 'Core Web Vitals Metrics'
1982
+ }
1983
+ }
1984
+ }
1985
+ }
1986
+ });
1987
+ }
1988
+ {{/if}}
1989
+
1990
+ // Enhanced Web Vitals Charts for Playwright Tests
1991
+ {{#if webVitalsCharts}}
1992
+ // Score Distribution Pie Chart
1993
+ const scoreCtx = document.getElementById('webVitalsScoreChart')?.getContext('2d');
1994
+ if (scoreCtx) {
1995
+ new Chart(scoreCtx, {
1996
+ type: 'doughnut',
1997
+ data: {
1998
+ labels: ['Good', 'Needs Improvement', 'Poor'],
1999
+ datasets: [{
2000
+ data: [
2001
+ {{webVitalsCharts.scoreDistribution.good}},
2002
+ {{webVitalsCharts.scoreDistribution.needsImprovement}},
2003
+ {{webVitalsCharts.scoreDistribution.poor}}
2004
+ ],
2005
+ backgroundColor: ['#10b981', '#f59e0b', '#ef4444']
2006
+ }]
2007
+ },
2008
+ options: {
2009
+ responsive: true,
2010
+ maintainAspectRatio: false,
2011
+ plugins: {
2012
+ legend: { position: 'bottom' }
2013
+ }
2014
+ }
2015
+ });
2016
+ }
2017
+
2018
+ // Combined Web Vitals Timeline Chart
2019
+ const timelineCtx = document.getElementById('webVitalsTimelineChart')?.getContext('2d');
2020
+ if (timelineCtx && {{json webVitalsCharts.timeSeries}} && {{json webVitalsCharts.timeSeries}}.unified) {
2021
+ const timeSeries = {{json webVitalsCharts.timeSeries}};
2022
+ const unifiedData = timeSeries.unified;
2023
+ const metrics = ['lcp', 'cls', 'inp', 'ttfb', 'fcp', 'fid'];
2024
+ const metricsConfig = {
2025
+ lcp: { label: 'LCP', color: '#FF6B6B', yAxisID: 'y' },
2026
+ cls: { label: 'CLS', color: '#45B7D1', yAxisID: 'y1' },
2027
+ inp: { label: 'INP', color: '#4ECDC4', yAxisID: 'y' },
2028
+ ttfb: { label: 'TTFB', color: '#FECA57', yAxisID: 'y' },
2029
+ fcp: { label: 'FCP', color: '#96CEB4', yAxisID: 'y' },
2030
+ fid: { label: 'FID', color: '#9370DB', yAxisID: 'y' }
2031
+ };
2032
+
2033
+ // Create datasets for available metrics using unified data
2034
+ const datasets = [];
2035
+ metrics.forEach(metric => {
2036
+ if (unifiedData.data[metric] && unifiedData.data[metric].some(v => v !== null)) {
2037
+ const config = metricsConfig[metric];
2038
+ datasets.push({
2039
+ label: config.label,
2040
+ data: unifiedData.data[metric],
2041
+ borderColor: config.color,
2042
+ backgroundColor: config.color + '20',
2043
+ tension: 0.1,
2044
+ yAxisID: config.yAxisID,
2045
+ pointRadius: 3,
2046
+ pointHoverRadius: 5,
2047
+ spanGaps: true // Connect lines across null values
2048
+ });
2049
+ }
2050
+ });
2051
+
2052
+ const labels = unifiedData.labels;
2053
+
2054
+ new Chart(timelineCtx, {
2055
+ type: 'line',
2056
+ data: {
2057
+ labels: labels,
2058
+ datasets: datasets
2059
+ },
2060
+ options: {
2061
+ responsive: true,
2062
+ maintainAspectRatio: false,
2063
+ interaction: {
2064
+ mode: 'index',
2065
+ intersect: false
2066
+ },
2067
+ plugins: {
2068
+ legend: {
2069
+ position: 'top',
2070
+ labels: {
2071
+ usePointStyle: true,
2072
+ boxWidth: 6
2073
+ }
2074
+ },
2075
+ tooltip: {
2076
+ callbacks: {
2077
+ label: function(context) {
2078
+ const metric = context.dataset.label.toLowerCase();
2079
+ const value = context.parsed.y;
2080
+ if (metric === 'cls') {
2081
+ return `${context.dataset.label}: ${value.toFixed(3)}`;
2082
+ } else if (value < 1000) {
2083
+ return `${context.dataset.label}: ${Math.round(value)}ms`;
2084
+ } else {
2085
+ return `${context.dataset.label}: ${(value / 1000).toFixed(2)}s`;
2086
+ }
2087
+ }
2088
+ }
2089
+ }
2090
+ },
2091
+ scales: {
2092
+ x: {
2093
+ display: true,
2094
+ title: {
2095
+ display: true,
2096
+ text: 'Time'
2097
+ }
2098
+ },
2099
+ y: {
2100
+ type: 'linear',
2101
+ display: true,
2102
+ position: 'left',
2103
+ title: {
2104
+ display: true,
2105
+ text: 'Time (ms)'
2106
+ },
2107
+ beginAtZero: true
2108
+ },
2109
+ y1: {
2110
+ type: 'linear',
2111
+ display: true,
2112
+ position: 'right',
2113
+ title: {
2114
+ display: true,
2115
+ text: 'CLS Score'
2116
+ },
2117
+ max: 1,
2118
+ beginAtZero: true,
2119
+ grid: {
2120
+ drawOnChartArea: false,
2121
+ },
2122
+ }
2123
+ }
2124
+ }
2125
+ });
2126
+ }
2127
+
2128
+ // Time Series Charts for Each Metric
2129
+ {{#each webVitalsCharts.metrics}}
2130
+ const {{this}}Ctx = document.getElementById('webVitals{{this}}Chart')?.getContext('2d');
2131
+ if ({{this}}Ctx && {{json ../webVitalsCharts.timeSeries}}.{{this}}) {
2132
+ const timeSeriesData = {{json ../webVitalsCharts.timeSeries}}.{{this}};
2133
+ new Chart({{this}}Ctx, {
2134
+ type: 'line',
2135
+ data: {
2136
+ labels: timeSeriesData.labels,
2137
+ datasets: [{
2138
+ label: '{{this}}',
2139
+ data: timeSeriesData.data,
2140
+ borderColor: timeSeriesData.borderColor,
2141
+ backgroundColor: timeSeriesData.backgroundColor + '20',
2142
+ tension: 0.1
2143
+ }]
2144
+ },
2145
+ options: {
2146
+ responsive: true,
2147
+ maintainAspectRatio: false,
2148
+ plugins: {
2149
+ legend: { display: false }
2150
+ },
2151
+ scales: {
2152
+ y: {
2153
+ beginAtZero: true,
2154
+ title: {
2155
+ display: true,
2156
+ text: '{{#ifEquals this "cls"}}Score{{else}}ms{{/ifEquals}}'
2157
+ }
2158
+ }
2159
+ }
2160
+ }
2161
+ });
2162
+ }
2163
+ {{/each}}
2164
+
2165
+ // Percentiles Chart
2166
+ const percentilesCtx = document.getElementById('webVitalsPercentilesChart')?.getContext('2d');
2167
+ if (percentilesCtx && {{json webVitalsCharts.distributions}}) {
2168
+ const distributions = {{json webVitalsCharts.distributions}};
2169
+ const metrics = Object.keys(distributions);
2170
+ const datasets = [
2171
+ {
2172
+ label: 'P50 (Median)',
2173
+ data: metrics.map(m => distributions[m]?.median || 0),
2174
+ backgroundColor: '#4ECDC4'
2175
+ },
2176
+ {
2177
+ label: 'P75',
2178
+ data: metrics.map(m => distributions[m]?.p75 || 0),
2179
+ backgroundColor: '#45B7D1'
2180
+ },
2181
+ {
2182
+ label: 'P90',
2183
+ data: metrics.map(m => distributions[m]?.p90 || 0),
2184
+ backgroundColor: '#FECA57'
2185
+ },
2186
+ {
2187
+ label: 'P95',
2188
+ data: metrics.map(m => distributions[m]?.p95 || 0),
2189
+ backgroundColor: '#FF6B6B'
2190
+ },
2191
+ {
2192
+ label: 'P99',
2193
+ data: metrics.map(m => distributions[m]?.p99 || 0),
2194
+ backgroundColor: '#C44569'
2195
+ }
2196
+ ];
2197
+
2198
+ new Chart(percentilesCtx, {
2199
+ type: 'bar',
2200
+ data: {
2201
+ labels: metrics.map(m => m.toUpperCase()),
2202
+ datasets: datasets
2203
+ },
2204
+ options: {
2205
+ responsive: true,
2206
+ maintainAspectRatio: false,
2207
+ plugins: {
2208
+ legend: { position: 'top' },
2209
+ title: {
2210
+ display: true,
2211
+ text: 'Web Vitals Percentile Distribution'
2212
+ }
2213
+ },
2214
+ scales: {
2215
+ y: {
2216
+ beginAtZero: true,
2217
+ type: 'logarithmic',
2218
+ title: {
2219
+ display: true,
2220
+ text: 'Value (log scale)'
2221
+ }
2222
+ }
2223
+ }
2224
+ }
2225
+ });
2226
+ }
2227
+ {{/if}}
2228
+
2229
+ // Verification Performance Chart
2230
+ {{#if summary.verification_metrics}}
2231
+ const verificationCtx = document.getElementById('verificationChart')?.getContext('2d');
2232
+ if (verificationCtx && {{json results}}) {
2233
+ const verificationData = {{json results}}.filter(r => r.custom_metrics?.verification_metrics)
2234
+ .map(r => ({
2235
+ timestamp: r.timestamp,
2236
+ duration: r.custom_metrics.verification_metrics.duration,
2237
+ success: r.custom_metrics.verification_metrics.success,
2238
+ stepName: r.custom_metrics.verification_metrics.step_name || 'Unknown'
2239
+ }));
2240
+
2241
+ new Chart(verificationCtx, {
2242
+ type: 'scatter',
2243
+ data: {
2244
+ datasets: [{
2245
+ label: 'Successful Verifications',
2246
+ data: verificationData.filter(d => d.success).map(d => ({
2247
+ x: d.timestamp,
2248
+ y: d.duration
2249
+ })),
2250
+ backgroundColor: '#10b981',
2251
+ borderColor: '#10b981',
2252
+ pointRadius: 4
2253
+ }, {
2254
+ label: 'Failed Verifications',
2255
+ data: verificationData.filter(d => !d.success).map(d => ({
2256
+ x: d.timestamp,
2257
+ y: d.duration
2258
+ })),
2259
+ backgroundColor: '#ef4444',
2260
+ borderColor: '#ef4444',
2261
+ pointRadius: 4
2262
+ }]
2263
+ },
2264
+ options: {
2265
+ responsive: true,
2266
+ maintainAspectRatio: false,
2267
+ plugins: {
2268
+ title: {
2269
+ display: true,
2270
+ text: 'Verification Step Performance Over Time',
2271
+ font: { size: 16 }
2272
+ },
2273
+ tooltip: {
2274
+ callbacks: {
2275
+ title: function(context) {
2276
+ return new Date(context[0].parsed.x).toLocaleTimeString();
2277
+ },
2278
+ label: function(context) {
2279
+ const point = verificationData[context.dataIndex];
2280
+ return [
2281
+ `Duration: ${Math.round(context.parsed.y)}ms`,
2282
+ `Step: ${point?.stepName || 'Unknown'}`,
2283
+ `Status: ${point?.success ? 'Success' : 'Failed'}`
2284
+ ];
2285
+ }
2286
+ }
2287
+ }
2288
+ },
2289
+ scales: {
2290
+ x: {
2291
+ type: 'linear',
2292
+ position: 'bottom',
2293
+ title: {
2294
+ display: true,
2295
+ text: 'Time'
2296
+ }
2297
+ },
2298
+ y: {
2299
+ beginAtZero: true,
2300
+ title: {
2301
+ display: true,
2302
+ text: 'Duration (ms)'
2303
+ }
2304
+ }
2305
+ }
2306
+ }
2307
+ });
2308
+ }
2309
+ {{/if}}
2310
+
2311
+ // Connect Time Over Time Chart - only show if there's meaningful data
2312
+ const hasConnectTimeData = connectTimeData && connectTimeData.length > 0 &&
2313
+ connectTimeData.some(d => d.avg_connect_time > 0);
2314
+ if (hasConnectTimeData) {
2315
+ const connectTimeCtx = document.getElementById('connectTimeChart').getContext('2d');
2316
+ new Chart(connectTimeCtx, {
2317
+ type: 'line',
2318
+ data: {
2319
+ datasets: [{
2320
+ label: 'Avg Connect Time (ms)',
2321
+ data: connectTimeData.map(d => ({
2322
+ x: d.timestamp,
2323
+ y: d.avg_connect_time
2324
+ })),
2325
+ borderColor: '#10b981',
2326
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
2327
+ tension: 0.4,
2328
+ fill: true,
2329
+ pointRadius: 3
2330
+ }]
2331
+ },
2332
+ options: {
2333
+ responsive: true,
2334
+ maintainAspectRatio: false,
2335
+ plugins: {
2336
+ title: {
2337
+ display: true,
2338
+ text: 'TCP Connection Time Over Time',
2339
+ font: { size: 16 }
2340
+ },
2341
+ tooltip: {
2342
+ callbacks: {
2343
+ title: function(context) {
2344
+ return new Date(context[0].parsed.x).toLocaleTimeString();
2345
+ },
2346
+ label: function(context) {
2347
+ return `Connect Time: ${Math.round(context.parsed.y)}ms`;
2348
+ }
2349
+ }
2350
+ }
2351
+ },
2352
+ scales: {
2353
+ x: {
2354
+ type: 'time',
2355
+ time: {
2356
+ displayFormats: {
2357
+ second: 'HH:mm:ss'
2358
+ }
2359
+ },
2360
+ title: {
2361
+ display: true,
2362
+ text: 'Time'
2363
+ }
2364
+ },
2365
+ y: {
2366
+ beginAtZero: true,
2367
+ title: {
2368
+ display: true,
2369
+ text: 'Connect Time (ms)'
2370
+ }
2371
+ }
2372
+ }
2373
+ }
2374
+ });
2375
+ } else {
2376
+ // Hide connect time chart container if no data
2377
+ const connectTimeContainer = document.getElementById('connectTimeChart')?.closest('.chart-container, .bg-white, [class*="rounded"]');
2378
+ if (connectTimeContainer) connectTimeContainer.style.display = 'none';
2379
+ }
2380
+
2381
+ // Latency Over Time Chart - only show if there's meaningful data
2382
+ const hasLatencyData = latencyData && latencyData.length > 0 &&
2383
+ latencyData.some(d => d.avg_latency > 0);
2384
+ if (hasLatencyData) {
2385
+ const latencyCtx = document.getElementById('latencyChart').getContext('2d');
2386
+ new Chart(latencyCtx, {
2387
+ type: 'line',
2388
+ data: {
2389
+ datasets: [{
2390
+ label: 'Avg Latency / TTFB (ms)',
2391
+ data: latencyData.map(d => ({
2392
+ x: d.timestamp,
2393
+ y: d.avg_latency
2394
+ })),
2395
+ borderColor: '#f59e0b',
2396
+ backgroundColor: 'rgba(245, 158, 11, 0.1)',
2397
+ tension: 0.4,
2398
+ fill: true,
2399
+ pointRadius: 3
2400
+ }]
2401
+ },
2402
+ options: {
2403
+ responsive: true,
2404
+ maintainAspectRatio: false,
2405
+ plugins: {
2406
+ title: {
2407
+ display: true,
2408
+ text: 'Latency (Time to First Byte) Over Time',
2409
+ font: { size: 16 }
2410
+ },
2411
+ tooltip: {
2412
+ callbacks: {
2413
+ title: function(context) {
2414
+ return new Date(context[0].parsed.x).toLocaleTimeString();
2415
+ },
2416
+ label: function(context) {
2417
+ return `Latency: ${Math.round(context.parsed.y)}ms`;
2418
+ }
2419
+ }
2420
+ }
2421
+ },
2422
+ scales: {
2423
+ x: {
2424
+ type: 'time',
2425
+ time: {
2426
+ displayFormats: {
2427
+ second: 'HH:mm:ss'
2428
+ }
2429
+ },
2430
+ title: {
2431
+ display: true,
2432
+ text: 'Time'
2433
+ }
2434
+ },
2435
+ y: {
2436
+ beginAtZero: true,
2437
+ title: {
2438
+ display: true,
2439
+ text: 'Latency (ms)'
2440
+ }
2441
+ }
2442
+ }
2443
+ }
2444
+ });
2445
+ } else {
2446
+ // Hide latency chart container if no data
2447
+ const latencyContainer = document.getElementById('latencyChart')?.closest('.chart-container, .bg-white, [class*="rounded"]');
2448
+ if (latencyContainer) latencyContainer.style.display = 'none';
2449
+ }
2450
+ </script>
2451
+ </body>
2452
+
2453
+ </html>