Time | 42 |Path | 43 |Method | 44 |Response Time | 45 |Status Code | 46 |
---|
Tracking | 217 |Path | 218 |Method | 219 |Status Code | 220 |Simulate User Activity | 221 |
---|
35 | fast 36 | 🐆
37 | 38 | 39 |40 | slow 41 | 🐌
42 | 43 | 44 |45 | new link 46 |
47 | 48 | 49 |50 | Use our API! 51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /dummy/module/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const responseTime = require("response-time"); 3 | const listAllEndpoints = require("express-list-endpoints"); 4 | 5 | /** 6 | * Start a new Express server that will store and serve a 7 | * list of logs and endpoints of the target server 8 | */ 9 | const app = express(); 10 | 11 | /** 12 | * Registed endpoints are stored in memory and can be exported 13 | * through the /endpoints endpoint 14 | */ 15 | let endpoints = []; 16 | 17 | /** 18 | * Logs are stored in memory until they are cleared by a request 19 | * made to the DELETE /metrics endpoint 20 | */ 21 | let logs = []; 22 | 23 | module.exports = { 24 | gatherMetrics: responseTime((req, res, time) => { 25 | if (req.url) { 26 | logs.push({ 27 | date_created: new Date(), 28 | path: req.route?.path, 29 | url: req.url, 30 | method: req.method, 31 | status_code: res.statusCode, 32 | response_time: Number(time.toFixed(3)), 33 | }); 34 | } 35 | }), 36 | 37 | registerEndpoint: (req, res, next) => { 38 | return next(); 39 | }, 40 | 41 | exportEndpoints: (app) => { 42 | const registeredEndpoints = listAllEndpoints(app).filter((endpoint) => 43 | endpoint.middlewares.includes("registerEndpoint") 44 | ); 45 | const formattedEndpoints = []; 46 | for (const unformattedEndpoint of registeredEndpoints) 47 | for (const method of unformattedEndpoint.methods) 48 | formattedEndpoints.push({ 49 | path: unformattedEndpoint.path, 50 | method: method, 51 | }); 52 | return (endpoints = formattedEndpoints); 53 | }, 54 | 55 | exportAllEndpoints: (app) => { 56 | const registeredEndpoints = listAllEndpoints(app); 57 | const formattedEndpoints = []; 58 | for (const unformattedEndpoint of registeredEndpoints) 59 | for (const method of unformattedEndpoint.methods) 60 | formattedEndpoints.push({ 61 | path: unformattedEndpoint.path, 62 | method: method, 63 | }); 64 | return (endpoints = formattedEndpoints); 65 | }, 66 | 67 | startMetricsServer: async function (PORT = 9991) { 68 | app.get("/metrics", (req, res) => { 69 | return res.status(200).json(logs); 70 | }); 71 | app.delete("/metrics", (req, res) => { 72 | res.status(200).json(logs); 73 | logs = []; 74 | return; 75 | }); 76 | app.get("/endpoints", (req, res) => { 77 | return res.json(endpoints); 78 | }); 79 | app.listen(PORT, () => { 80 | console.log(`Metrics server started on port ${PORT}`); 81 | }); 82 | }, 83 | }; 84 | -------------------------------------------------------------------------------- /dummy/module/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-endpoints-monitor", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "express-endpoints-monitor", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "express": "^4.18.2", 13 | "express-list-endpoints": "^6.0.0", 14 | "response-time": "^2.3.2" 15 | } 16 | }, 17 | "node_modules/accepts": { 18 | "version": "1.3.8", 19 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 20 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 21 | "dependencies": { 22 | "mime-types": "~2.1.34", 23 | "negotiator": "0.6.3" 24 | }, 25 | "engines": { 26 | "node": ">= 0.6" 27 | } 28 | }, 29 | "node_modules/array-flatten": { 30 | "version": "1.1.1", 31 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 32 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 33 | }, 34 | "node_modules/body-parser": { 35 | "version": "1.20.1", 36 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", 37 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", 38 | "dependencies": { 39 | "bytes": "3.1.2", 40 | "content-type": "~1.0.4", 41 | "debug": "2.6.9", 42 | "depd": "2.0.0", 43 | "destroy": "1.2.0", 44 | "http-errors": "2.0.0", 45 | "iconv-lite": "0.4.24", 46 | "on-finished": "2.4.1", 47 | "qs": "6.11.0", 48 | "raw-body": "2.5.1", 49 | "type-is": "~1.6.18", 50 | "unpipe": "1.0.0" 51 | }, 52 | "engines": { 53 | "node": ">= 0.8", 54 | "npm": "1.2.8000 || >= 1.4.16" 55 | } 56 | }, 57 | "node_modules/bytes": { 58 | "version": "3.1.2", 59 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 60 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 61 | "engines": { 62 | "node": ">= 0.8" 63 | } 64 | }, 65 | "node_modules/call-bind": { 66 | "version": "1.0.2", 67 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 68 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 69 | "dependencies": { 70 | "function-bind": "^1.1.1", 71 | "get-intrinsic": "^1.0.2" 72 | }, 73 | "funding": { 74 | "url": "https://github.com/sponsors/ljharb" 75 | } 76 | }, 77 | "node_modules/content-disposition": { 78 | "version": "0.5.4", 79 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 80 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 81 | "dependencies": { 82 | "safe-buffer": "5.2.1" 83 | }, 84 | "engines": { 85 | "node": ">= 0.6" 86 | } 87 | }, 88 | "node_modules/content-type": { 89 | "version": "1.0.4", 90 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 91 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", 92 | "engines": { 93 | "node": ">= 0.6" 94 | } 95 | }, 96 | "node_modules/cookie": { 97 | "version": "0.5.0", 98 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 99 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", 100 | "engines": { 101 | "node": ">= 0.6" 102 | } 103 | }, 104 | "node_modules/cookie-signature": { 105 | "version": "1.0.6", 106 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 107 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 108 | }, 109 | "node_modules/debug": { 110 | "version": "2.6.9", 111 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 112 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 113 | "dependencies": { 114 | "ms": "2.0.0" 115 | } 116 | }, 117 | "node_modules/depd": { 118 | "version": "2.0.0", 119 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 120 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 121 | "engines": { 122 | "node": ">= 0.8" 123 | } 124 | }, 125 | "node_modules/destroy": { 126 | "version": "1.2.0", 127 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 128 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 129 | "engines": { 130 | "node": ">= 0.8", 131 | "npm": "1.2.8000 || >= 1.4.16" 132 | } 133 | }, 134 | "node_modules/ee-first": { 135 | "version": "1.1.1", 136 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 137 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 138 | }, 139 | "node_modules/encodeurl": { 140 | "version": "1.0.2", 141 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 142 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 143 | "engines": { 144 | "node": ">= 0.8" 145 | } 146 | }, 147 | "node_modules/escape-html": { 148 | "version": "1.0.3", 149 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 150 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 151 | }, 152 | "node_modules/etag": { 153 | "version": "1.8.1", 154 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 155 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 156 | "engines": { 157 | "node": ">= 0.6" 158 | } 159 | }, 160 | "node_modules/express": { 161 | "version": "4.18.2", 162 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", 163 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", 164 | "dependencies": { 165 | "accepts": "~1.3.8", 166 | "array-flatten": "1.1.1", 167 | "body-parser": "1.20.1", 168 | "content-disposition": "0.5.4", 169 | "content-type": "~1.0.4", 170 | "cookie": "0.5.0", 171 | "cookie-signature": "1.0.6", 172 | "debug": "2.6.9", 173 | "depd": "2.0.0", 174 | "encodeurl": "~1.0.2", 175 | "escape-html": "~1.0.3", 176 | "etag": "~1.8.1", 177 | "finalhandler": "1.2.0", 178 | "fresh": "0.5.2", 179 | "http-errors": "2.0.0", 180 | "merge-descriptors": "1.0.1", 181 | "methods": "~1.1.2", 182 | "on-finished": "2.4.1", 183 | "parseurl": "~1.3.3", 184 | "path-to-regexp": "0.1.7", 185 | "proxy-addr": "~2.0.7", 186 | "qs": "6.11.0", 187 | "range-parser": "~1.2.1", 188 | "safe-buffer": "5.2.1", 189 | "send": "0.18.0", 190 | "serve-static": "1.15.0", 191 | "setprototypeof": "1.2.0", 192 | "statuses": "2.0.1", 193 | "type-is": "~1.6.18", 194 | "utils-merge": "1.0.1", 195 | "vary": "~1.1.2" 196 | }, 197 | "engines": { 198 | "node": ">= 0.10.0" 199 | } 200 | }, 201 | "node_modules/express-list-endpoints": { 202 | "version": "6.0.0", 203 | "resolved": "https://registry.npmjs.org/express-list-endpoints/-/express-list-endpoints-6.0.0.tgz", 204 | "integrity": "sha512-1I30bSVego+AU/eSsX/bV2xrOXW5tFhsuXZp7wZd9396bAAxH7KHaAwLXQYra0Aw33xA67HmNiceGf2SOvXaLg==", 205 | "engines": { 206 | "node": ">=10" 207 | } 208 | }, 209 | "node_modules/finalhandler": { 210 | "version": "1.2.0", 211 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 212 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 213 | "dependencies": { 214 | "debug": "2.6.9", 215 | "encodeurl": "~1.0.2", 216 | "escape-html": "~1.0.3", 217 | "on-finished": "2.4.1", 218 | "parseurl": "~1.3.3", 219 | "statuses": "2.0.1", 220 | "unpipe": "~1.0.0" 221 | }, 222 | "engines": { 223 | "node": ">= 0.8" 224 | } 225 | }, 226 | "node_modules/forwarded": { 227 | "version": "0.2.0", 228 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 229 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 230 | "engines": { 231 | "node": ">= 0.6" 232 | } 233 | }, 234 | "node_modules/fresh": { 235 | "version": "0.5.2", 236 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 237 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 238 | "engines": { 239 | "node": ">= 0.6" 240 | } 241 | }, 242 | "node_modules/function-bind": { 243 | "version": "1.1.1", 244 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 245 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 246 | }, 247 | "node_modules/get-intrinsic": { 248 | "version": "1.1.3", 249 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", 250 | "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", 251 | "dependencies": { 252 | "function-bind": "^1.1.1", 253 | "has": "^1.0.3", 254 | "has-symbols": "^1.0.3" 255 | }, 256 | "funding": { 257 | "url": "https://github.com/sponsors/ljharb" 258 | } 259 | }, 260 | "node_modules/has": { 261 | "version": "1.0.3", 262 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 263 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 264 | "dependencies": { 265 | "function-bind": "^1.1.1" 266 | }, 267 | "engines": { 268 | "node": ">= 0.4.0" 269 | } 270 | }, 271 | "node_modules/has-symbols": { 272 | "version": "1.0.3", 273 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 274 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 275 | "engines": { 276 | "node": ">= 0.4" 277 | }, 278 | "funding": { 279 | "url": "https://github.com/sponsors/ljharb" 280 | } 281 | }, 282 | "node_modules/http-errors": { 283 | "version": "2.0.0", 284 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 285 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 286 | "dependencies": { 287 | "depd": "2.0.0", 288 | "inherits": "2.0.4", 289 | "setprototypeof": "1.2.0", 290 | "statuses": "2.0.1", 291 | "toidentifier": "1.0.1" 292 | }, 293 | "engines": { 294 | "node": ">= 0.8" 295 | } 296 | }, 297 | "node_modules/iconv-lite": { 298 | "version": "0.4.24", 299 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 300 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 301 | "dependencies": { 302 | "safer-buffer": ">= 2.1.2 < 3" 303 | }, 304 | "engines": { 305 | "node": ">=0.10.0" 306 | } 307 | }, 308 | "node_modules/inherits": { 309 | "version": "2.0.4", 310 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 311 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 312 | }, 313 | "node_modules/ipaddr.js": { 314 | "version": "1.9.1", 315 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 316 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 317 | "engines": { 318 | "node": ">= 0.10" 319 | } 320 | }, 321 | "node_modules/media-typer": { 322 | "version": "0.3.0", 323 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 324 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 325 | "engines": { 326 | "node": ">= 0.6" 327 | } 328 | }, 329 | "node_modules/merge-descriptors": { 330 | "version": "1.0.1", 331 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 332 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 333 | }, 334 | "node_modules/methods": { 335 | "version": "1.1.2", 336 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 337 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 338 | "engines": { 339 | "node": ">= 0.6" 340 | } 341 | }, 342 | "node_modules/mime": { 343 | "version": "1.6.0", 344 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 345 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 346 | "bin": { 347 | "mime": "cli.js" 348 | }, 349 | "engines": { 350 | "node": ">=4" 351 | } 352 | }, 353 | "node_modules/mime-db": { 354 | "version": "1.52.0", 355 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 356 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 357 | "engines": { 358 | "node": ">= 0.6" 359 | } 360 | }, 361 | "node_modules/mime-types": { 362 | "version": "2.1.35", 363 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 364 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 365 | "dependencies": { 366 | "mime-db": "1.52.0" 367 | }, 368 | "engines": { 369 | "node": ">= 0.6" 370 | } 371 | }, 372 | "node_modules/ms": { 373 | "version": "2.0.0", 374 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 375 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 376 | }, 377 | "node_modules/negotiator": { 378 | "version": "0.6.3", 379 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 380 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 381 | "engines": { 382 | "node": ">= 0.6" 383 | } 384 | }, 385 | "node_modules/object-inspect": { 386 | "version": "1.12.2", 387 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", 388 | "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", 389 | "funding": { 390 | "url": "https://github.com/sponsors/ljharb" 391 | } 392 | }, 393 | "node_modules/on-finished": { 394 | "version": "2.4.1", 395 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 396 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 397 | "dependencies": { 398 | "ee-first": "1.1.1" 399 | }, 400 | "engines": { 401 | "node": ">= 0.8" 402 | } 403 | }, 404 | "node_modules/on-headers": { 405 | "version": "1.0.2", 406 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", 407 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", 408 | "engines": { 409 | "node": ">= 0.8" 410 | } 411 | }, 412 | "node_modules/parseurl": { 413 | "version": "1.3.3", 414 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 415 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 416 | "engines": { 417 | "node": ">= 0.8" 418 | } 419 | }, 420 | "node_modules/path-to-regexp": { 421 | "version": "0.1.7", 422 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 423 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 424 | }, 425 | "node_modules/proxy-addr": { 426 | "version": "2.0.7", 427 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 428 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 429 | "dependencies": { 430 | "forwarded": "0.2.0", 431 | "ipaddr.js": "1.9.1" 432 | }, 433 | "engines": { 434 | "node": ">= 0.10" 435 | } 436 | }, 437 | "node_modules/qs": { 438 | "version": "6.11.0", 439 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 440 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 441 | "dependencies": { 442 | "side-channel": "^1.0.4" 443 | }, 444 | "engines": { 445 | "node": ">=0.6" 446 | }, 447 | "funding": { 448 | "url": "https://github.com/sponsors/ljharb" 449 | } 450 | }, 451 | "node_modules/range-parser": { 452 | "version": "1.2.1", 453 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 454 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 455 | "engines": { 456 | "node": ">= 0.6" 457 | } 458 | }, 459 | "node_modules/raw-body": { 460 | "version": "2.5.1", 461 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", 462 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", 463 | "dependencies": { 464 | "bytes": "3.1.2", 465 | "http-errors": "2.0.0", 466 | "iconv-lite": "0.4.24", 467 | "unpipe": "1.0.0" 468 | }, 469 | "engines": { 470 | "node": ">= 0.8" 471 | } 472 | }, 473 | "node_modules/response-time": { 474 | "version": "2.3.2", 475 | "resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.2.tgz", 476 | "integrity": "sha512-MUIDaDQf+CVqflfTdQ5yam+aYCkXj1PY8fjlPDQ6ppxJlmgZb864pHtA750mayywNg8tx4rS7qH9JXd/OF+3gw==", 477 | "dependencies": { 478 | "depd": "~1.1.0", 479 | "on-headers": "~1.0.1" 480 | }, 481 | "engines": { 482 | "node": ">= 0.8.0" 483 | } 484 | }, 485 | "node_modules/response-time/node_modules/depd": { 486 | "version": "1.1.2", 487 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 488 | "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", 489 | "engines": { 490 | "node": ">= 0.6" 491 | } 492 | }, 493 | "node_modules/safe-buffer": { 494 | "version": "5.2.1", 495 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 496 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 497 | "funding": [ 498 | { 499 | "type": "github", 500 | "url": "https://github.com/sponsors/feross" 501 | }, 502 | { 503 | "type": "patreon", 504 | "url": "https://www.patreon.com/feross" 505 | }, 506 | { 507 | "type": "consulting", 508 | "url": "https://feross.org/support" 509 | } 510 | ] 511 | }, 512 | "node_modules/safer-buffer": { 513 | "version": "2.1.2", 514 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 515 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 516 | }, 517 | "node_modules/send": { 518 | "version": "0.18.0", 519 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 520 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 521 | "dependencies": { 522 | "debug": "2.6.9", 523 | "depd": "2.0.0", 524 | "destroy": "1.2.0", 525 | "encodeurl": "~1.0.2", 526 | "escape-html": "~1.0.3", 527 | "etag": "~1.8.1", 528 | "fresh": "0.5.2", 529 | "http-errors": "2.0.0", 530 | "mime": "1.6.0", 531 | "ms": "2.1.3", 532 | "on-finished": "2.4.1", 533 | "range-parser": "~1.2.1", 534 | "statuses": "2.0.1" 535 | }, 536 | "engines": { 537 | "node": ">= 0.8.0" 538 | } 539 | }, 540 | "node_modules/send/node_modules/ms": { 541 | "version": "2.1.3", 542 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 543 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 544 | }, 545 | "node_modules/serve-static": { 546 | "version": "1.15.0", 547 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 548 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 549 | "dependencies": { 550 | "encodeurl": "~1.0.2", 551 | "escape-html": "~1.0.3", 552 | "parseurl": "~1.3.3", 553 | "send": "0.18.0" 554 | }, 555 | "engines": { 556 | "node": ">= 0.8.0" 557 | } 558 | }, 559 | "node_modules/setprototypeof": { 560 | "version": "1.2.0", 561 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 562 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 563 | }, 564 | "node_modules/side-channel": { 565 | "version": "1.0.4", 566 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 567 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 568 | "dependencies": { 569 | "call-bind": "^1.0.0", 570 | "get-intrinsic": "^1.0.2", 571 | "object-inspect": "^1.9.0" 572 | }, 573 | "funding": { 574 | "url": "https://github.com/sponsors/ljharb" 575 | } 576 | }, 577 | "node_modules/statuses": { 578 | "version": "2.0.1", 579 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 580 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 581 | "engines": { 582 | "node": ">= 0.8" 583 | } 584 | }, 585 | "node_modules/toidentifier": { 586 | "version": "1.0.1", 587 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 588 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 589 | "engines": { 590 | "node": ">=0.6" 591 | } 592 | }, 593 | "node_modules/type-is": { 594 | "version": "1.6.18", 595 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 596 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 597 | "dependencies": { 598 | "media-typer": "0.3.0", 599 | "mime-types": "~2.1.24" 600 | }, 601 | "engines": { 602 | "node": ">= 0.6" 603 | } 604 | }, 605 | "node_modules/unpipe": { 606 | "version": "1.0.0", 607 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 608 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 609 | "engines": { 610 | "node": ">= 0.8" 611 | } 612 | }, 613 | "node_modules/utils-merge": { 614 | "version": "1.0.1", 615 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 616 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 617 | "engines": { 618 | "node": ">= 0.4.0" 619 | } 620 | }, 621 | "node_modules/vary": { 622 | "version": "1.1.2", 623 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 624 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 625 | "engines": { 626 | "node": ">= 0.8" 627 | } 628 | } 629 | }, 630 | "dependencies": { 631 | "accepts": { 632 | "version": "1.3.8", 633 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 634 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 635 | "requires": { 636 | "mime-types": "~2.1.34", 637 | "negotiator": "0.6.3" 638 | } 639 | }, 640 | "array-flatten": { 641 | "version": "1.1.1", 642 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 643 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 644 | }, 645 | "body-parser": { 646 | "version": "1.20.1", 647 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", 648 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", 649 | "requires": { 650 | "bytes": "3.1.2", 651 | "content-type": "~1.0.4", 652 | "debug": "2.6.9", 653 | "depd": "2.0.0", 654 | "destroy": "1.2.0", 655 | "http-errors": "2.0.0", 656 | "iconv-lite": "0.4.24", 657 | "on-finished": "2.4.1", 658 | "qs": "6.11.0", 659 | "raw-body": "2.5.1", 660 | "type-is": "~1.6.18", 661 | "unpipe": "1.0.0" 662 | } 663 | }, 664 | "bytes": { 665 | "version": "3.1.2", 666 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 667 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" 668 | }, 669 | "call-bind": { 670 | "version": "1.0.2", 671 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 672 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 673 | "requires": { 674 | "function-bind": "^1.1.1", 675 | "get-intrinsic": "^1.0.2" 676 | } 677 | }, 678 | "content-disposition": { 679 | "version": "0.5.4", 680 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 681 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 682 | "requires": { 683 | "safe-buffer": "5.2.1" 684 | } 685 | }, 686 | "content-type": { 687 | "version": "1.0.4", 688 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 689 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 690 | }, 691 | "cookie": { 692 | "version": "0.5.0", 693 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 694 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" 695 | }, 696 | "cookie-signature": { 697 | "version": "1.0.6", 698 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 699 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 700 | }, 701 | "debug": { 702 | "version": "2.6.9", 703 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 704 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 705 | "requires": { 706 | "ms": "2.0.0" 707 | } 708 | }, 709 | "depd": { 710 | "version": "2.0.0", 711 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 712 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" 713 | }, 714 | "destroy": { 715 | "version": "1.2.0", 716 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 717 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" 718 | }, 719 | "ee-first": { 720 | "version": "1.1.1", 721 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 722 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 723 | }, 724 | "encodeurl": { 725 | "version": "1.0.2", 726 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 727 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" 728 | }, 729 | "escape-html": { 730 | "version": "1.0.3", 731 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 732 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 733 | }, 734 | "etag": { 735 | "version": "1.8.1", 736 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 737 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" 738 | }, 739 | "express": { 740 | "version": "4.18.2", 741 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", 742 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", 743 | "requires": { 744 | "accepts": "~1.3.8", 745 | "array-flatten": "1.1.1", 746 | "body-parser": "1.20.1", 747 | "content-disposition": "0.5.4", 748 | "content-type": "~1.0.4", 749 | "cookie": "0.5.0", 750 | "cookie-signature": "1.0.6", 751 | "debug": "2.6.9", 752 | "depd": "2.0.0", 753 | "encodeurl": "~1.0.2", 754 | "escape-html": "~1.0.3", 755 | "etag": "~1.8.1", 756 | "finalhandler": "1.2.0", 757 | "fresh": "0.5.2", 758 | "http-errors": "2.0.0", 759 | "merge-descriptors": "1.0.1", 760 | "methods": "~1.1.2", 761 | "on-finished": "2.4.1", 762 | "parseurl": "~1.3.3", 763 | "path-to-regexp": "0.1.7", 764 | "proxy-addr": "~2.0.7", 765 | "qs": "6.11.0", 766 | "range-parser": "~1.2.1", 767 | "safe-buffer": "5.2.1", 768 | "send": "0.18.0", 769 | "serve-static": "1.15.0", 770 | "setprototypeof": "1.2.0", 771 | "statuses": "2.0.1", 772 | "type-is": "~1.6.18", 773 | "utils-merge": "1.0.1", 774 | "vary": "~1.1.2" 775 | } 776 | }, 777 | "express-list-endpoints": { 778 | "version": "6.0.0", 779 | "resolved": "https://registry.npmjs.org/express-list-endpoints/-/express-list-endpoints-6.0.0.tgz", 780 | "integrity": "sha512-1I30bSVego+AU/eSsX/bV2xrOXW5tFhsuXZp7wZd9396bAAxH7KHaAwLXQYra0Aw33xA67HmNiceGf2SOvXaLg==" 781 | }, 782 | "finalhandler": { 783 | "version": "1.2.0", 784 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 785 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 786 | "requires": { 787 | "debug": "2.6.9", 788 | "encodeurl": "~1.0.2", 789 | "escape-html": "~1.0.3", 790 | "on-finished": "2.4.1", 791 | "parseurl": "~1.3.3", 792 | "statuses": "2.0.1", 793 | "unpipe": "~1.0.0" 794 | } 795 | }, 796 | "forwarded": { 797 | "version": "0.2.0", 798 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 799 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" 800 | }, 801 | "fresh": { 802 | "version": "0.5.2", 803 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 804 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" 805 | }, 806 | "function-bind": { 807 | "version": "1.1.1", 808 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 809 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 810 | }, 811 | "get-intrinsic": { 812 | "version": "1.1.3", 813 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", 814 | "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", 815 | "requires": { 816 | "function-bind": "^1.1.1", 817 | "has": "^1.0.3", 818 | "has-symbols": "^1.0.3" 819 | } 820 | }, 821 | "has": { 822 | "version": "1.0.3", 823 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 824 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 825 | "requires": { 826 | "function-bind": "^1.1.1" 827 | } 828 | }, 829 | "has-symbols": { 830 | "version": "1.0.3", 831 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 832 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" 833 | }, 834 | "http-errors": { 835 | "version": "2.0.0", 836 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 837 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 838 | "requires": { 839 | "depd": "2.0.0", 840 | "inherits": "2.0.4", 841 | "setprototypeof": "1.2.0", 842 | "statuses": "2.0.1", 843 | "toidentifier": "1.0.1" 844 | } 845 | }, 846 | "iconv-lite": { 847 | "version": "0.4.24", 848 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 849 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 850 | "requires": { 851 | "safer-buffer": ">= 2.1.2 < 3" 852 | } 853 | }, 854 | "inherits": { 855 | "version": "2.0.4", 856 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 857 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 858 | }, 859 | "ipaddr.js": { 860 | "version": "1.9.1", 861 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 862 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 863 | }, 864 | "media-typer": { 865 | "version": "0.3.0", 866 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 867 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" 868 | }, 869 | "merge-descriptors": { 870 | "version": "1.0.1", 871 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 872 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 873 | }, 874 | "methods": { 875 | "version": "1.1.2", 876 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 877 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" 878 | }, 879 | "mime": { 880 | "version": "1.6.0", 881 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 882 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 883 | }, 884 | "mime-db": { 885 | "version": "1.52.0", 886 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 887 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 888 | }, 889 | "mime-types": { 890 | "version": "2.1.35", 891 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 892 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 893 | "requires": { 894 | "mime-db": "1.52.0" 895 | } 896 | }, 897 | "ms": { 898 | "version": "2.0.0", 899 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 900 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 901 | }, 902 | "negotiator": { 903 | "version": "0.6.3", 904 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 905 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" 906 | }, 907 | "object-inspect": { 908 | "version": "1.12.2", 909 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", 910 | "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" 911 | }, 912 | "on-finished": { 913 | "version": "2.4.1", 914 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 915 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 916 | "requires": { 917 | "ee-first": "1.1.1" 918 | } 919 | }, 920 | "on-headers": { 921 | "version": "1.0.2", 922 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", 923 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" 924 | }, 925 | "parseurl": { 926 | "version": "1.3.3", 927 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 928 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 929 | }, 930 | "path-to-regexp": { 931 | "version": "0.1.7", 932 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 933 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 934 | }, 935 | "proxy-addr": { 936 | "version": "2.0.7", 937 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 938 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 939 | "requires": { 940 | "forwarded": "0.2.0", 941 | "ipaddr.js": "1.9.1" 942 | } 943 | }, 944 | "qs": { 945 | "version": "6.11.0", 946 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 947 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 948 | "requires": { 949 | "side-channel": "^1.0.4" 950 | } 951 | }, 952 | "range-parser": { 953 | "version": "1.2.1", 954 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 955 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 956 | }, 957 | "raw-body": { 958 | "version": "2.5.1", 959 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", 960 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", 961 | "requires": { 962 | "bytes": "3.1.2", 963 | "http-errors": "2.0.0", 964 | "iconv-lite": "0.4.24", 965 | "unpipe": "1.0.0" 966 | } 967 | }, 968 | "response-time": { 969 | "version": "2.3.2", 970 | "resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.2.tgz", 971 | "integrity": "sha512-MUIDaDQf+CVqflfTdQ5yam+aYCkXj1PY8fjlPDQ6ppxJlmgZb864pHtA750mayywNg8tx4rS7qH9JXd/OF+3gw==", 972 | "requires": { 973 | "depd": "~1.1.0", 974 | "on-headers": "~1.0.1" 975 | }, 976 | "dependencies": { 977 | "depd": { 978 | "version": "1.1.2", 979 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 980 | "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" 981 | } 982 | } 983 | }, 984 | "safe-buffer": { 985 | "version": "5.2.1", 986 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 987 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 988 | }, 989 | "safer-buffer": { 990 | "version": "2.1.2", 991 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 992 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 993 | }, 994 | "send": { 995 | "version": "0.18.0", 996 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 997 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 998 | "requires": { 999 | "debug": "2.6.9", 1000 | "depd": "2.0.0", 1001 | "destroy": "1.2.0", 1002 | "encodeurl": "~1.0.2", 1003 | "escape-html": "~1.0.3", 1004 | "etag": "~1.8.1", 1005 | "fresh": "0.5.2", 1006 | "http-errors": "2.0.0", 1007 | "mime": "1.6.0", 1008 | "ms": "2.1.3", 1009 | "on-finished": "2.4.1", 1010 | "range-parser": "~1.2.1", 1011 | "statuses": "2.0.1" 1012 | }, 1013 | "dependencies": { 1014 | "ms": { 1015 | "version": "2.1.3", 1016 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1017 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1018 | } 1019 | } 1020 | }, 1021 | "serve-static": { 1022 | "version": "1.15.0", 1023 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 1024 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 1025 | "requires": { 1026 | "encodeurl": "~1.0.2", 1027 | "escape-html": "~1.0.3", 1028 | "parseurl": "~1.3.3", 1029 | "send": "0.18.0" 1030 | } 1031 | }, 1032 | "setprototypeof": { 1033 | "version": "1.2.0", 1034 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1035 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 1036 | }, 1037 | "side-channel": { 1038 | "version": "1.0.4", 1039 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 1040 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 1041 | "requires": { 1042 | "call-bind": "^1.0.0", 1043 | "get-intrinsic": "^1.0.2", 1044 | "object-inspect": "^1.9.0" 1045 | } 1046 | }, 1047 | "statuses": { 1048 | "version": "2.0.1", 1049 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1050 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" 1051 | }, 1052 | "toidentifier": { 1053 | "version": "1.0.1", 1054 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1055 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" 1056 | }, 1057 | "type-is": { 1058 | "version": "1.6.18", 1059 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1060 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1061 | "requires": { 1062 | "media-typer": "0.3.0", 1063 | "mime-types": "~2.1.24" 1064 | } 1065 | }, 1066 | "unpipe": { 1067 | "version": "1.0.0", 1068 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1069 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" 1070 | }, 1071 | "utils-merge": { 1072 | "version": "1.0.1", 1073 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1074 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" 1075 | }, 1076 | "vary": { 1077 | "version": "1.1.2", 1078 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1079 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" 1080 | } 1081 | } 1082 | } 1083 | -------------------------------------------------------------------------------- /dummy/module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-endpoints-monitor", 3 | "version": "1.0.2", 4 | "description": "", 5 | "main": "index.js", 6 | "keywords": [], 7 | "author": "", 8 | "license": "ISC", 9 | "dependencies": { 10 | "express": "^4.18.2", 11 | "express-list-endpoints": "^6.0.0", 12 | "response-time": "^2.3.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /dummy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dummy", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node ." 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express-endpoints-monitor": "^1.0.2", 14 | "nodemon": "^2.0.20" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /dummy/server.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const express = require("express"); 3 | const app = express(); 4 | const apiRouter = require("./api.js"); 5 | const ourModule = require("express-endpoints-monitor"); 6 | 7 | const PORT = 3000; 8 | const METRICS_PORT = 9991; 9 | 10 | app.use(express.json()); 11 | app.use(ourModule.gatherMetrics); 12 | 13 | app.use(express.static(path.join(__dirname, "assets"))); 14 | 15 | app.get("/", (req, res) => 16 | res.sendFile(path.resolve(__dirname, "./index.html")) 17 | ); 18 | 19 | app.use("/api", apiRouter); 20 | 21 | app.get("/fast", 22 | ourModule.registerEndpoint, 23 | (req, res) => { 24 | res.status(201).send("fast"); 25 | }); 26 | 27 | app.put("/fast", 28 | ourModule.registerEndpoint, 29 | (req, res) => { 30 | res.status(204).send("fast"); 31 | }); 32 | 33 | app.get("/slow", ourModule.registerEndpoint, (req, res) => { 34 | const validStatusCodes = [ 35 | 100, 102, 200, 200, 200, 202, 203, 204, 204, 210, 301, 302, 400, 401, 403, 404, 500, 505 36 | ]; 37 | 38 | const statusCode = 39 | validStatusCodes[Math.floor(Math.random() * validStatusCodes.length)]; 40 | const artificialDelay = Math.random() * 900; 41 | setTimeout(() => res.status(statusCode).send("slow"), artificialDelay); 42 | }); 43 | 44 | app.listen(PORT, () => { 45 | console.log(`Target server started on port ${PORT}`); 46 | 47 | // ourModule.exportEndpoints(app); 48 | ourModule.exportAllEndpoints(app); 49 | ourModule.startMetricsServer(METRICS_PORT); 50 | }); 51 | -------------------------------------------------------------------------------- /electron/electron-main.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const { app, BrowserWindow } = require("electron"); 3 | const url = require("url"); 4 | 5 | const { SERVER_URL } = process.env; 6 | 7 | function createWindow() { 8 | let win = new BrowserWindow({ 9 | width: 960, 10 | height: 700, 11 | webPreferences: { 12 | nodeIntegration: true, 13 | worldSafeExecuteJavaScript: true, 14 | contextIsolation: true, 15 | }, 16 | }); 17 | if (process.env.NODE_ENV === 'production') { 18 | indexPath = url.format({ 19 | protocol: 'http:', 20 | host: 'localhost:9990', 21 | pathname: 'index.html', 22 | slashes: true 23 | }) 24 | } else { 25 | indexPath = url.format({ 26 | protocol: 'http:', 27 | host: 'localhost:8080', 28 | pathname: 'index.html', 29 | slashes: true 30 | }) 31 | } 32 | setTimeout(() => win.loadURL(indexPath), 1000); 33 | win.once('ready-to-show', () => { 34 | win.show() 35 | }) 36 | } 37 | 38 | app.whenReady().then(() => { 39 | createWindow(); 40 | app.on("activate", () => { 41 | if (BrowserWindow.getAllWindows().length === 0) createWindow(); 42 | }); 43 | }); 44 | 45 | app.on("window-all-closed", () => { 46 | if (process.platform !== "darwin") app.quit(); 47 | }); 48 | -------------------------------------------------------------------------------- /examples/endpoint_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/examples/endpoint_view.png -------------------------------------------------------------------------------- /examples/home_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/examples/home_view.png -------------------------------------------------------------------------------- /examples/intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/examples/intro.png -------------------------------------------------------------------------------- /examples/monitoring_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/examples/monitoring_view.png -------------------------------------------------------------------------------- /examples/simulation_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/DataDoc/b3ee07ce10363eb11cbe0a770c0f7b336fd0c14c/examples/simulation_view.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datadoc", 3 | "version": "1.0.0", 4 | "description": "", 5 | "productName": "DataDoc", 6 | "main": "electron/electron-main.js", 7 | "scripts": { 8 | "start": "NODE_ENV=production nodemon server/server.js & NODE_ENV=production electron .", 9 | "build": "NODE_ENV=production webpack", 10 | "dev": "clear & NODE_ENV=development webpack serve & NODE_ENV=development nodemon server/server.js & electron .", 11 | "dev:mock": "npm run dev & nodemon dummy/server.js", 12 | "test": "jest --verbose", 13 | "electron": "electron ." 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/oslabs-beta/DataDoc.git" 18 | }, 19 | "keywords": [], 20 | "author": "Jo Huang https://github.com/jochuang, Jonathan Huang https://github.com/JH51, Jamie Schiff https://github.com/jamieschiff, Mariam Zakariadze https://github.com/mariamzakariadze", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/oslabs-beta/DataDoc/issues" 24 | }, 25 | "homepage": "https://github.com/oslabs-beta/DataDoc#readme", 26 | "devDependencies": { 27 | "@babel/core": "^7.20.5", 28 | "@babel/preset-env": "^7.20.2", 29 | "@babel/preset-react": "^7.18.6", 30 | "babel-loader": "^9.1.0", 31 | "css-loader": "^6.7.2", 32 | "electron": "^22.0.0", 33 | "html-webpack-plugin": "^5.5.0", 34 | "jest": "^29.3.1", 35 | "react-router-dom": "^6.4.5", 36 | "sass-loader": "^13.2.0", 37 | "style-loader": "^3.3.1", 38 | "webpack": "^5.75.0", 39 | "webpack-cli": "^5.0.1", 40 | "webpack-dev-server": "^4.11.1", 41 | "webpack-hot-middleware": "^2.25.3" 42 | }, 43 | "dependencies": { 44 | "@emotion/react": "^11.10.5", 45 | "@emotion/styled": "^11.10.5", 46 | "@influxdata/influxdb-client": "^1.33.0", 47 | "@mui/icons-material": "^5.11.0", 48 | "@mui/material": "^5.11.1", 49 | "@sendgrid/mail": "^7.7.0", 50 | "chart.js": "^4.0.1", 51 | "chartjs-adapter-moment": "^1.0.1", 52 | "compression": "^1.7.4", 53 | "cors": "^2.8.5", 54 | "dotenv": "^16.0.3", 55 | "dotenv-webpack": "^8.0.1", 56 | "express": "^4.18.2", 57 | "express-endpoints-monitor": "^1.0.0", 58 | "node-fetch": "^2.6.7", 59 | "nodemon": "^2.0.20", 60 | "pg": "^8.8.0", 61 | "prom2json-se": "^0.6.0", 62 | "react": "^18.2.0", 63 | "react-chartjs-2": "^5.0.1", 64 | "react-dom": "^18.2.0", 65 | "react-draggable": "^4.4.5", 66 | "react-pro-sidebar": "^0.7.1", 67 | "react-router": "^6.4.5", 68 | "response-time": "^2.3.2", 69 | "sass": "^1.56.2", 70 | "twilio": "^3.84.0", 71 | "typescript": "^4.9.4", 72 | "uuidv4": "^6.2.13" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /server/controllers/influxController.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const { InfluxDB } = require("@influxdata/influxdb-client"); 3 | const path = require("path"); 4 | const influxClient = require("../models/influx-client.js"); 5 | 6 | const token = process.env.DB_INFLUXDB_INIT_ADMIN_TOKEN; 7 | const org = process.env.DB_INFLUXDB_INIT_ORG; 8 | const bucket = process.env.DB_INFLUXDB_INIT_BUCKET; 9 | 10 | const queryApi = new InfluxDB({ 11 | url: "http://localhost:8086", 12 | token: token 13 | }).getQueryApi({ 14 | org, 15 | gzip: true, 16 | headers: { 17 | "Content-Encoding": "gzip" 18 | } 19 | }); 20 | 21 | const influxController = {}; 22 | 23 | let range = "1m"; 24 | 25 | // declare a data object to store chart data 26 | const data = { 27 | respTimeLineData: [], 28 | respTimeHistData: [], 29 | reqFreqLineData: [], 30 | statusPieData: [] 31 | }; 32 | 33 | influxController.updateRange = (req, res, next) => { 34 | range = req.body.range || range; 35 | console.log(`Updated range to: ${range}`) 36 | return next(); 37 | } 38 | 39 | influxController.getRespTimeLineData = (req, res, next) => { 40 | const fluxQuery = ` 41 | from(bucket: "dev-bucket") 42 | |> range(start: -${range}) 43 | |> filter(fn: (r) => r["_measurement"] == "monitoring${req.query?.workspaceId ? '_' + req.query.workspaceId : ''}") 44 | |> filter(fn: (r) => r["_field"] == "res_time") 45 | |> filter(fn: (r) => r["method"] == "${req.query.method}") 46 | |> filter(fn: (r) => r["path"] == "${req.query.path}") 47 | |> yield(name: "mean") 48 | `; 49 | 50 | // declare a metrics object to collect labels and data 51 | const metrics = []; 52 | 53 | queryApi.queryRows(fluxQuery, { 54 | next(row, tableMeta) { 55 | const o = tableMeta.toObject(row); 56 | metrics.push({ x: o._time, y: o._value }); 57 | }, 58 | error(error) { 59 | console.log("Query Finished ERROR"); 60 | return next(error); 61 | }, 62 | complete() { 63 | data.respTimeLineData = metrics; 64 | res.locals.data = data; 65 | // console.log("Query Finished SUCCESS"); 66 | return next(); 67 | } 68 | }); 69 | }; 70 | 71 | influxController.getRespTimeHistData = (req, res, next) => { 72 | const fluxQuery = ` 73 | from(bucket: "dev-bucket") 74 | |> range(start: -${range}) 75 | |> filter(fn: (r) => r["_measurement"] == "monitoring${req.query?.workspaceId ? '_' + req.query.workspaceId : ''}") 76 | |> filter(fn: (r) => r["_field"] == "res_time") 77 | |> filter(fn: (r) => r["method"] == "${req.query.method}") 78 | |> filter(fn: (r) => r["path"] == "${req.query.path}") 79 | |> histogram(bins: [0.1, 0.5, 1.0, 5.0, 10.0, 50.0, 100.0, 500.0, 1000.0, 5000.0]) 80 | `; 81 | 82 | // declare a metrics object to collect labels and data 83 | const metrics = []; 84 | 85 | // declare a variable 'le' (lower than or equal to) and 'respFreq' to collect labels and data 86 | const le = []; 87 | const respFreq = []; 88 | 89 | queryApi.queryRows(fluxQuery, { 90 | next(row, tableMeta) { 91 | const o = tableMeta.toObject(row); 92 | le.push(o.le); 93 | respFreq.push(o._value); 94 | }, 95 | error(error) { 96 | console.log("Query Finished ERROR"); 97 | return next(error); 98 | }, 99 | complete() { 100 | const newResFreq = respFreq.map((el, i) => { 101 | if (i === 0) return respFreq[i]; 102 | return respFreq[i] - respFreq[i - 1]; 103 | }); 104 | for (let i = 0; i < newResFreq.length; i++) { 105 | metrics.push({ x: le[i], y: newResFreq[i] }); 106 | } 107 | data.respTimeHistData = metrics; 108 | res.locals.data = data; 109 | // console.log("Query Finished SUCCESS"); 110 | return next(); 111 | } 112 | }); 113 | }; 114 | 115 | influxController.getReqFreqLineData = (req, res, next) => { 116 | const fluxQuery = ` 117 | from(bucket: "dev-bucket") 118 | |> range(start: -${range}) 119 | |> filter(fn: (r) => r["_measurement"] == "monitoring${req.query?.workspaceId ? '_' + req.query.workspaceId : ''}") 120 | |> filter(fn: (r) => r["_field"] == "res_time") 121 | |> filter(fn: (r) => r["method"] == "${req.query.method}") 122 | |> filter(fn: (r) => r["path"] == "${req.query.path}") 123 | |> aggregateWindow(every: ${(() => {switch (range) { 124 | case "1m": return "10s" 125 | case "5m": return "1m" 126 | case "30m": return "5m" 127 | default: return "10s" 128 | } 129 | })()}, fn: count, createEmpty: false) 130 | `; 131 | 132 | // declare a metrics object to collect labels and data 133 | const metrics = []; 134 | 135 | queryApi.queryRows(fluxQuery, { 136 | next(row, tableMeta) { 137 | const o = tableMeta.toObject(row); 138 | metrics.push({ x: o._time, y: o._value }); 139 | }, 140 | error(error) { 141 | console.log("Query Finished ERROR"); 142 | return next(error); 143 | }, 144 | complete() { 145 | data.reqFreqLineData = metrics; 146 | res.locals.data = data; 147 | // console.log("Query Finished SUCCESS"); 148 | return next(); 149 | } 150 | }); 151 | }; 152 | 153 | influxController.getStatusPieData = (req, res, next) => { 154 | const influxQuery = ` 155 | from(bucket: "dev-bucket") 156 | |> range(start: -${range}) 157 | |> filter(fn: (r) => r["_measurement"] == "monitoring${req.query?.workspaceId ? '_' + req.query.workspaceId : ''}") 158 | |> filter(fn: (r) => r["_field"] == "status_code") 159 | |> filter(fn: (r) => r["method"] == "${req.query.method}") 160 | |> filter(fn: (r) => r["path"] == "${req.query.path}") 161 | |> group(columns: ["_value"]) 162 | |> count(column: "_field") 163 | |> group() 164 | `; 165 | 166 | // declare a metrics object to collect labels and data 167 | const metrics = []; 168 | 169 | // declare a stats object to collect labels and data 170 | queryApi.queryRows(influxQuery, { 171 | next(row, tableMeta) { 172 | const o = tableMeta.toObject(row); 173 | metrics.push({ x: o._value, y: o._field }); 174 | }, 175 | error(error) { 176 | console.log("Query Finished ERROR"); 177 | return next(error); 178 | }, 179 | complete() { 180 | data.statusPieData = metrics; 181 | res.locals.data = data; 182 | // console.log("Query Finished SUCCESS"); 183 | return next(); 184 | } 185 | }); 186 | }; 187 | 188 | influxController.getEndpointLogs = (req, res, next) => { 189 | const influxQuery = ` 190 | from(bucket: "dev-bucket") 191 | |> range(start: -${range}) 192 | |> filter(fn: (r) => r["_measurement"] == "monitoring") 193 | |> filter(fn: (r) => r["_field"] == "res_time" or r["_field"] == "status_code") 194 | |> filter(fn: (r) => r["method"] == "${req.query.method}") 195 | |> filter(fn: (r) => r["path"] == "${req.query.path}") 196 | `; 197 | 198 | // declare a logs object to collect labels and data 199 | const logs = {}; 200 | 201 | // declare a stats object to collect labels and data 202 | queryApi.queryRows(influxQuery, { 203 | next(row, tableMeta) { 204 | const dataObject = tableMeta.toObject(row); 205 | if (logs[dataObject._time] === undefined) logs[dataObject._time] = {}; 206 | logs[dataObject._time].timestamp = dataObject._time; 207 | logs[dataObject._time][dataObject._field] = dataObject._value; 208 | }, 209 | error(error) { 210 | console.log("Query Finished ERROR"); 211 | return next(error); 212 | }, 213 | complete() { 214 | res.locals.logs = Object.values(logs); 215 | return next(); 216 | } 217 | }); 218 | }; 219 | 220 | module.exports = influxController; 221 | -------------------------------------------------------------------------------- /server/controllers/pgController.js: -------------------------------------------------------------------------------- 1 | const postgresClient = require("../models/postgres-client"); 2 | const influxController = require("./influxController"); 3 | 4 | const pgController = { 5 | 6 | deleteEndpointsByWorkspaceId: async (req, res, next) => { 7 | // const {workspaceId} = req.params; 8 | const {workspaceId} = res.locals; 9 | const queryText = ` 10 | DELETE 11 | FROM endpoints 12 | WHERE workspace_id=${workspaceId} 13 | ;`; 14 | try { 15 | await postgresClient.query(queryText); 16 | } catch (err) { 17 | return next(err); 18 | } 19 | return next(); 20 | }, 21 | 22 | updateEndpointById: async (req, res, next) => { 23 | const _id = Number(req.params?._id); 24 | const { method, path, tracking } = req.body; 25 | const queryText = ` 26 | UPDATE 27 | endpoints 28 | SET 29 | method='${method}', 30 | path='${path}', 31 | tracking=${tracking} 32 | WHERE 33 | _id=${_id} ; 34 | `; 35 | try { 36 | postgresClient.query(queryText); 37 | return next(); 38 | } catch (err) { 39 | return next(err); 40 | } 41 | }, 42 | updateEndpointByRoute: async (req, res, next) => { 43 | const { workspaceId, method, path, tracking} = req.body 44 | const queryText = ` 45 | UPDATE 46 | endpoints 47 | SET 48 | method='${method}', 49 | path='${path}', 50 | tracking=${tracking} 51 | WHERE 52 | workspace_id=${workspaceId} AND 53 | method='${method}' AND 54 | path='${path}' 55 | ;`; 56 | try { 57 | postgresClient.query(queryText); 58 | } catch (err) { 59 | return next(err); 60 | } 61 | return next(); 62 | } 63 | }; 64 | 65 | module.exports = pgController; 66 | -------------------------------------------------------------------------------- /server/models/influx-client.js: -------------------------------------------------------------------------------- 1 | const { InfluxDB, Point } = require("@influxdata/influxdb-client"); 2 | const dotenv = require("dotenv"); 3 | const path = require("path"); 4 | dotenv.config({ path: path.resolve(__dirname, "../../.env") }); 5 | 6 | const token = process.env.DB_INFLUXDB_INIT_ADMIN_TOKEN; 7 | const org = process.env.DB_INFLUXDB_INIT_ORG; 8 | const bucket = process.env.DB_INFLUXDB_INIT_BUCKET; 9 | 10 | const insertToDB = () => { 11 | // create a new instance of influxDB, providing URL and API token 12 | const client = new InfluxDB({ url: "http://localhost:8086", token: token }); 13 | // create a write client, providing influxDB organization and bucket name 14 | const writeApi = client.getWriteApi(org, bucket, "ns"); 15 | // create default tags to all points 16 | // writeApi.useDefaultTags({endpoint: '/signup'}) 17 | 18 | // use the point constructor passing in "measurement" (table) 19 | const point = new Point("metrics") 20 | .tag("path", "/good") 21 | .tag("method", "GET") 22 | .floatField("res_time", 60) 23 | .intField("status_code", 200); 24 | // .timestamp() 25 | 26 | writeApi.writePoint(point); 27 | 28 | writeApi.close().then(() => { 29 | console.log("WRITE FINISHED"); 30 | }); 31 | }; 32 | 33 | const insertMultiple = (pointsArr) => { 34 | try { 35 | const client = new InfluxDB({ 36 | url: "http://localhost:8086", 37 | token: token, 38 | options: { 39 | headers: { "Content-Encoding": "gzip" } 40 | } 41 | }); 42 | const writeApi = client.getWriteApi(org, bucket, "ms", { 43 | gzipThreshold: 0, 44 | headers: { "Content-Encoding": "gzip" } 45 | }); 46 | writeApi.writePoints(pointsArr); 47 | writeApi.close(); 48 | return true; 49 | } catch (e) { 50 | return false; 51 | } 52 | }; 53 | 54 | const insertRegistration = (point) => { 55 | const client = new InfluxDB({ url: "http://localhost:8086", token: token }); 56 | const writeApi = client.getWriteApi(org, bucket, "ns"); 57 | 58 | writeApi.writePoint(point); 59 | writeApi.close().then(() => { 60 | console.log("WRITE FINISHED"); 61 | }); 62 | }; 63 | 64 | module.exports = { insertToDB, insertMultiple, insertRegistration }; 65 | -------------------------------------------------------------------------------- /server/models/postgres-client.js: -------------------------------------------------------------------------------- 1 | const { Pool, Client } = require("pg"); 2 | const dotenv = require("dotenv"); 3 | dotenv.config(); 4 | 5 | const { PG_HOST, PG_PORT, PG_USER, PG_PASS, PG_DB } = process.env; 6 | 7 | const client = new Client({ 8 | host: PG_HOST, 9 | port: PG_PORT, 10 | user: PG_USER, 11 | password: PG_PASS, 12 | database: PG_DB, 13 | }) 14 | 15 | client.connect().then(() => { 16 | console.log("Connected to database"); 17 | client.query("SELECT NOW()"); 18 | }) 19 | 20 | module.exports = { 21 | query: (text, params, callback) => { 22 | return client.query(text, params, callback); 23 | }, 24 | }; -------------------------------------------------------------------------------- /server/models/postgres-init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE workspaces ( 2 | _id SERIAL, 3 | name TEXT NOT NULL, 4 | domain TEXT NOT NULL, 5 | port INTEGER, 6 | metrics_port INTEGER 7 | ); 8 | 9 | CREATE TABLE endpoints ( 10 | _id SERIAL, 11 | method TEXT NOT NULL, 12 | path TEXT NOT NULL, 13 | tracking BOOLEAN DEFAULT false, 14 | workspace_id INTEGER NOT NULL 15 | ); 16 | 17 | ALTER TABLE endpoints 18 | ADD CONSTRAINT endpoints_uq 19 | UNIQUE (method, path, workspace_id); -------------------------------------------------------------------------------- /server/routes/chartdata.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const compression = require("compression"); 3 | const influxController = require("../controllers/influxController.js"); 4 | 5 | const router = express.Router(); 6 | router.use(compression()); 7 | 8 | // * Retrieve line chart data from InfluxDB 9 | router.get( 10 | "/", 11 | influxController.getRespTimeLineData, 12 | influxController.getRespTimeHistData, 13 | influxController.getReqFreqLineData, 14 | influxController.getStatusPieData, 15 | (req, res) => { 16 | return res.status(200).json(res.locals.data); 17 | } 18 | ); 19 | 20 | // * Update chart range 21 | router.post("/", 22 | influxController.updateRange, 23 | (req, res) => res.sendStatus(204) 24 | ) 25 | 26 | module.exports = router; 27 | -------------------------------------------------------------------------------- /server/routes/logRouter.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const compression = require("compression"); 3 | const influxController = require("../controllers/influxController.js"); 4 | 5 | const router = express.Router(); 6 | router.use(compression()); 7 | 8 | // get line chart data 9 | router.get( 10 | "/", 11 | influxController.getEndpointLogs, 12 | (req, res) => { 13 | return res.status(200).json(res.locals.logs); 14 | } 15 | ); 16 | 17 | module.exports = router; 18 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | require("dotenv").config({ path: path.resolve(__dirname, "../.env") }); 3 | const express = require("express"); 4 | const fetch = require("node-fetch"); 5 | const cors = require("cors"); 6 | const influxClient = require("./models/influx-client.js"); 7 | const { Point } = require("@influxdata/influxdb-client"); 8 | const chartRouter = require("./routes/chartdata"); 9 | const logRouter = require("./routes/logRouter.js"); 10 | const postgresClient = require("./models/postgres-client.js"); 11 | const { 12 | PhoneNumberContext 13 | } = require("twilio/lib/rest/lookups/v1/phoneNumber.js"); 14 | const pgController = require("./controllers/pgController.js"); 15 | const { query } = require("express"); 16 | 17 | const MODE = process.env.NODE_ENV || "production"; 18 | const PORT = process.env.PORT || 9990; 19 | 20 | const app = express(); 21 | app.use(express.json()); 22 | app.use(cors()); 23 | app.use(express.urlencoded({ extended: true })); 24 | 25 | if (MODE === "production") { 26 | app.use(express.static(path.join(__dirname, "../dist"))); 27 | } 28 | 29 | // * Route all /chartdata requests to chartRouter 30 | app.use("/chartdata", chartRouter); 31 | app.use("/logdata", logRouter); 32 | 33 | let intervalId; 34 | let logs = []; 35 | let selectedEndpoints = []; 36 | let activeWorkspaceId; 37 | let monitoringStartTime, monitoringEndTime, timeElapsed; 38 | const trackedWorkspaces = {}; 39 | 40 | const updateTimeElapsed = function () { 41 | monitoringEndTime = new Date(); 42 | timeElapsed = new Date(monitoringEndTime - monitoringStartTime); 43 | if (timeElapsed < 60 * 1000) return timeElapsed.getSeconds() + "s"; 44 | return timeElapsed.getMinutes() + "m" + (timeElapsed.getSeconds() % 60) + "s"; 45 | }; 46 | 47 | const timeSince = (startDate) => { 48 | const timeElapsed = new Date(new Date() - startDate) 49 | if (timeElapsed < 60 * 1000) return timeElapsed.getSeconds() + "s"; 50 | return timeElapsed.getMinutes() + "m" + (timeElapsed.getSeconds() % 60) + "s"; 51 | } 52 | 53 | const scrapeDataFromMetricsServer = async (metricsPort, tableName) => { 54 | try { 55 | logs = await ( 56 | await fetch(`http://localhost:${metricsPort}/metrics`, { 57 | method: "DELETE" 58 | }) 59 | ).json(); 60 | console.log(`Storing to ${logs.length} entries to ${tableName}`) 61 | storeLogsToDatabase(logs, tableName); 62 | return logs; 63 | } catch (e) { 64 | console.error(e); 65 | return []; 66 | } 67 | }; 68 | 69 | const storeLogsToDatabase = async (logsArr, tableName) => { 70 | try { 71 | const pointsArr = logsArr.map((log) => { 72 | return new Point(tableName) 73 | .tag("path", log.path) 74 | .tag("url", log.url) 75 | .tag("method", log.method) 76 | .floatField("res_time", log.response_time) 77 | .intField("status_code", log.status_code) 78 | .timestamp(new Date(log.date_created).getTime()); 79 | }); 80 | return influxClient.insertMultiple(pointsArr); 81 | } catch (e) { 82 | console.error(e); 83 | return false; 84 | } 85 | }; 86 | 87 | const getTrackedEndpointsByWorkspaceId = async (workspaceId) => { 88 | const queryText = ` 89 | SELECT * 90 | FROM endpoints 91 | WHERE 92 | workspace_id=${workspaceId} AND 93 | tracking=${true} 94 | ;`; 95 | const dbResponse = await postgresClient.query(queryText); 96 | return dbResponse.rows; 97 | } 98 | 99 | const pingEndpoints = async (domain, port, endpoints = []) => { 100 | const formattedPort = (port !== undefined && 0 < port && port < 9999) ? ':' + port : ''; 101 | for (const endpoint of endpoints) { 102 | try { 103 | await fetch(`http://${domain}${formattedPort}${endpoint.path}`, { 104 | method: endpoint.method, 105 | headers: { "Cache-Control": "no-store" } 106 | }); 107 | } catch (e) { 108 | console.error(e); 109 | } 110 | } 111 | }; 112 | 113 | // endpoint to register user email and status codes to database 114 | app.post( 115 | "/registration", 116 | (req, res, next) => { 117 | let { subscribers, status300, status400, status500 } = req.body; 118 | try { 119 | const point = new Point("registration") 120 | .tag("email", subscribers) 121 | .booleanField("300", status300) 122 | .booleanField("400", status400) 123 | .booleanField("500", status500); 124 | influxClient.insertRegistration(point); 125 | return next(); 126 | } catch (e) { 127 | console.error(e); 128 | } 129 | }, 130 | (req, res) => res.sendStatus(200) 131 | ); 132 | 133 | app.get("/monitoring/:workspaceId", 134 | (req, res) => { 135 | const {workspaceId} = req.params; 136 | return res.status(200).json(trackedWorkspaces[workspaceId]?.active || false) 137 | } 138 | ); 139 | 140 | app.post("/monitoring", async (req, res) => { 141 | // * active is a boolean, interval is in seconds 142 | const { active, domain, metricsPort, mode, port, verbose, workspaceId } = req.body; 143 | 144 | if (active) { 145 | // * Enforce a minimum interval 146 | let interval = Math.max(0.5, req.body.interval); 147 | if (trackedWorkspaces[workspaceId] === undefined) trackedWorkspaces[workspaceId] = {}; 148 | if (trackedWorkspaces[workspaceId].intervalId) clearInterval(intervalId); 149 | const start = new Date(); 150 | const endpoints = await getTrackedEndpointsByWorkspaceId(workspaceId) || []; 151 | trackedWorkspaces[workspaceId] = Object.assign(trackedWorkspaces[workspaceId] ? trackedWorkspaces[workspaceId] : {}, { 152 | active, 153 | interval, 154 | intervalId: setInterval(() => { 155 | const elapsed = timeSince(trackedWorkspaces[workspaceId].start || new Date()); 156 | trackedWorkspaces[workspaceId].elapsed = elapsed; 157 | if (verbose) { 158 | console.clear(); 159 | console.log(`Monitoring for ${elapsed}`); 160 | } 161 | pingEndpoints(domain, port || '', endpoints); 162 | scrapeDataFromMetricsServer(metricsPort || 9991, `${mode}_${workspaceId}`); 163 | }, interval * 1000), 164 | domain, 165 | endpoints, 166 | metricsPort, 167 | mode, 168 | port, 169 | start, 170 | end: null, 171 | elapsed: 0, 172 | }) 173 | } 174 | 175 | else { 176 | if (trackedWorkspaces[workspaceId]) { 177 | clearInterval(trackedWorkspaces[workspaceId]?.intervalId) 178 | trackedWorkspaces[workspaceId].active = false; 179 | } 180 | trackedWorkspaces[workspaceId] = Object.assign(trackedWorkspaces[workspaceId] ? trackedWorkspaces[workspaceId] : {}, { 181 | active, 182 | intervalId: null, 183 | endpoints: [], 184 | end: new Date(), 185 | }) 186 | }; 187 | 188 | if (verbose) { 189 | console.clear(); 190 | console.log(`ACTIVE: ${active}`); 191 | } 192 | 193 | res.sendStatus(204); 194 | }); 195 | 196 | const pingOneEndpoint = async (URI, method) => { 197 | console.log(`Sending traffic to: ${URI}`) 198 | try { 199 | await fetch(URI, { 200 | method: method, 201 | headers: { 202 | "Cache-Control": "no-cache" 203 | } 204 | }); 205 | } catch (e) { 206 | console.error(e); 207 | } 208 | }; 209 | 210 | const performRPS = async (domain, port, path, method, RPS) => { 211 | // console.log("PERFORMRPS"); 212 | // console.table({ 213 | // domain, 214 | // port, 215 | // path, 216 | // method, 217 | // RPS, 218 | // }) 219 | // console.log("PERFORMRPS URI\nhttp://" + domain + (typeof port === "number") ? port : '' + path); 220 | const interval = Math.floor(1000 / RPS); 221 | if (intervalId) clearInterval(intervalId); 222 | let counter = 0; 223 | intervalId = setInterval(() => { 224 | console.clear() 225 | console.log(++counter); 226 | pingOneEndpoint(`http://${domain}${(typeof port === "number") ? ':' + port : ''}${path}`, method); 227 | }, interval); 228 | }; 229 | 230 | const rpswithInterval = async (domain, port, path, method, RPS, timeInterval) => { 231 | if (intervalId) clearInterval(intervalId); 232 | intervalId = setInterval(() => { 233 | performRPS(domain, port, path, method, RPS); 234 | console.log("PING FINISHED"); 235 | }, timeInterval * 1000); 236 | }; 237 | 238 | app.post("/simulation", async (req, res) => { 239 | const { workspaceId, domain, port, path, method, metricsPort, RPS, timeInterval, setTime, stop } = req.body; 240 | if (!stop) { 241 | rpswithInterval(domain, port, path, method, RPS, timeInterval); 242 | scrapeDataFromMetricsServer(metricsPort, `simulation_${workspaceId}`); 243 | } else { 244 | clearInterval(intervalId) 245 | console.log("Scraping..."); 246 | scrapeDataFromMetricsServer(metricsPort, `simulation_${workspaceId}`) 247 | }; 248 | console.log("PING RESULT DONE"); 249 | return res.sendStatus(200); 250 | }); 251 | 252 | app.get("/metrics", async (req, res) => { 253 | return res.status(200).json(logs); 254 | }); 255 | 256 | app.put("/endpoints/:_id", 257 | pgController.updateEndpointById, 258 | (req, res) => { 259 | return res.sendStatus(204); 260 | } 261 | ) 262 | 263 | app.put("/endpoints2/", 264 | pgController.updateEndpointByRoute, 265 | (req, res) => { 266 | return res.sendStatus(204); 267 | } 268 | ) 269 | 270 | app.put("/routes/server", async (req, res, next) => { 271 | const { workspaceId, metricsPort } = req.body; 272 | try { 273 | res.locals.workspaceId = workspaceId; 274 | const response = await fetch(`http://localhost:${metricsPort}/endpoints`) 275 | const routes = await response.json(); 276 | let queryText = ` 277 | DELETE 278 | FROM endpoints 279 | WHERE workspace_id=${workspaceId} 280 | ;`; 281 | routes.forEach((route) => { 282 | // route.status = 200; 283 | route.tracking = false; 284 | queryText += ` 285 | INSERT INTO endpoints (method, path, tracking, workspace_id) 286 | VALUES ('${route.method}', '${route.path}', ${route.tracking}, ${workspaceId}) 287 | ON CONFLICT ON CONSTRAINT endpoints_uq 288 | DO UPDATE SET tracking = ${route.tracking}; 289 | `; 290 | }); 291 | await postgresClient.query(queryText); 292 | } 293 | catch (err) { 294 | return console.error(err); 295 | } 296 | let dbResponse = []; 297 | try { 298 | queryText = ` 299 | SELECT * 300 | FROM endpoints 301 | WHERE workspace_id=${workspaceId} 302 | ;`; 303 | dbResponse = (await postgresClient.query(queryText)).rows; 304 | } 305 | catch (err) { 306 | console.error(err); 307 | return next(err); 308 | } 309 | return res.status(200).json(dbResponse); 310 | }); 311 | 312 | app.get("/routes/:workspace_id", async (req, res) => { 313 | const { workspace_id } = req.params; 314 | const queryText = ` 315 | SELECT * 316 | FROM endpoints 317 | WHERE workspace_id = $1;`; 318 | const dbResponse = await postgresClient.query(queryText, [workspace_id]); 319 | return res.status(200).json(dbResponse.rows); 320 | }); 321 | 322 | app.post("/routes/:workspace_id", async (req, res) => { 323 | const { workspace_id } = req.params; 324 | let queryText = ""; 325 | req.body.forEach((URI) => { 326 | queryText += ` 327 | INSERT INTO endpoints (method, path, tracking, workspace_id) 328 | VALUES ('${URI.method}', '${URI.path}', ${URI.tracking}, ${workspace_id}) 329 | ON CONFLICT ON CONSTRAINT endpoints_uq 330 | DO UPDATE SET tracking = ${URI.tracking};`; 331 | }); 332 | postgresClient.query(queryText); 333 | selectedEndpoints = req.body.filter((URI) => URI.tracking) || req.body; 334 | return res.sendStatus(204); 335 | }); 336 | 337 | // get existing workspaces for the user 338 | app.get("/workspaces", async (req, res) => { 339 | const queryText = ` 340 | SELECT * 341 | FROM workspaces 342 | ;`; 343 | const dbResponse = await postgresClient.query(queryText); 344 | return res.status(200).json(dbResponse.rows); 345 | }); 346 | 347 | // create a new workspace for the user 348 | app.post("/workspaces", async (req, res) => { 349 | const { name, domain, port, metricsPort } = req.body; 350 | let queryText = ` 351 | INSERT INTO workspaces (name, domain, port, metrics_port) 352 | VALUES ($1, $2, $3, $4) 353 | ;`; 354 | postgresClient.query(queryText, [name, domain, port, metricsPort]); 355 | return res.sendStatus(204); 356 | }); 357 | 358 | app.delete("/workspaces", async (req, res) => { 359 | const { workspace_id } = req.body; 360 | const queryText = ` 361 | DELETE FROM workspaces 362 | WHERE _id=${workspace_id} 363 | ;`; 364 | await postgresClient.query(queryText); 365 | return res.sendStatus(204); 366 | }); 367 | 368 | app.delete("/endpoints/:workspaceId", 369 | pgController.deleteEndpointsByWorkspaceId, 370 | async (req, res) => { 371 | return res.sendStatus(204); 372 | } 373 | ); 374 | 375 | app.listen(PORT, () => { 376 | console.log( 377 | `Application server started on port ${PORT}\n${MODE.toUpperCase()} mode` 378 | ); 379 | }); 380 | -------------------------------------------------------------------------------- /server/twilio/email.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv') 2 | const path = require('path'); 3 | dotenv.config({path: path.resolve(__dirname, "../../.env")}); 4 | 5 | const sendGridMail = require("@sendgrid/mail"); 6 | // console.log(process.env.SENDGRID_API_KEY) 7 | sendGridMail.setApiKey(process.env.SENDGRID_API_KEY); 8 | 9 | //function to build out the body of the email 10 | const msg = () => { 11 | const body = 12 | "This is a notifaction from the Datatective team. We have found an outage in your system, please open up the Datatective desktop application for more information."; 13 | return { 14 | to: "example@domain.com", // ! query from database 15 | from: "datadocteam@gmail.com", 16 | subject: "IMPORTANT: outage detected from datatective", 17 | text: body, 18 | html: "{body}", 19 | }; 20 | }; 21 | 22 | //send the message using the send method from the SendGrid email package 23 | async function sendEmail() { 24 | try { 25 | await sendGridMail.send(msg()); 26 | console.log("Email notification successfully send"); 27 | } catch (error) { 28 | console.log("There was an error sending an email notification"); 29 | console.log("THIS IS THE ERROR: ", error); 30 | console.log("this is the API key: ", process.env.SENDGRID_API_KEY); 31 | if (error.response) { 32 | console.log(error.response.body); 33 | } 34 | } 35 | } 36 | 37 | (async () => { 38 | console.log("Sending email"); 39 | await sendEmail(); 40 | })(); 41 | -------------------------------------------------------------------------------- /server/twilio/twilio.js: -------------------------------------------------------------------------------- 1 | const twilio = require("twilio"); 2 | -------------------------------------------------------------------------------- /template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |