├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── dev ├── export.json ├── index.js └── report.html ├── jest.config.js ├── package-lock.json ├── package.json ├── screenshot └── k6.png ├── src ├── index.test.ts ├── index.ts ├── reporter.ts ├── types.ts └── util.ts ├── templates └── template.ejs └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.tgz 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /dev 2 | /src 3 | node_modules 4 | /screenshot 5 | *.test.ts 6 | *.tgz 7 | jest.config.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 15 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Baidi Liu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # k6-html-reporter 2 | 3 | A light weight html reporter for k6 4 | 5 | [![Build Status](https://travis-ci.com/szboynono/k6-html-reporter.svg?branch=main)](https://travis-ci.com/szboynono/k6-html-reporter) 6 | ![npm](https://img.shields.io/npm/v/k6-html-reporter.svg) 7 | ![npm](https://img.shields.io/npm/dw/k6-html-reporter.svg) 8 | 9 | 10 | 11 | ## Install 12 | NPM: 13 | 14 | ``` bash 15 | npm install k6-html-reporter --save-dev 16 | ``` 17 | 18 | YARN: 19 | 20 | ```bash 21 | yarn add k6-html-reporter --dev 22 | ``` 23 | 24 | 25 | 26 | ## Usage 27 | 28 | 1. Install the package 29 | 2. Create a js/ts file and specify the options: 30 | 31 | ```js 32 | 33 | const reporter = require('k6-html-reporter'); 34 | 35 | const options = { 36 | jsonFile: , 37 | output: , 38 | }; 39 | 40 | reporter.generateSummaryReport(options); 41 | 42 | ``` 43 | 44 | for typescript 45 | 46 | ```ts 47 | import {generateSummaryReport} from 'k6-html-reporter'; 48 | 49 | const options = { 50 | jsonFile: , 51 | output: , 52 | }; 53 | 54 | generateSummaryReport(options); 55 | ``` 56 | 3. Output a JSON summary output with the `handleSummary` function provided by k6, [more info](https://k6.io/docs/results-visualization/end-of-test-summary). 57 | ```js 58 | export default function () { /** some tests here */} 59 | export function handleSummary(data) { 60 | console.log('Preparing the end-of-test summary...'); 61 | return { 62 | : JSON.stringify(data), 63 | } 64 | } 65 | ``` 66 | 67 | > **Note**: The ` --summary-export=path/to/file.json` run option is no longer recomanded after k6 v0.30.0. 68 | 69 | 4. Run the code in step two as a node.js script after the test execution: 70 | ```bash 71 | node xxxx.js 72 | ``` 73 | 74 | ### Sample report: 75 | ![Alt text](./screenshot/k6.png?raw=true "Optional Title") 76 | -------------------------------------------------------------------------------- /dev/export.json: -------------------------------------------------------------------------------- 1 | { 2 | "root_group": { 3 | "groups": [ 4 | { 5 | "path": "::bigger group", 6 | "id": "226c69e91e8a461f435cbf90a58e220a", 7 | "groups": [ 8 | { 9 | "name": "group one", 10 | "path": "::bigger group::group one", 11 | "id": "07005d932c18483963641fb146c1def3", 12 | "groups": [], 13 | "checks": [ 14 | { 15 | "fails": 149, 16 | "name": "status is 201 in group one", 17 | "path": "::bigger group::group one::status is 201 in group one", 18 | "id": "992a99f5953e3f64aac15d77bc3f1235", 19 | "passes": 0 20 | } 21 | ] 22 | }, 23 | { 24 | "name": "group two", 25 | "path": "::bigger group::group two", 26 | "id": "67c97ab65738bc85d1f8da78e117315a", 27 | "groups": [], 28 | "checks": [ 29 | { 30 | "name": "status is 200 in group one", 31 | "path": "::bigger group::group two::status is 200 in group one", 32 | "id": "9d81d9068470b1a392559b29d4b3294d", 33 | "passes": 149, 34 | "fails": 0 35 | } 36 | ] 37 | } 38 | ], 39 | "checks": [], 40 | "name": "bigger group" 41 | } 42 | ], 43 | "checks": [ 44 | { 45 | "name": "status is 200 in individual group", 46 | "path": "::status is 200 in individual group", 47 | "id": "a04a5961ccb8808a2490d8a850747ee5", 48 | "passes": 149, 49 | "fails": 0 50 | } 51 | ], 52 | "name": "", 53 | "path": "", 54 | "id": "d41d8cd98f00b204e9800998ecf8427e" 55 | }, 56 | "options": { 57 | "summaryTrendStats": [ 58 | "avg", 59 | "min", 60 | "med", 61 | "max", 62 | "p(90)", 63 | "p(95)" 64 | ], 65 | "summaryTimeUnit": "" 66 | }, 67 | "metrics": { 68 | "http_req_tls_handshaking": { 69 | "type": "trend", 70 | "contains": "time", 71 | "values": { 72 | "med": 0, 73 | "max": 0, 74 | "p(90)": 0, 75 | "p(95)": 0, 76 | "avg": 0, 77 | "min": 0 78 | } 79 | }, 80 | "vus": { 81 | "type": "gauge", 82 | "contains": "default", 83 | "values": { 84 | "min": 1, 85 | "max": 9, 86 | "value": 1 87 | } 88 | }, 89 | "http_req_blocked": { 90 | "type": "trend", 91 | "contains": "time", 92 | "values": { 93 | "min": 0.0039, 94 | "med": 0.0162, 95 | "max": 97.9576, 96 | "p(90)": 0.02458000000000005, 97 | "p(95)": 34.254439999999995, 98 | "avg": 2.6951053691275164 99 | } 100 | }, 101 | "http_req_waiting": { 102 | "values": { 103 | "p(90)": 41.78374000000001, 104 | "p(95)": 43.446780000000004, 105 | "avg": 37.561263758389266, 106 | "min": 31.3698, 107 | "med": 35.505, 108 | "max": 220.0312 109 | }, 110 | "type": "trend", 111 | "contains": "time" 112 | }, 113 | "data_received": { 114 | "type": "counter", 115 | "contains": "data", 116 | "values": { 117 | "count": 1645162, 118 | "rate": 52973.82121211533 119 | } 120 | }, 121 | "iterations": { 122 | "values": { 123 | "count": 149, 124 | "rate": 4.797764208391139 125 | }, 126 | "thresholds": { 127 | "count > 1000": { 128 | "ok": false 129 | } 130 | }, 131 | "type": "counter", 132 | "contains": "default" 133 | }, 134 | "http_req_sending": { 135 | "values": { 136 | "med": 0.0421, 137 | "max": 2.9039, 138 | "p(90)": 0.06222000000000004, 139 | "p(95)": 0.11393999999999987, 140 | "avg": 0.060091275167785195, 141 | "min": 0.0098 142 | }, 143 | "type": "trend", 144 | "contains": "time" 145 | }, 146 | "group_duration": { 147 | "type": "trend", 148 | "contains": "time", 149 | "values": { 150 | "max": 2.1008, 151 | "p(90)": 0.21678, 152 | "p(95)": 0.26238999999999996, 153 | "avg": 0.08931565995525728, 154 | "min": 0.0096, 155 | "med": 0.0534 156 | } 157 | }, 158 | "http_reqs": { 159 | "type": "counter", 160 | "contains": "default", 161 | "values": { 162 | "count": 149, 163 | "rate": 4.797764208391139 164 | } 165 | }, 166 | "http_req_duration": { 167 | "type": "trend", 168 | "contains": "time", 169 | "values": { 170 | "min": 32.015, 171 | "med": 35.7894, 172 | "max": 225.6931, 173 | "p(90)": 42.34002000000001, 174 | "p(95)": 44.55441999999999, 175 | "avg": 38.04265570469799 176 | }, 177 | "thresholds": { 178 | "p(90) < 4": { 179 | "ok": false 180 | }, 181 | "p(95) < 800": { 182 | "ok": true 183 | }, 184 | "p(99.9) < 2000": { 185 | "ok": true 186 | } 187 | } 188 | }, 189 | "http_req_receiving": { 190 | "type": "trend", 191 | "contains": "time", 192 | "values": { 193 | "med": 0.1969, 194 | "max": 6.3429, 195 | "p(90)": 0.9294600000000013, 196 | "p(95)": 2.013659999999999, 197 | "avg": 0.4213006711409398, 198 | "min": 0.0413 199 | } 200 | }, 201 | "data_sent": { 202 | "contains": "data", 203 | "values": { 204 | "count": 11324, 205 | "rate": 364.6300798377266 206 | }, 207 | "type": "counter" 208 | }, 209 | "vus_max": { 210 | "type": "gauge", 211 | "contains": "default", 212 | "values": { 213 | "value": 10, 214 | "min": 10, 215 | "max": 10 216 | } 217 | }, 218 | "iteration_duration": { 219 | "type": "trend", 220 | "contains": "time", 221 | "values": { 222 | "max": 1332.5394, 223 | "p(90)": 1044.70342, 224 | "p(95)": 1079.7749000000001, 225 | "avg": 1042.4809630872487, 226 | "min": 1032.6804, 227 | "med": 1037.3097 228 | } 229 | }, 230 | "http_req_connecting": { 231 | "type": "trend", 232 | "contains": "time", 233 | "values": { 234 | "p(90)": 0, 235 | "p(95)": 34.10865999999999, 236 | "avg": 2.2673449664429532, 237 | "min": 0, 238 | "med": 0, 239 | "max": 44.6731 240 | } 241 | }, 242 | "errorRate": { 243 | "type": "rate", 244 | "contains": "default", 245 | "values": { 246 | "rate": 0, 247 | "passes": 0, 248 | "fails": 29 249 | }, 250 | "thresholds": { 251 | "rate < 0.1": { 252 | "ok": false 253 | } 254 | } 255 | }, 256 | "checks": { 257 | "type": "rate", 258 | "contains": "default", 259 | "values": { 260 | "rate": 0.6666666666666666, 261 | "passes": 298, 262 | "fails": 149 263 | }, 264 | "thresholds": { 265 | "rate>0.9": { 266 | "ok": false 267 | } 268 | } 269 | } 270 | } 271 | } -------------------------------------------------------------------------------- /dev/index.js: -------------------------------------------------------------------------------- 1 | const reporter = require('../dist/index'); 2 | 3 | reporter.generateSummaryReport({ 4 | jsonFile: './export.json', 5 | output: '.' 6 | }) -------------------------------------------------------------------------------- /dev/report.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | K6 Summary Report 6 | 7 | 29 | 30 | 31 | 32 |
33 |
34 |
35 |

