├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── codeql-analysis.yml │ └── nodejs.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── console.png ├── devtools-safari.png ├── devtools.png ├── index.js ├── index.test.js ├── jest.config.js ├── package-lock.json └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilentImp/express-middleware-headers-server-timing/e2dd96eed39f9af49d55e2aff26731ce6c3d421d/.eslintignore -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | node: true, 5 | commonjs: true, 6 | es2021: true, 7 | 'jest/globals': true 8 | }, 9 | extends: [ 10 | 'standard' 11 | ], 12 | settings: { 13 | jest: { 14 | version: 27 15 | } 16 | }, 17 | parserOptions: { 18 | ecmaVersion: 12 19 | }, 20 | plugins: ['jest'], 21 | rules: { 22 | 'jest/no-disabled-tests': 'warn', 23 | 'jest/no-focused-tests': 'error', 24 | 'jest/no-identical-title': 'error', 25 | 'jest/prefer-to-have-length': 'warn', 26 | 'jest/valid-expect': 'error' 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '22 21 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Testing middleware 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Use Node 16 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: '16' 17 | - name: install dependencies 18 | run: npm ci 19 | - name: run tests 20 | run: npm run checks 21 | - run: echo "This job's status is ${{ job.status }}." 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | node_modules 3 | *.log 4 | .DS_Store 5 | package-lock.json 6 | .husky 7 | coverage 8 | *.png 9 | CONTRIBUTING.md 10 | *.test.js 11 | jest.config.js 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Feel free to send PR with proper descriptions. 2 | No special requirements. 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Server-Timing Header 2 | 3 | This is middleware for [Express](https://expressjs.com/) that allow you monitor server-side performance in the browser with use of [Service-Timing](https://w3c.github.io/server-timing/) headers. 4 | 5 | - Great for identifying server-side performance issues. 6 | - Supported in Chrome, Safari and Mozilla. 7 | - _1.52 KB_ with all dependencies, minified and gzipped. 8 | - Tested. 9 | 10 | ## Usage 11 | 12 | **Step 1:** install [package](https://www.npmjs.com/package/server-timing-header). 13 | 14 | npm i -S server-timing-header 15 | 16 | **Step 2:** add middleware. 17 | 18 | ```diff javascript 19 | + const serverTimingMiddleware = require('server-timing-header'); 20 | const express = require('express'); 21 | const app = express(); 22 | 23 | + app.use(serverTimingMiddleware({sendHeaders: (process.env.NODE_ENV !== 'production')})); 24 | ``` 25 | 26 | **Step 3:** measure how long take to get data. 27 | 28 | ```diff javascript 29 | app.get('/', function (req, res, next) { 30 | + req.serverTiming.from('db'); 31 | // fetching data from database 32 | + req.serverTiming.to('db'); 33 | // … 34 | }); 35 | ``` 36 | 37 | **Step 4:** check Server-Timing in the network tab of Chrome DevTools. 38 | 39 | ![screenshot from chrome](https://raw.githubusercontent.com/SilentImp/express-middleware-headers-server-timing/master/devtools.png) 40 | 41 | ## Examples 42 | 43 |
Measure time between two points 44 | 45 | Most common use-case — measure time between two points. 46 | 47 | ```javascript 48 | const express = require('express'); 49 | const serverTimingMiddleware = require('server-timing-header'); 50 | const port = 3000; 51 | const app = express(); 52 | app.use(serverTimingMiddleware({sendHeaders: (process.env.NODE_ENV !== 'production')})); 53 | app.get('/', function (req, res, next) { 54 | req.serverTiming.from('db'); 55 | // fetching data from database 56 | req.serverTiming.to('db'); 57 | }); 58 | app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 59 | ``` 60 | 61 |
62 | 63 |
Add metric manually 64 | 65 | In case we recieve timing from external source we may just add metric we need. 66 | 67 | ```javascript 68 | const express = require('express'); 69 | const serverTimingMiddleware = require('server-timing-header'); 70 | const port = 3000; 71 | const app = express(); 72 | app.use(serverTimingMiddleware()); 73 | app.get('/', function (req, res, next) { 74 | // You got time metric from the external source 75 | req.serverTiming.add('cache', 'Cache Read', 23.2); 76 | }); 77 | app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 78 | ``` 79 | 80 |
81 | 82 |
Add hook to modify data before send 83 | 84 | In some cases you may need to modify data before send it to browser. In example bellow we can't separate time of rendering and time of acquiring data. To make render time more precise we may devide time we use to get data from the rendering time. 85 | 86 | ```javascript 87 | const express = require('express'); 88 | const serverTimingMiddleware = require('server-timing-header'); 89 | const port = 3000; 90 | const app = express(); 91 | app.use(serverTimingMiddleware()); 92 | app.get('/', function (req, res, next) { 93 | req.serverTiming.from('render'); 94 | req.serverTiming.from('data'); 95 | // fetching data from database 96 | req.serverTiming.to('data'); 97 | req.serverTiming.to('render'); 98 | }); 99 | app.use(function (req, res, next) { 100 | // If one measurement include other inside you may substract times 101 | req.serverTiming.addHook('substractDataTimeFromRenderTime', function (metrics) { 102 | const updated = { ...metrics }; 103 | if (updated.data && updated.render) { 104 | const renderDuration = req.serverTiming.calculateDurationSmart(updated.render); 105 | const dataDuration = req.serverTiming.calculateDurationSmart(updated.data); 106 | updated.render.duration = Math.abs(renderDuration - dataDuration); 107 | } 108 | return updated; 109 | }); 110 | }); 111 | app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 112 | ``` 113 | 114 |
115 | 116 |
Result in the Chrome DevTools 117 | 118 | ![screenshot from chrome](https://raw.githubusercontent.com/SilentImp/express-middleware-headers-server-timing/master/devtools.png) 119 | 120 |
121 | 122 |
Result in the Safari DevTools 123 | 124 | ![screenshot from safari](https://raw.githubusercontent.com/SilentImp/express-middleware-headers-server-timing/master/devtools-safari.png) 125 | 126 |
127 | 128 |
Access metrics values from JavaScript on a client 129 | 130 | You may access data from JavaScript with help of [PerformanceServerTiming](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceServerTiming). 131 | 132 | ```javascript 133 | ['navigation', 'resource'] 134 | .forEach(function(entryType) { 135 | performance.getEntriesByType(entryType).forEach(function({name: url, serverTiming}) { 136 | serverTiming.forEach(function({name, duration, description}) { 137 | console.info('expressjs middleware =', 138 | JSON.stringify({url, entryType, name, duration, description}, null, 2)) 139 | }) 140 | }) 141 | }) 142 | ``` 143 | 144 | ![screenshot](https://raw.githubusercontent.com/SilentImp/express-middleware-headers-server-timing/master/console.png) 145 | 146 |
147 | 148 | ## Support 149 | 150 | [Current support:](https://caniuse.com/#feat=server-timing) 151 | 152 | - Chrome v.60 153 | - FF v.63 154 | - Safari v.12.1 _(no api support)_ 155 | 156 | # Documentation 157 | 158 | 159 | 160 | ### Table of Contents 161 | 162 | - [ServerTiming](#servertiming) 163 | - [addHook](#addhook) 164 | - [Parameters](#parameters) 165 | - [Examples](#examples) 166 | - [removeHook](#removehook) 167 | - [Parameters](#parameters-1) 168 | - [from](#from) 169 | - [Parameters](#parameters-2) 170 | - [Examples](#examples-1) 171 | - [to](#to) 172 | - [Parameters](#parameters-3) 173 | - [Examples](#examples-2) 174 | - [description](#description) 175 | - [Parameters](#parameters-4) 176 | - [duration](#duration) 177 | - [Parameters](#parameters-5) 178 | - [add](#add) 179 | - [Parameters](#parameters-6) 180 | - [Examples](#examples-3) 181 | - [calculateDurationSmart](#calculatedurationsmart) 182 | - [Parameters](#parameters-7) 183 | - [oldStyle](#oldstyle) 184 | - [Parameters](#parameters-8) 185 | - [newStyle](#newstyle) 186 | - [Parameters](#parameters-9) 187 | - [index](#index) 188 | - [Parameters](#parameters-10) 189 | - [Examples](#examples-4) 190 | 191 | ## ServerTiming 192 | 193 | - **See: ** 194 | 195 | Middleware for express.js to add Server Timing headers 196 | 197 | **Meta** 198 | 199 | - **author**: Anton Nemtsev <thesilentimp@gmail.com> 200 | 201 | ### addHook 202 | 203 | Add callback to modify data before create and send headers 204 | 205 | #### Parameters 206 | 207 | - `name` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** — hook name 208 | - `callback` **[function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** — function that may modify data before send headers 209 | - `callbackIndex` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** index that will be used to sort callbacks before execution 210 | 211 | #### Examples 212 | 213 | Add hook to mutate the metrics 214 | 215 | 216 | ```javascript 217 | const express = require('express'); 218 | const serverTimingMiddleware = require('server-timing-header'); 219 | const port = 3000; 220 | const app = express(); 221 | app.use(serverTimingMiddleware()); 222 | app.use(function (req, res, next) { 223 | // If one measurement include other inside you may substract times 224 | req.serverTiming.addHook('substractDataTimeFromRenderTime', function (metrics) { 225 | const updated = { ...metrics }; 226 | if (updated.data && updated.render) { 227 | const renderDuration = req.serverTiming.calculateDurationSmart(updated.render); 228 | const dataDuration = req.serverTiming.calculateDurationSmart(updated.data); 229 | updated.render.duration = Math.abs(renderDuration - dataDuration); 230 | } 231 | return updated; 232 | }); 233 | }); 234 | app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 235 | ``` 236 | 237 | ### removeHook 238 | 239 | Remove callback with specific name 240 | 241 | #### Parameters 242 | 243 | - `name` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** — hook name 244 | 245 | ### from 246 | 247 | Set start time for metric 248 | 249 | #### Parameters 250 | 251 | - `name` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** — metric name 252 | - `description` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** — description of the metric 253 | 254 | #### Examples 255 | 256 | You may define only start time for metric 257 | 258 | 259 | ```javascript 260 | const express = require('express'); 261 | const serverTimingMiddleware = require('server-timing-header'); 262 | const port = 3000; 263 | const app = express(); 264 | app.use(serverTimingMiddleware()); 265 | app.get('/', function (req, res, next) { 266 | // If you define only start time for metric, 267 | // then as the end time will be used header sent time 268 | req.serverTiming.from('metric', 'metric description'); 269 | // fetching data from database 270 | }); 271 | app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 272 | ``` 273 | 274 | ### to 275 | 276 | Set end time for metric 277 | 278 | #### Parameters 279 | 280 | - `name` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** — metric name 281 | - `description` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** — description of the metric 282 | 283 | #### Examples 284 | 285 | You may define only end time for metric 286 | 287 | 288 | ```javascript 289 | const express = require('express'); 290 | const serverTimingMiddleware = require('server-timing-header'); 291 | const port = 3000; 292 | const app = express(); 293 | app.use(serverTimingMiddleware()); 294 | app.get('/', function (req, res, next) { 295 | // fetching data from database 296 | // If you define only end time for metric, 297 | // then as the start time will be used middleware initialization time 298 | req.serverTiming.to('metric'); 299 | }); 300 | app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 301 | ``` 302 | 303 | ### description 304 | 305 | Add description to specific metric 306 | 307 | #### Parameters 308 | 309 | - `name` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** — metric name 310 | - `description` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** — description of the metric 311 | 312 | ### duration 313 | 314 | Add duration to specific metric 315 | 316 | #### Parameters 317 | 318 | - `name` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** — metric name 319 | - `duration` **float** — duration of the metric 320 | 321 | ### add 322 | 323 | Add metric 324 | 325 | #### Parameters 326 | 327 | - `name` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** metric name 328 | - `description` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** — metric description 329 | - `duration` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** — metric duration (optional, default `0.0`) 330 | 331 | #### Examples 332 | 333 | Add metric 334 | 335 | 336 | ```javascript 337 | const express = require('express'); 338 | const serverTimingMiddleware = require('server-timing-header'); 339 | const port = 3000; 340 | const app = express(); 341 | app.use(serverTimingMiddleware()); 342 | app.get('/', function (req, res, next) { 343 | // You got time metric from the external source 344 | req.serverTiming.add('metric', 'metric description', 52.3); 345 | }); 346 | app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 347 | ``` 348 | 349 | ### calculateDurationSmart 350 | 351 | Calculate duration between two timestamps, if from or two is undefined — will use initialization time and current time to replace 352 | 353 | #### Parameters 354 | 355 | - `metric` **[object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** — object that contain metric information 356 | - `metric.name` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** — metric name 357 | - `metric.description` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** — metric description 358 | - `metric.from` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<integer>** — start time [seconds, nanoseconds], if undefined, initialization time will be used 359 | - `metric.to` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<integer>** — end time [seconds, nanoseconds], if undefined, current timestamp will be used 360 | - `metric.duration` **integer** — time in milliseconds, if not undefined method will just return durations 361 | 362 | Returns **integer** duration in milliseconds 363 | 364 | ### oldStyle 365 | 366 | Build server-timing header value by old specification 367 | 368 | #### Parameters 369 | 370 | - `name` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** metric name 371 | - `description` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** metric description 372 | - `duration` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** metric duration 373 | 374 | Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** — server-timing header value 375 | 376 | ### newStyle 377 | 378 | Build server-timing header value by current specification 379 | 380 | #### Parameters 381 | 382 | - `name` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** metric name 383 | - `description` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** metric description 384 | - `duration` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** metric duration 385 | 386 | Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** — server-timing header value 387 | 388 | ## index 389 | 390 | Express middleware add serverTiming to request and 391 | make sure that we will send this headers before express finish request 392 | 393 | ### Parameters 394 | 395 | - `options` **[object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)?** — middleware options (optional, default `{}`) 396 | - `options.sendHeaders` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?** should middleware send headers (may be disabled for some environments) (optional, default `true`) 397 | 398 | ### Examples 399 | 400 | How to add middleware 401 | 402 | 403 | ```javascript 404 | const express = require('express'); 405 | const serverTimingMiddleware = require('server-timing-header'); 406 | const port = 3000; 407 | const app = express(); 408 | app.use(serverTimingMiddleware()); 409 | app.get('/', function (req, res, next) { 410 | req.serverTiming.from('db'); 411 | // fetching data from database 412 | req.serverTiming.to('db'); 413 | }); 414 | app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 415 | ``` 416 | 417 | Returns **[function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** return express middleware 418 | -------------------------------------------------------------------------------- /console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilentImp/express-middleware-headers-server-timing/e2dd96eed39f9af49d55e2aff26731ce6c3d421d/console.png -------------------------------------------------------------------------------- /devtools-safari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilentImp/express-middleware-headers-server-timing/e2dd96eed39f9af49d55e2aff26731ce6c3d421d/devtools-safari.png -------------------------------------------------------------------------------- /devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilentImp/express-middleware-headers-server-timing/e2dd96eed39f9af49d55e2aff26731ce6c3d421d/devtools.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const onHeaders = require('on-headers') 2 | 3 | const HEADER_NAME = 'server-timing' 4 | const INVALID_NAME = 'Name contain forbidden symbols' 5 | const HEADERS_SENT = 'Headers was already sent and we can not add new headers' 6 | 7 | /** 8 | * Middleware for express.js to add Server Timing headers 9 | * 10 | * @namespace ServerTiming 11 | * @class ServerTiming 12 | * @see https://w3c.github.io/server-timing/ 13 | * @author Anton Nemtsev 14 | * 15 | */ 16 | class ServerTiming { 17 | /** 18 | * Create server timing controller 19 | * @constructor 20 | * @param {string} [userAgent] — string that contain user agent description 21 | * @param {boolean} [sendHeaders=true] - you may send or don't send headers depending on environment 22 | */ 23 | constructor (userAgent = '', sendHeaders = true) { 24 | // Before 64 version Chrome support old server-timing 25 | // specification with different syntax 26 | const isChrome = userAgent.indexOf(' Chrome/') > -1 27 | const chromeData = / Chrome\/([\d]+)./gi.exec(userAgent) 28 | const chromeVersion = 29 | chromeData === null ? null : parseInt(chromeData[1], 10) 30 | const isCanary = isChrome && chromeVersion > 64 31 | this.oldSpecification = isChrome && !isCanary 32 | 33 | /** 34 | * If start time is not specified for metric 35 | * we will use time of middleware initialization 36 | * @private 37 | * @type {integer[]} - time of middleware initialization [seconds, nanoseconds] 38 | */ 39 | this.initialized = process.hrtime() 40 | 41 | /** 42 | * Should middleware send headers 43 | * @private 44 | * @type {boolean} - if false middleware will not add headers 45 | */ 46 | this.sendHeaders = sendHeaders 47 | 48 | /** 49 | * @private 50 | * @type {object} - We will store time metrics in this object 51 | */ 52 | this.metrics = {} 53 | 54 | /** 55 | * @private 56 | * @type {array} - Array of callbacks 57 | */ 58 | this.hooks = [] 59 | 60 | // We should keep consistent context for non static methods 61 | Object.getOwnPropertyNames(Object.getPrototypeOf(this)).forEach(name => { 62 | const method = this[name] 63 | if (name !== 'constructor' && typeof method === 'function') { 64 | this[name] = method.bind(this) 65 | } 66 | }) 67 | } 68 | 69 | /** 70 | * Add callback to modify data before create and send headers 71 | * @public 72 | * @param {string} name — hook name 73 | * @param {function} callback — function that may modify data before send headers 74 | * @param {number} callbackIndex - index that will be used to sort callbacks before execution 75 | * @example Add hook to mutate the metrics 76 | * const express = require('express'); 77 | * const serverTimingMiddleware = require('server-timing-header'); 78 | * const port = 3000; 79 | * const app = express(); 80 | * app.use(serverTimingMiddleware()); 81 | * app.use(function (req, res, next) { 82 | * // If one measurement include other inside you may substract times 83 | * req.serverTiming.addHook('substractDataTimeFromRenderTime', function (metrics) { 84 | * const updated = { ...metrics }; 85 | * if (updated.data && updated.render) { 86 | * const renderDuration = req.serverTiming.calculateDurationSmart(updated.render); 87 | * const dataDuration = req.serverTiming.calculateDurationSmart(updated.data); 88 | * updated.render.duration = Math.abs(renderDuration - dataDuration); 89 | * } 90 | * return updated; 91 | * }); 92 | * }); 93 | * app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 94 | */ 95 | addHook (name, callback, callbackIndex) { 96 | let index = callbackIndex 97 | if (index === undefined) { 98 | index = this.hooks.length + 1 99 | } 100 | this.hooks.push({ 101 | name, 102 | callback, 103 | index 104 | }) 105 | } 106 | 107 | /** 108 | * Remove callback with specific name 109 | * @public 110 | * @param {string} name — hook name 111 | */ 112 | removeHook (name) { 113 | this.hooks = this.hooks.filter( 114 | ({ name: callbackName }) => callbackName !== name 115 | ) 116 | } 117 | 118 | /** 119 | * Set start time for metric 120 | * @public 121 | * @param {string} name — metric name 122 | * @param {string} [description] — description of the metric 123 | * @throw {Error} — throw an error if name is not valid 124 | * @example You may define only start time for metric 125 | * const express = require('express'); 126 | * const serverTimingMiddleware = require('server-timing-header'); 127 | * const port = 3000; 128 | * const app = express(); 129 | * app.use(serverTimingMiddleware()); 130 | * app.get('/', function (req, res, next) { 131 | * // If you define only start time for metric, 132 | * // then as the end time will be used header sent time 133 | * req.serverTiming.from('metric', 'metric description'); 134 | * // fetching data from database 135 | * }); 136 | * app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 137 | */ 138 | from (name, description) { 139 | this.set(name, 'from', process.hrtime()) 140 | if (description) this.description(name, description) 141 | } 142 | 143 | /** 144 | * Set end time for metric 145 | * @public 146 | * @param {string} name — metric name 147 | * @param {string} [description] — description of the metric 148 | * @throw {Error} — throw an error if name is not valid 149 | * @example You may define only end time for metric 150 | * const express = require('express'); 151 | * const serverTimingMiddleware = require('server-timing-header'); 152 | * const port = 3000; 153 | * const app = express(); 154 | * app.use(serverTimingMiddleware()); 155 | * app.get('/', function (req, res, next) { 156 | * // fetching data from database 157 | * // If you define only end time for metric, 158 | * // then as the start time will be used middleware initialization time 159 | * req.serverTiming.to('metric'); 160 | * }); 161 | * app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 162 | */ 163 | to (name, description) { 164 | this.set(name, 'to', process.hrtime()) 165 | if (description) this.description(name, description) 166 | } 167 | 168 | /** 169 | * Add description to specific metric 170 | * @public 171 | * @param {string} name — metric name 172 | * @param {string} description — description of the metric 173 | * @throw {Error} — throw an error if name is not valid 174 | */ 175 | description (name, description) { 176 | this.set(name, 'description', description) 177 | } 178 | 179 | /** 180 | * Add duration to specific metric 181 | * @public 182 | * @param {string} name — metric name 183 | * @param {float} duration — duration of the metric 184 | * @throw {Error} — throw an error if name is not valid 185 | */ 186 | duration (name, duration) { 187 | this.set(name, 'duration', duration) 188 | } 189 | 190 | /** 191 | * Add property for metric 192 | * or create new metric with this property 193 | * if metric with this name not found 194 | * @private 195 | * @param {string} name - metric name 196 | * @param {string} field - property name 197 | * @param {mixed} value — property value 198 | * @throw {Error} — throw an error if name contains invalid characters 199 | */ 200 | set (name, field, value) { 201 | if (!ServerTiming.nameIsValid(name)) throw new Error(INVALID_NAME) 202 | if (typeof this.metrics[name] === 'undefined') { 203 | this.metrics[name] = { [[field]]: value } 204 | } else { 205 | this.metrics[name][field] = value 206 | } 207 | } 208 | 209 | /** 210 | * Add metric 211 | * @param {string} name - metric name 212 | * @param {string} description — metric description 213 | * @param {number} duration — metric duration 214 | * @throw {Error} — throw an error if name contains invalid characters 215 | * @example Add metric 216 | * const express = require('express'); 217 | * const serverTimingMiddleware = require('server-timing-header'); 218 | * const port = 3000; 219 | * const app = express(); 220 | * app.use(serverTimingMiddleware()); 221 | * app.get('/', function (req, res, next) { 222 | * // You got time metric from the external source 223 | * req.serverTiming.add('metric', 'metric description', 52.3); 224 | * }); 225 | * app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 226 | */ 227 | add (name, description, duration = 0.0) { 228 | if (!ServerTiming.nameIsValid(name)) throw new Error(INVALID_NAME) 229 | this.metrics[name] = { 230 | description, 231 | duration 232 | } 233 | } 234 | 235 | /** 236 | * Send current set of server timing headers 237 | * @private 238 | * @param {object} response — express.js response object 239 | * @see https://expressjs.com/en/4x/api.html#res 240 | * @example How to add middleware 241 | * const express = require('express'); 242 | * const serverTimingMiddleware = require('server-timing-header'); 243 | * const port = 3000; 244 | * const app = express(); 245 | * app.use(serverTimingMiddleware()); 246 | * app.get('/', function (req, res, next) { 247 | * req.serverTiming.from('db'); 248 | * // fetching data from database 249 | * req.serverTiming.to('db'); 250 | * }); 251 | * app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 252 | */ 253 | addHeaders (response) { 254 | if (!this.addHeaders) return 255 | if (response.headerSent) throw new Error(HEADERS_SENT) 256 | const updatedMetrics = this.hooks 257 | .sort(({ index: indexA }, { index: indexB }) => indexA - indexB) 258 | .map(({ callback }) => callback) 259 | .reduce((metrics, callback) => { 260 | return callback(metrics) 261 | }, this.metrics) 262 | let metrics = Object.entries(updatedMetrics).reduce( 263 | (collector, element) => { 264 | const [name, { from, to, description, duration }] = element 265 | collector.push( 266 | ServerTiming.buildHeader( 267 | { 268 | name, 269 | description, 270 | from: from || this.initialized, 271 | to: to || process.hrtime(), 272 | duration 273 | }, 274 | this.oldSpecification 275 | ) 276 | ) 277 | return collector 278 | }, 279 | [] 280 | ) 281 | 282 | if ( 283 | Array.isArray(response.headers['server-timing']) && 284 | response.headers['server-timing'].length > 0 285 | ) { 286 | metrics = [ 287 | ...response.headers['server-timing'], 288 | ...metrics 289 | ] 290 | } 291 | 292 | if (metrics.length > 0) response.set(HEADER_NAME, metrics) 293 | this.metrics = {} 294 | } 295 | 296 | /** 297 | * Build server-timing header value by old specification 298 | * @param {string} name - metric name 299 | * @param {string} description - metric description 300 | * @param {string} duration - metric duration 301 | * @return {string} — server-timing header value 302 | */ 303 | static oldStyle (name, description, duration) { 304 | return `${name}${typeof duration !== 'undefined' ? `=${duration}` : ''}${ 305 | typeof description !== 'undefined' ? `; "${description}"` : '' 306 | }` 307 | } 308 | 309 | /** 310 | * Build server-timing header value by current specification 311 | * @param {string} name - metric name 312 | * @param {string} description - metric description 313 | * @param {string} duration - metric duration 314 | * @return {string} — server-timing header value 315 | */ 316 | static newStyle (name, description, duration) { 317 | return `${name}${ 318 | typeof description !== 'undefined' ? `;desc="${description}"` : '' 319 | }${typeof duration !== 'undefined' ? `;dur=${duration}` : ''}` 320 | } 321 | 322 | /** 323 | * Build server timing headers 324 | * @static 325 | * @private 326 | * @param {object} metric — object that contain metric information 327 | * @param {string} metric.name — metric name 328 | * @param {string} metric.description — metric description 329 | * @param {integer[]} metric.from — start time [seconds, nanoseconds] 330 | * @param {integer[]} metric.to — end time [seconds, nanoseconds] 331 | * @return {string} — header value with timings for specific metric 332 | */ 333 | static buildHeader ( 334 | { name, description, duration, from, to }, 335 | oldSpecification = false 336 | ) { 337 | const time = duration || ServerTiming.calculateDuration(from, to) 338 | return oldSpecification 339 | ? ServerTiming.oldStyle(name, description, time) 340 | : ServerTiming.newStyle(name, description, time) 341 | } 342 | 343 | /** 344 | * Calculate duration between two timestamps, if from or two is undefined — will use initialization time and current time to replace 345 | * @public 346 | * @param {object} metric — object that contain metric information 347 | * @param {string} metric.name — metric name 348 | * @param {string} metric.description — metric description 349 | * @param {integer[]} metric.from — start time [seconds, nanoseconds], if undefined, initialization time will be used 350 | * @param {integer[]} metric.to — end time [seconds, nanoseconds], if undefined, current timestamp will be used 351 | * @param {integer} metric.duration — time in milliseconds, if not undefined method will just return durations 352 | * @return {integer} - duration in milliseconds 353 | */ 354 | calculateDurationSmart (metric) { 355 | const fromLabel = metric.from || this.initialized 356 | const toLabel = metric.to || process.hrtime() 357 | return ( 358 | metric.duration || ServerTiming.calculateDuration(fromLabel, toLabel) 359 | ) 360 | } 361 | 362 | /** 363 | * Calculate duration between two timestamps 364 | * @static 365 | * @private 366 | * @param {integer[]} from — start time [seconds, nanoseconds] 367 | * @param {integer[]} to — end time [seconds, nanoseconds] 368 | * @return {integer} - duration in milliseconds 369 | */ 370 | static calculateDuration (from, to) { 371 | const fromTime = parseInt(from[0] * 1e3 + from[1] * 1e-6, 10) 372 | const toTime = parseInt(to[0] * 1e3 + to[1] * 1e-6, 10) 373 | return Math.abs(toTime - fromTime) 374 | } 375 | 376 | /** 377 | * Check if metric name is valid 378 | * (),/:;<=>?@[\]{}" Don't allowed 379 | * Minimal length is one symbol 380 | * Digits, alphabet characters, 381 | * and !#$%&'*+-.^_`|~ are allowed 382 | * 383 | * @static 384 | * @private 385 | * @see https://www.w3.org/TR/2019/WD-server-timing-20190307/#the-server-timing-header-field 386 | * @see https://tools.ietf.org/html/rfc7230#section-3.2.6 387 | * @param {string} name — metric name 388 | * @return {boolean} — is name valid 389 | */ 390 | static nameIsValid (name) { 391 | return /^[!#$%&'*+\-.^_`|~0-9a-z]+$/gi.test(name) 392 | } 393 | } 394 | 395 | /** 396 | * Express middleware add serverTiming to request and 397 | * make sure that we will send this headers before express finish request 398 | * @exports serverTimingMiddleware 399 | * @param {object} [options] — middleware options 400 | * @param {boolean} [options.sendHeaders] - should middleware send headers (may be disabled for some environments) 401 | * @return {function} - return express middleware 402 | * @example How to add middleware 403 | * const express = require('express'); 404 | * const serverTimingMiddleware = require('server-timing-header'); 405 | * const port = 3000; 406 | * const app = express(); 407 | * app.use(serverTimingMiddleware()); 408 | * app.get('/', function (req, res, next) { 409 | * req.serverTiming.from('db'); 410 | * // fetching data from database 411 | * req.serverTiming.to('db'); 412 | * }); 413 | * app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 414 | */ 415 | module.exports = ({ sendHeaders = true } = {}) => { 416 | function serverTimingMiddleware (request, response, next) { 417 | // Adding controller to request object 418 | request.serverTiming = new ServerTiming( 419 | request.header('user-agent'), 420 | sendHeaders 421 | ) 422 | 423 | // We should send server-timing headers before headers are sent 424 | if (sendHeaders) { 425 | onHeaders(response, () => { 426 | request.serverTiming.addHeaders(response) 427 | }) 428 | } 429 | 430 | next() 431 | } 432 | 433 | return serverTimingMiddleware 434 | } 435 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | const { Request } = require('jest-express/lib/request') 2 | const { Response } = require('jest-express/lib/response') 3 | 4 | const middleware = require('./index.js') 5 | 6 | const modernChrome = () => 7 | 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3809.100 Safari/537.36' 8 | const oldChrome = () => 9 | 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3809.100 Safari/537.36' 10 | 11 | describe('server Timing middleware should', () => { 12 | it('add serverTiming object to request object', () => { 13 | expect.assertions(1) 14 | const next = jest.fn() 15 | const request = new Request() 16 | request.header = modernChrome 17 | const response = new Response() 18 | 19 | middleware()(request, response, next) 20 | 21 | expect(request).toHaveProperty('serverTiming') 22 | }) 23 | 24 | it('pass request to next middleware', () => { 25 | expect.assertions(1) 26 | const next = jest.fn() 27 | const request = new Request() 28 | request.header = modernChrome 29 | const response = new Response() 30 | 31 | middleware()(request, response, next) 32 | expect(next).toHaveBeenCalledWith() 33 | }) 34 | 35 | it('allow measure time between two points', () => { 36 | expect.assertions(3) 37 | const next = jest.fn() 38 | const request = new Request() 39 | request.header = modernChrome 40 | const response = new Response() 41 | 42 | middleware({ sendHeaders: false })(request, response, next) 43 | 44 | request.serverTiming.from( 45 | 'userData', 46 | 'getting user data from user microservice' 47 | ) 48 | request.serverTiming.to('userData') 49 | request.serverTiming.addHeaders(response) 50 | 51 | expect(response.headers).toHaveProperty('server-timing') 52 | expect(response.headers['server-timing']).toHaveLength(1) 53 | expect(response.headers['server-timing'][0]).toStrictEqual( 54 | expect.stringContaining( 55 | 'userData;desc="getting user data from user microservice";dur=' 56 | ) 57 | ) 58 | }) 59 | 60 | it('allow measure time between two points more than once', () => { 61 | expect.assertions(4) 62 | const next = jest.fn() 63 | const request = new Request() 64 | request.header = modernChrome 65 | const response = new Response() 66 | 67 | middleware({ sendHeaders: false })(request, response, next) 68 | 69 | request.serverTiming.from( 70 | 'userData', 71 | 'getting user data from user microservice' 72 | ) 73 | request.serverTiming.to('userData') 74 | request.serverTiming.from( 75 | 'itemData', 76 | 'getting item data from item microservice' 77 | ) 78 | request.serverTiming.to('userData') 79 | request.serverTiming.addHeaders(response) 80 | 81 | expect(response.headers).toHaveProperty('server-timing') 82 | expect(response.headers['server-timing']).toHaveLength(2) 83 | expect(response.headers['server-timing'][0]).toStrictEqual( 84 | expect.stringContaining( 85 | 'userData;desc="getting user data from user microservice";dur=' 86 | ) 87 | ) 88 | expect(response.headers['server-timing'][1]).toStrictEqual( 89 | expect.stringContaining( 90 | 'itemData;desc="getting item data from item microservice";dur=' 91 | ) 92 | ) 93 | }) 94 | 95 | it('allow add server timing with exact duration', () => { 96 | expect.assertions(3) 97 | const next = jest.fn() 98 | const request = new Request() 99 | request.header = modernChrome 100 | const response = new Response() 101 | 102 | middleware({ sendHeaders: false })(request, response, next) 103 | 104 | request.serverTiming.add( 105 | 'userData', 106 | 'getting user data from user microservice', 107 | 123 108 | ) 109 | request.serverTiming.addHeaders(response) 110 | 111 | expect(response.headers).toHaveProperty('server-timing') 112 | expect(response.headers['server-timing']).toHaveLength(1) 113 | expect(response.headers['server-timing']).toContainEqual( 114 | 'userData;desc="getting user data from user microservice";dur=123' 115 | ) 116 | }) 117 | 118 | it('allow add server timing with exact duration more then once', () => { 119 | expect.assertions(4) 120 | const next = jest.fn() 121 | const request = new Request() 122 | request.header = modernChrome 123 | const response = new Response() 124 | 125 | middleware({ sendHeaders: false })(request, response, next) 126 | 127 | request.serverTiming.add( 128 | 'userData', 129 | 'getting user data from user microservice', 130 | 123 131 | ) 132 | request.serverTiming.add( 133 | 'itemData', 134 | 'getting item data from item microservice', 135 | 234 136 | ) 137 | request.serverTiming.addHeaders(response) 138 | 139 | expect(response.headers).toHaveProperty('server-timing') 140 | expect(response.headers['server-timing']).toHaveLength(2) 141 | expect(response.headers['server-timing']).toContainEqual( 142 | 'userData;desc="getting user data from user microservice";dur=123' 143 | ) 144 | expect(response.headers['server-timing']).toContainEqual( 145 | 'itemData;desc="getting item data from item microservice";dur=234' 146 | ) 147 | }) 148 | 149 | it('allow to overwrite server timing duration', () => { 150 | expect.assertions(3) 151 | const next = jest.fn() 152 | const request = new Request() 153 | request.header = modernChrome 154 | const response = new Response() 155 | 156 | middleware({ sendHeaders: false })(request, response, next) 157 | 158 | request.serverTiming.add( 159 | 'userData', 160 | 'getting user data from user microservice', 161 | 123 162 | ) 163 | request.serverTiming.duration('userData', 234) 164 | request.serverTiming.addHeaders(response) 165 | 166 | expect(response.headers).toHaveProperty('server-timing') 167 | expect(response.headers['server-timing']).toHaveLength(1) 168 | expect(response.headers['server-timing']).toContainEqual( 169 | 'userData;desc="getting user data from user microservice";dur=234' 170 | ) 171 | }) 172 | 173 | it('allow to overwrite server timing description', () => { 174 | expect.assertions(3) 175 | const next = jest.fn() 176 | const request = new Request() 177 | request.header = modernChrome 178 | const response = new Response() 179 | 180 | middleware({ sendHeaders: false })(request, response, next) 181 | 182 | request.serverTiming.add('userData', 'bla bla bla', 123) 183 | 184 | request.serverTiming.description( 185 | 'userData', 186 | 'getting user data from user microservice' 187 | ) 188 | 189 | request.serverTiming.addHeaders(response) 190 | 191 | expect(response.headers).toHaveProperty('server-timing') 192 | expect(response.headers['server-timing']).toHaveLength(1) 193 | expect(response.headers['server-timing']).toContainEqual( 194 | 'userData;desc="getting user data from user microservice";dur=123' 195 | ) 196 | }) 197 | 198 | it('should use old standards for Chrome v60-64', () => { 199 | expect.assertions(3) 200 | const next = jest.fn() 201 | const request = new Request() 202 | request.header = oldChrome 203 | const response = new Response() 204 | 205 | middleware({ sendHeaders: false })(request, response, next) 206 | 207 | request.serverTiming.add( 208 | 'userData', 209 | 'getting user data from user microservice', 210 | 123 211 | ) 212 | request.serverTiming.duration('userData', 234) 213 | request.serverTiming.addHeaders(response) 214 | 215 | expect(response.headers).toHaveProperty('server-timing') 216 | expect(response.headers['server-timing']).toHaveLength(1) 217 | expect(response.headers['server-timing']).toContainEqual( 218 | 'userData=234; "getting user data from user microservice"' 219 | ) 220 | }) 221 | 222 | it('would not rewrite headers added by other application', () => { 223 | expect.assertions(3) 224 | const next = jest.fn() 225 | const request = new Request() 226 | const response = new Response() 227 | const presentHeader = 'db;dur=53, app;dur=47.2' 228 | response.headers = { 229 | 'server-timing': [ 230 | presentHeader 231 | ] 232 | } 233 | 234 | middleware({ sendHeaders: false })(request, response, next) 235 | 236 | request.serverTiming.add( 237 | 'userData', 238 | 'getting user data from user microservice', 239 | 123 240 | ) 241 | request.serverTiming.duration('userData', 234) 242 | request.serverTiming.addHeaders(response) 243 | 244 | expect(response.headers).toHaveProperty('server-timing') 245 | expect(response.headers['server-timing']).toHaveLength(2) 246 | expect(response.headers['server-timing'].includes(presentHeader)).toBe(true) 247 | }) 248 | 249 | it('allow add hook to modify the data before add headers', () => { 250 | expect.assertions(2) 251 | const next = jest.fn() 252 | const request = new Request() 253 | request.header = modernChrome 254 | const response = new Response() 255 | 256 | middleware({ sendHeaders: false })(request, response, next) 257 | 258 | request.serverTiming.addHook('substractDataTimeFromRenderTime', metrics => { 259 | const updated = { ...metrics } 260 | if (updated.data && updated.render) { 261 | const renderDuration = request.serverTiming.calculateDurationSmart( 262 | updated.render 263 | ) 264 | const dataDuration = request.serverTiming.calculateDurationSmart( 265 | updated.data 266 | ) 267 | updated.render.duration = Math.abs(renderDuration - dataDuration) 268 | } 269 | return updated 270 | }) 271 | 272 | request.serverTiming.add('render', 'rendering app', 600) 273 | request.serverTiming.add('data', 'getting data from microservices', 550) 274 | 275 | request.serverTiming.addHeaders(response) 276 | 277 | expect(response.headers).toHaveProperty('server-timing') 278 | expect(response.headers['server-timing']).toContainEqual( 279 | 'render;desc="rendering app";dur=50' 280 | ) 281 | }) 282 | 283 | it('allow remove hook', () => { 284 | expect.assertions(2) 285 | const next = jest.fn() 286 | const request = new Request() 287 | request.header = modernChrome 288 | const response = new Response() 289 | 290 | middleware({ sendHeaders: false })(request, response, next) 291 | 292 | request.serverTiming.addHook('substractDataTimeFromRenderTime', metrics => { 293 | const updated = { ...metrics } 294 | if (updated.data && updated.render) { 295 | const renderDuration = request.serverTiming.calculateDurationSmart( 296 | updated.render 297 | ) 298 | const dataDuration = request.serverTiming.calculateDurationSmart( 299 | updated.data 300 | ) 301 | updated.render.duration = Math.abs(renderDuration - dataDuration) 302 | } 303 | return updated 304 | }) 305 | 306 | request.serverTiming.add('render', 'rendering app', 600) 307 | request.serverTiming.add('data', 'getting data from microservices', 550) 308 | 309 | request.serverTiming.removeHook('substractDataTimeFromRenderTime') 310 | 311 | request.serverTiming.addHeaders(response) 312 | 313 | expect(response.headers).toHaveProperty('server-timing') 314 | expect(response.headers['server-timing']).toContainEqual( 315 | 'render;desc="rendering app";dur=600' 316 | ) 317 | }) 318 | }) 319 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: { 3 | name: 'Server Timing', 4 | color: 'blue' 5 | }, 6 | collectCoverage: true, 7 | coverageThreshold: { 8 | global: { 9 | lines: 90 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server-timing-header", 3 | "version": "1.9.7", 4 | "description": "Allow you add metrics via Server-Timing header", 5 | "main": "index.js", 6 | "scripts": { 7 | "checks": "npm run lint && npm run size && npm t", 8 | "size": "size-limit", 9 | "postversion": "git push && git push --tags", 10 | "version": "git add . -A", 11 | "premajor": "npm run checks", 12 | "major": "npm version major && npm publish --tag latest --access public", 13 | "preminor": "npm run checks", 14 | "minor": "npm version minor && npm publish --tag latest --access public", 15 | "prepatch": "npm run checks", 16 | "patch": "npm version patch && npm publish --tag latest --access public", 17 | "predopreminor": "npm run checks", 18 | "dopreminor": "npm version preminor && npm publish --tag next --access public", 19 | "predopremajor": "npm run checks", 20 | "dopremajor": "npm version premajor && npm publish --tag next --access public", 21 | "predoprepatch": "npm run checks", 22 | "doprepatch": "npm version prepatch && npm publish --tag next --access public", 23 | "doc": "documentation readme index.js -s 'Documentation'", 24 | "lint": "eslint *.js --fix", 25 | "lint-staged": "lint-staged", 26 | "test": "jest", 27 | "prepare": "husky install" 28 | }, 29 | "lint-staged": { 30 | "*.js": [ 31 | "eslint --fix", 32 | "git add", 33 | "npm t -- --onlyChanged --passWithNoTests" 34 | ] 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/SilentImp/express-middleware-headers-server-timing.git" 39 | }, 40 | "size-limit": [ 41 | { 42 | "limit": "3 KB", 43 | "path": "index.js" 44 | } 45 | ], 46 | "keywords": [ 47 | "expressjs", 48 | "performance", 49 | "server", 50 | "timing", 51 | "servertiming", 52 | "server-timing", 53 | "monitor", 54 | "monitoring", 55 | "speed", 56 | "middleware", 57 | "express" 58 | ], 59 | "author": "Anton Nemtsev ", 60 | "license": "MPL-2.0", 61 | "bugs": { 62 | "url": "https://github.com/SilentImp/express-middleware-headers-server-timing/issues" 63 | }, 64 | "homepage": "https://github.com/SilentImp/express-middleware-headers-server-timing#readme", 65 | "dependencies": { 66 | "on-headers": "^1.0.2" 67 | }, 68 | "devDependencies": { 69 | "@size-limit/preset-small-lib": "^6.0.4", 70 | "documentation": "^3.0.4", 71 | "eslint": "^7.32.0", 72 | "eslint-config-standard": "^16.0.3", 73 | "eslint-plugin-import": "^2.25.2", 74 | "eslint-plugin-jest": "^25.2.2", 75 | "eslint-plugin-node": "^11.1.0", 76 | "eslint-plugin-promise": "^5.1.1", 77 | "husky": "^7.0.4", 78 | "jest": "^27.3.1", 79 | "jest-express": "^1.12.0", 80 | "lint-staged": "^11.2.6", 81 | "prettier": "^2.4.1" 82 | } 83 | } 84 | --------------------------------------------------------------------------------