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