K6 Summary Report

36 |

Generated at 6/29/2021, 9:47:10 PM

37 |
38 | 39 | 40 |
41 |
42 | 46 |

Pass/fail criteria used to specify the performance expectations of the system under test.

47 | 48 | 51 | 52 |
53 | 54 |
55 |

passes: 2

56 |

fails: 4

57 |
    58 | 59 |
  • 60 |
    iterations
    61 |
      62 | 63 | 64 |
    • 65 | 66 | count > 1000 67 |
    • 68 | 69 |
    70 |
  • 71 | 72 |
  • 73 |
    http_req_duration
    74 |
      75 | 76 | 77 |
    • 78 | 79 | p(90) < 4 80 |
    • 81 | 82 | 83 |
    • 84 | 85 | p(95) < 800 86 |
    • 87 | 88 | 89 |
    • 90 | 91 | p(99.9) < 2000 92 |
    • 93 | 94 |
    95 |
  • 96 | 97 |
  • 98 |
    errorRate
    99 |
      100 | 101 | 102 |
    • 103 | 104 | rate < 0.1 105 |
    • 106 | 107 |
    108 |
  • 109 | 110 |
  • 111 |
    checks
    112 |
      113 | 114 | 115 |
    • 116 | 117 | rate>0.9 118 |
    • 119 | 120 |
    121 |
  • 122 | 123 |
124 |
125 |
126 | 127 | 128 | 129 |
130 |
131 | 135 |

Asserts that don't halt the execution unless specified in the threshold.

136 |
137 |
138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 |
#checkpassesfails
1status is 200 in individual group1490
2bigger group ⇀ group one ⇀ status is 201 in group one0149
3bigger group ⇀ group two ⇀ status is 200 in group one1490
178 |

passes: 298

179 |

fails: 149

180 |

pass rate: 66.67%

181 | 182 | 183 | 184 |

Threshold for checks was not met, more details in the threshold session

185 | 186 | 187 |
188 |
189 | 190 | 191 |
192 |
193 | 197 |

Important aspect of metrics management in k6

198 |
199 |
200 | 201 |
202 |
A metric that cumulatively sums added values.
203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 |
#namecountrate
1data_received164516252973.82121211533
2data_sent11324364.6300798377266
3http_reqs1494.797764208391139
4iterations1494.797764208391139
252 |
253 | 254 | 255 | 256 |
257 |
A metric that allows for calculating statistics on the added values (min, max, average and percentiles).
258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 |
#nameavgmaxmedminP(90)P(95)
1group_duration0.09 ms2.10 ms0.05 ms0.01 ms0.22 ms0.26 ms
2http_req_blocked2.70 ms97.96 ms0.02 ms0.00 ms0.02 ms34.25 ms
3http_req_connecting2.27 ms44.67 ms0.00 ms0.00 ms0.00 ms34.11 ms
4http_req_duration38.04 ms225.69 ms35.79 ms32.02 ms42.34 ms44.55 ms
5http_req_receiving0.42 ms6.34 ms0.20 ms0.04 ms0.93 ms2.01 ms
6http_req_sending0.06 ms2.90 ms0.04 ms0.01 ms0.06 ms0.11 ms
7http_req_tls_handshaking0.00 ms0.00 ms0.00 ms0.00 ms0.00 ms0.00 ms
8http_req_waiting37.56 ms220.03 ms35.51 ms31.37 ms41.78 ms43.45 ms
9iteration_duration1042.48 ms1332.54 ms1037.31 ms1032.68 ms1044.70 ms1079.77 ms
392 |
393 | 394 | 395 | 396 |
397 |
A metric that tracks the percentage of added values that are non-zero.
398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 |
#nameratepassesfails
1errorRate0029
422 |
423 | 424 | 425 | 426 |
427 |
A metric that stores the min, max and last values added to it.
428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 |
#namemaxminvalue
1vus911
2vus_max101010
462 | 463 | 464 |

3 metrics failed the thresholds, more details in the threshold session

465 | 466 | 467 |
468 | 469 |
470 |
471 |
472 |
Powered by k6-html-reporter
473 | 474 | 475 | 476 | 477 |
478 |
479 | 480 | 481 | 482 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/src" 4 | ], 5 | "testMatch": [ 6 | "**/__tests__/**/*.+(ts|tsx|js)", 7 | "**/?(*.)+(spec|test).+(ts|tsx|js)" 8 | ], 9 | "transform": { 10 | "^.+\\.(ts|tsx)$": "ts-jest" 11 | }, 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "k6-html-reporter", 3 | "version": "1.0.5", 4 | "description": "A html reporter for k6", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "dependencies": { 8 | "ejs": "^3.1.5" 9 | }, 10 | "devDependencies": { 11 | "@types/jest": "^26.0.20", 12 | "@types/node": "^14.14.10", 13 | "jest": "^26.6.3", 14 | "ts-jest": "^26.5.3", 15 | "ts-node": "^9.1.0", 16 | "typescript": "^4.2.3" 17 | }, 18 | "scripts": { 19 | "build": "rm -rf dist && tsc", 20 | "dev": "rm -rf dist && tsc && cd dev && node index.js", 21 | "start": "ts-node src/index.ts", 22 | "test": "jest" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/szboynono/k6-html-reporter.git" 27 | }, 28 | "keywords": [ 29 | "report", 30 | "k6", 31 | "html", 32 | "json to html", 33 | "k6 html reporter", 34 | "k6 summary report" 35 | ], 36 | "author": "", 37 | "license": "ISC", 38 | "bugs": { 39 | "url": "https://github.com/szboynono/k6-html-reporter/issues" 40 | }, 41 | "homepage": "https://github.com/szboynono/k6-html-reporter#readme" 42 | } 43 | -------------------------------------------------------------------------------- /screenshot/k6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szboynono/k6-html-reporter/6470d7a96ea91fb3da2fd064ce6c89ce7e055bd4/screenshot/k6.png -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as reporter from "./reporter"; 2 | import { generateSummaryReport } from './index' 3 | 4 | describe('generateSummaryReport()', () => { 5 | test('should generate a summary report', () => { 6 | jest.spyOn(reporter, 'generate').mockImplementation(jest.fn()) 7 | generateSummaryReport({} as any) 8 | expect(reporter.generate).toHaveBeenCalledWith({}) 9 | }) 10 | }) -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { generate } from "./reporter"; 2 | import { Options } from "./types"; 3 | 4 | export function generateSummaryReport(options: Options) { 5 | generate(options); 6 | } 7 | -------------------------------------------------------------------------------- /src/reporter.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import ejs from "ejs"; 4 | import { DisplayTotalThresholdResult, MetricsType, Options } from "./types"; 5 | import { compareNameAscending } from "./util"; 6 | 7 | export function generate(options: Options) { 8 | const resolvedInputPath = path.resolve(process.cwd(), options.jsonFile); 9 | const resolvedOutputPath = path.resolve(process.cwd(), options.output); 10 | const jsonReport = readJsonReport(resolvedInputPath); 11 | writeHtmlReport(jsonReport, resolvedOutputPath); 12 | } 13 | 14 | function readJsonReport(filePath: string): JSON { 15 | const resolvedPath = path.resolve(__dirname, filePath); 16 | const rawData = fs.readFileSync(resolvedPath); 17 | return JSON.parse(rawData.toString()); 18 | } 19 | 20 | function writeHtmlReport(content: JSON, filePath: string): void { 21 | const time = new Date().toLocaleString(); 22 | const templatePath = path.resolve(__dirname, "../templates/template.ejs"); 23 | const checkRootGroupData = content["root_group"]; 24 | const metricsData = content["metrics"]; 25 | 26 | const { 27 | checkMetric, 28 | counterMetrics, 29 | trendMetrics, 30 | gaugeMetrics, 31 | rateMetrics, 32 | allThresholds, 33 | totalThresholdResult, 34 | } = mapMetrics(metricsData); 35 | const checks = getChecks(checkRootGroupData).map((data) => { 36 | const splitedPath = data.path.split("::"); 37 | splitedPath.shift(); 38 | 39 | return { 40 | ...data, 41 | pathArray: splitedPath.join(" \u21C0 "), 42 | }; 43 | }); 44 | 45 | ejs.renderFile( 46 | templatePath, 47 | { 48 | checks, 49 | rateMetrics: rateMetrics.sort(compareNameAscending), 50 | checkMetric, 51 | counterMetrics: counterMetrics.sort(compareNameAscending), 52 | trendMetrics: trendMetrics.sort(compareNameAscending), 53 | gaugeMetrics: gaugeMetrics.sort(compareNameAscending), 54 | allThresholds, 55 | totalThresholdResult, 56 | time, 57 | }, 58 | {}, 59 | function (err, str) { 60 | if (err) { 61 | console.error(err); 62 | } 63 | 64 | let html = ejs.render(str); 65 | if (!fs.existsSync(filePath)) { 66 | fs.mkdirSync(filePath, { recursive: true }); 67 | } 68 | fs.writeFileSync(`${filePath}/report.html`, html); 69 | console.log(`Report is created at ${filePath}`); 70 | } 71 | ); 72 | } 73 | 74 | function mapMetrics(data: Object) { 75 | let checkMetric = {}; 76 | const counterMetrics = []; 77 | const trendMetrics = []; 78 | const gaugeMetrics = []; 79 | const rateMetrics = []; 80 | const allThresholds = []; 81 | 82 | let totalThresholdResult: DisplayTotalThresholdResult = { 83 | passes: 0, 84 | fails: 0, 85 | failedMetricsNum: 0, 86 | }; 87 | 88 | Object.entries(data).forEach(([key, value]) => { 89 | if (value.type === MetricsType.COUNTER) { 90 | const { updatedThresholdResult, displayThreshold, metric } = 91 | handleMetricValues(key, value, totalThresholdResult); 92 | totalThresholdResult = { 93 | ...totalThresholdResult, 94 | ...updatedThresholdResult, 95 | }; 96 | if (displayThreshold) { 97 | allThresholds.push(displayThreshold); 98 | } 99 | counterMetrics.push(metric); 100 | } else if (value.type === MetricsType.TREND) { 101 | const { updatedThresholdResult, displayThreshold, metric } = 102 | handleMetricValues(key, value, totalThresholdResult); 103 | totalThresholdResult = { 104 | ...totalThresholdResult, 105 | ...updatedThresholdResult, 106 | }; 107 | if (displayThreshold) { 108 | allThresholds.push(displayThreshold); 109 | } 110 | trendMetrics.push(metric); 111 | } else if (key === "checks") { 112 | const { updatedThresholdResult, displayThreshold, metric } = 113 | handleMetricValues(key, value, totalThresholdResult); 114 | totalThresholdResult = { 115 | ...totalThresholdResult, 116 | ...updatedThresholdResult, 117 | }; 118 | if (displayThreshold) { 119 | allThresholds.push(displayThreshold); 120 | } 121 | checkMetric = metric; 122 | } else if (value.type === MetricsType.GAUGE) { 123 | const { updatedThresholdResult, displayThreshold, metric } = 124 | handleMetricValues(key, value, totalThresholdResult); 125 | totalThresholdResult = { 126 | ...totalThresholdResult, 127 | ...updatedThresholdResult, 128 | }; 129 | if (displayThreshold) { 130 | allThresholds.push(displayThreshold); 131 | } 132 | gaugeMetrics.push(metric); 133 | } else if (value.type === MetricsType.RATE && key !== "checks") { 134 | const { updatedThresholdResult, displayThreshold, metric } = 135 | handleMetricValues(key, value, totalThresholdResult); 136 | totalThresholdResult = { 137 | ...totalThresholdResult, 138 | ...updatedThresholdResult, 139 | }; 140 | if (displayThreshold) { 141 | allThresholds.push(displayThreshold); 142 | } 143 | rateMetrics.push(metric); 144 | } 145 | }); 146 | return { 147 | checkMetric, 148 | counterMetrics, 149 | trendMetrics, 150 | gaugeMetrics, 151 | rateMetrics, 152 | allThresholds, 153 | totalThresholdResult, 154 | }; 155 | } 156 | 157 | function handleMetricValues( 158 | key: string, 159 | value: any, 160 | currentTotalThresholdResult: DisplayTotalThresholdResult 161 | ) { 162 | const metric = { 163 | name: key, 164 | ...value, 165 | thresholdFailed: undefined, 166 | }; 167 | 168 | const updatedThresholdResult = { ...currentTotalThresholdResult }; 169 | 170 | if (value.thresholds) { 171 | const [passes, fails] = thresholdResult(value.thresholds); 172 | 173 | if (fails > 0 && key !== "checks") { 174 | updatedThresholdResult.failedMetricsNum++; 175 | } 176 | updatedThresholdResult.passes += passes; 177 | updatedThresholdResult.fails += fails; 178 | metric.thresholdFailed = fails > 0; 179 | 180 | return { 181 | displayThreshold: { 182 | name: key, 183 | thresholds: value.thresholds, 184 | }, 185 | updatedThresholdResult, 186 | metric, 187 | }; 188 | } 189 | 190 | return { updatedThresholdResult, metric }; 191 | } 192 | 193 | function thresholdResult(thresholds: Object | undefined) { 194 | if (thresholds) { 195 | const thresholdArr = Object.values(thresholds); 196 | const passes = thresholdArr.filter((value) => value.ok === true).length; 197 | const fails = thresholdArr.length - passes; 198 | return [passes, fails]; 199 | } 200 | } 201 | 202 | function getChecks(data: any) { 203 | let checksOutput = []; 204 | 205 | findChecksRecursively(data); 206 | 207 | function findChecksRecursively(data) { 208 | if (data.groups.length === 0 && data.checks.length === 0) { 209 | return; 210 | } 211 | 212 | if (data.checks.length > 0) { 213 | Object.values(data.checks).forEach((value) => { 214 | checksOutput.push(value); 215 | }); 216 | } 217 | 218 | for (let item in data.groups) { 219 | findChecksRecursively(data.groups[item]); 220 | } 221 | } 222 | 223 | return checksOutput; 224 | } 225 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Options = { 2 | jsonFile: string, 3 | output: string, 4 | } 5 | 6 | export enum MetricsType { 7 | GAUGE = 'gauge', 8 | RATE = 'rate', 9 | TREND = 'trend', 10 | COUNTER = 'counter' 11 | } 12 | 13 | export interface DisplayTotalThresholdResult { 14 | passes: number, 15 | fails: number, 16 | failedMetricsNum: number 17 | } -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export const compareNameAscending = (a, b): number => { 2 | if (a.name < b.name) { 3 | return -1; 4 | } else if (a.name > b.name) { 5 | return 1; 6 | } else { 7 | return 0; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /templates/template.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | K6 Summary Report 6 | 7 | 8 | 30 | 31 | 32 | 33 |
34 |
35 |
36 |

K6 Summary Report

37 |

Generated at <%=time%>

38 |
39 | 40 | <% if(allThresholds.length > 0) { %> 41 |
42 |
43 | 47 |

Pass/fail criteria used to specify the performance expectations of the system under test.

48 | <% if(totalThresholdResult.fails > 0) { %> 49 | 52 | <% } else { %> 53 | 56 | <% } %> 57 |
58 | 59 |
60 |

passes: <%= totalThresholdResult.passes %>

61 |

fails: <%= totalThresholdResult.fails %>

62 |
    63 | <%allThresholds.forEach(data => {%> 64 |
  • 65 |
    <%= data.name %>
    66 |
      67 | <% for(let [key, value] of Object.entries(data.thresholds)) {%> 68 | <% if(value.ok === false) { %> 69 |
    • 70 | <%} else {%> 71 |
    • 72 | <%}%> 73 | <%= key %> 74 |
    • 75 | <%}%> 76 |
    77 |
  • 78 | <%})%> 79 |
80 |
81 |
82 | <% } %> 83 | 84 | <% if(checks.length > 0) {%> 85 |
86 |
87 | 91 |

Asserts that don't halt the execution unless specified in the threshold.

92 |
93 |
94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | <% for(var i=0; i < checks.length; i++) { %> 105 | <% if(checks[i].fails > 0) { %> 106 | 107 | <% } else { %> 108 | 109 | <% } %> 110 | 111 | 112 | 113 | 114 | 115 | <% } %> 116 | 117 |
#checkpassesfails
<%= i+1 %><%= checks[i].pathArray%><%= checks[i].passes %><%= checks[i].fails %>
118 |

passes: <%= checkMetric.values.passes %>

119 |

fails: <%= checkMetric.values.fails %>

120 |

pass rate: <%= (checkMetric.values.passes * 100/(checkMetric.values.fails + checkMetric.values.passes)).toFixed(2) %>%

121 | 122 | <% if(checkMetric["thresholds"]) { %> 123 | <% if(checkMetric.thresholdFailed === true) { %> 124 |

Threshold for checks was not met, more details in the threshold session

125 | <% } else { %> 126 |

All thresholds for checks were met

127 | <% } %> 128 | <% } %> 129 |
130 |
131 | <% } %> 132 | 133 |
134 |
135 | 139 |

Important aspect of metrics management in k6

140 |
141 |
142 | <% if(counterMetrics.length > 0) {%> 143 |
144 |
A metric that cumulatively sums added values.
145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | <% for(let i=0; i < counterMetrics.length; i++) { %> 156 | <% if(counterMetrics[i].thresholdFailed === true) {%> 157 | 158 | <% } else { %> 159 | 160 | <% } %> 161 | 162 | 163 | 164 | 165 | 166 | <% } %> 167 | 168 |
#namecountrate
<%= i+1 %><%= counterMetrics[i].name %><%= counterMetrics[i].values.count %><%= counterMetrics[i].values.rate %>
169 |
170 | <% } %> 171 | 172 | <% if(trendMetrics.length > 0) {%> 173 |
174 |
A metric that allows for calculating statistics on the added values (min, max, average and percentiles).
175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | <% for(let i=0; i < trendMetrics.length; i++) { %> 190 | <% if(trendMetrics[i].thresholdFailed === true) {%> 191 | 192 | <% } else { %> 193 | 194 | <% } %> 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | <% } %> 205 | 206 |
#nameavgmaxmedminP(90)P(95)
<%= i+1 %><%= trendMetrics[i].name %><%= trendMetrics[i].values.avg.toFixed(2) %> ms<%= trendMetrics[i].values.max.toFixed(2) %> ms<%= trendMetrics[i].values.med.toFixed(2) %> ms<%= trendMetrics[i].values.min.toFixed(2) %> ms<%= trendMetrics[i].values["p(90)"].toFixed(2) %> ms<%= trendMetrics[i].values["p(95)"].toFixed(2) %> ms
207 |
208 | <% } %> 209 | 210 | <% if(rateMetrics.length > 0) {%> 211 |
212 |
A metric that tracks the percentage of added values that are non-zero.
213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | <% for(let i=0; i < rateMetrics.length; i++) { %> 225 | <% if(rateMetrics[i].thresholdFailed === true) {%> 226 | 227 | <% } else { %> 228 | 229 | <% } %> 230 | 231 | 232 | 233 | 234 | 235 | 236 | <% } %> 237 | 238 |
#nameratepassesfails
<%= i+1 %><%= rateMetrics[i].name %><%= rateMetrics[i].values.rate %><%= rateMetrics[i].values.passes %><%= rateMetrics[i].values.fails %>
239 |
240 | <% } %> 241 | 242 | <% if(gaugeMetrics.length > 0) {%> 243 |
244 |
A metric that stores the min, max and last values added to it.
245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | <% for(let i=0; i < gaugeMetrics.length; i++) { %> 257 | <% if(gaugeMetrics[i].thresholdFailed === true) {%> 258 | 259 | <% } else { %> 260 | 261 | <% } %> 262 | 263 | 264 | 265 | 266 | 267 | 268 | <% } %> 269 | 270 |
#namemaxminvalue
<%= i+1 %><%= gaugeMetrics[i].name %><%= gaugeMetrics[i].values.max %><%= gaugeMetrics[i].values.min %><%= gaugeMetrics[i].values.value %>
271 | <% if(allThresholds.length > 0) { %> 272 | <%if(totalThresholdResult.failedMetricsNum === 0) {%> 273 |

All thresholds for metrics were met

274 | <%} else {%> 275 |

<%=totalThresholdResult.failedMetricsNum%> <%if(totalThresholdResult.failedMetricsNum === 1){%>metric<%} else {%>metrics<%}%> failed the thresholds, more details in the threshold session

276 | <%}%> 277 | <% } %> 278 |
279 | <% } %> 280 |
281 |
282 |
283 |
Powered by k6-html-reporter
284 | 285 | 286 | 287 | 288 |
289 |
290 | 291 | 292 | 293 | 294 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es2020", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "declaration": true 10 | }, 11 | "lib": ["es2015"], 12 | "exclude": [ 13 | "src/**/*.test.ts" 14 | ] 15 | } --------------------------------------------------------------------------------