├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .gitmodules ├── .npmignore ├── .travis.yml ├── README.md ├── examples └── images │ ├── example0.png │ ├── example1.png │ └── example2.png ├── lib ├── async-context.js ├── client-parser.js ├── middlewares │ ├── express.js │ ├── hapi.js │ └── koa.js ├── miniprofiler.js ├── storages │ ├── inmemory.js │ └── redis.js ├── ui.js └── utils.js ├── package.json └── tests ├── assets-test.js ├── basic-test.js ├── client-test.js ├── concurrent-async-test.js ├── custom-config-test.js ├── index.js ├── render-test.js ├── servers ├── async-provider.js ├── dummy-module.js ├── dummy-provider.js ├── express │ ├── async.js │ ├── custom-config.js │ ├── default.js │ ├── index.js │ ├── render.js │ ├── unauthorized.js │ └── unprofiled.js ├── hapi │ ├── async.js │ ├── custom-config.js │ ├── default.js │ ├── index.js │ ├── render.js │ ├── unauthorized.js │ └── unprofiled.js ├── index.js ├── koa │ ├── async.js │ ├── custom-config.js │ ├── default.js │ ├── index.js │ ├── render.js │ ├── unauthorized.js │ └── unprofiled.js └── views │ └── index.pug ├── share-test.js ├── step-test.js ├── timequery-test.js ├── unauthorized-test.js └── unprofiled-test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | ui 2 | coverage 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "node": true, 5 | "es6": true, 6 | "mocha": true 7 | }, 8 | "rules": { 9 | "semi": [2, "always"], 10 | "require-yield": 0, 11 | "strict": ["error", "global"], 12 | "no-unused-vars": ["error", { "vars": "all", "args": "none" }], 13 | "quotes": ["error", "single", { "avoidEscape": true } ], 14 | "space-before-function-paren": ["error", "never"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | pids 5 | *.pid 6 | *.seed 7 | *.rdb 8 | *.DS_STORE 9 | lib-cov 10 | coverage 11 | .nyc_output 12 | .grunt 13 | .lock-wscript 14 | build/Release 15 | node_modules 16 | jspm_packages 17 | .npm 18 | .node_repl_history 19 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ui"] 2 | path = ui 3 | url = https://github.com/MiniProfiler/ui.git 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | tests 3 | node_modules 4 | examples 5 | .travis.yml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "stable" 5 | 6 | sudo: required 7 | 8 | services: 9 | - docker 10 | 11 | before_script: 12 | - npm run start-services 13 | - npm run lint 14 | - sleep 3 15 | 16 | after_script: 17 | - npm run coverage 18 | - npm run check-coverage 19 | - npm run update-coveralls 20 | 21 | notifications: 22 | email: 23 | on_success: never 24 | on_failure: change 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MiniProfiler for Node.js 2 | 3 | Node.js implementation of Stack Exchange's MiniProfiler 4 | 5 | [![NPM](https://img.shields.io/npm/v/miniprofiler.svg)](https://www.npmjs.com/package/miniprofiler) 6 | [![Build](https://travis-ci.org/MiniProfiler/node.svg?branch=master)](https://travis-ci.org/MiniProfiler/node) 7 | [![Coverage](https://coveralls.io/repos/github/MiniProfiler/node/badge.svg?branch=master)](https://coveralls.io/github/MiniProfiler/node?branch=master) 8 | ![Dependencies](https://david-dm.org/MiniProfiler/node.svg) 9 | ![devDependencies](https://david-dm.org/MiniProfiler/node/dev-status.svg#info=devDependencies) 10 | 11 | ## Demonstration 12 | 13 | Visit [http://miniprofiler-demo.herokuapp.com](http://miniprofiler-demo.herokuapp.com) for a live demonstration. 14 | 15 | ## Installation 16 | 17 | ```bash 18 | $ npm install miniprofiler 19 | ``` 20 | 21 | You can hook up your application with any of the following packages are available on npm: 22 | 23 | | Name | About | Version | 24 | |-----------|-----------|-----------| 25 | | `miniprofiler-http` | Profile http(s) requests | [![NPM](https://img.shields.io/npm/v/miniprofiler-http.svg)](https://www.npmjs.com/package/miniprofiler-http) | 26 | | `miniprofiler-pg` | Profile [pg](https://www.npmjs.com/package/pg) queries | [![NPM](https://img.shields.io/npm/v/miniprofiler-pg.svg)](https://www.npmjs.com/package/miniprofiler-pg) | 27 | | `miniprofiler-redis`| Profile [redis](https://www.npmjs.com/package/redis) calls | [![NPM](https://img.shields.io/npm/v/miniprofiler-redis.svg)](https://www.npmjs.com/package/miniprofiler-redis) | 28 | 29 | ## Usage 30 | 31 | ### Simple usage with express.js 32 | 33 | `server.js` 34 | 35 | ```javascript 36 | var express = require('express') 37 | , miniprofiler = require('miniprofiler') 38 | , app = express(); 39 | 40 | app.set('view engine', 'pug'); 41 | app.use(miniprofiler.express()); 42 | 43 | app.get('/', function(req, res) { 44 | req.miniprofiler.step('Step 1', function() { 45 | req.miniprofiler.step('Step 2', function() { 46 | res.render('index'); 47 | }); 48 | }); 49 | }); 50 | 51 | app.listen(8080); 52 | ``` 53 | 54 | `index.pug` 55 | 56 | ```javascript 57 | doctype html 58 | html 59 | head 60 | title MiniProfiler Node.js Example 61 | body 62 | h1 Home Page 63 | | !{miniprofiler.include()} 64 | ``` 65 | 66 | When visiting `localhost:8080`, you should see this. 67 | 68 | ![](/examples/images/example0.png) 69 | 70 | ## API 71 | 72 | ### `miniprofiler.{framework}([options])` 73 | 74 | Replace `{framework}` with koa, express or hapi. 75 | 76 | This function returns a framework specific middleware that is responsible for initializing MiniProfiler on each request. 77 | 78 | #### `options` object properties 79 | | Property | Default | Description | 80 | |-----------|-----------|-------------| 81 | | enable | Always returns true | function(req, res) => boolean; this function is used to determine if the profiler should be enabled for the current request | 82 | | authorize | Always returns true | function(req, res) => boolean; this function is used to determine if the current request should be able to see the profiling results | 83 | 84 | ### `miniprofiler.{framework}.for([provider])` 85 | 86 | `provider` is a call for any of the supported providers listed [here](#installation). 87 | 88 | ### `miniprofiler.configure([options])` 89 | 90 | #### `options` object properties 91 | | Property | Default | Description | 92 | |-----------|-----------|-------------| 93 | | storage | InMemoryStorage({ max: 100, maxAge: 1000 \* 60 \* 60 }) | InMemoryStorage or RedisStorage; used to store or fetch a string JSON blob of profiling information | 94 | | ignoredPaths | [ ] | string array ; any request whose `url` property is in ignoredPaths will not be profiled | 95 | | trivialDurationThresholdMilliseconds | 2.5 | double ; any step lasting longer than this will be considered trivial, and hidden by default | 96 | | popupShowTimeWithChildren | false | boolean ; whether or not to include the "time with children" column | 97 | | popupRenderPosition | left | 'left', 'right', 'bottomLeft' or 'bottomRight' ; which side of the screen to display timings on | 98 | 99 | #### `options.storage` examples 100 | 101 | #### InMemoryStorage 102 | 103 | ``` 104 | miniprofiler.configure({ 105 | storage: miniprofiler.storage.InMemoryStorage({ lruCacheOptions }); 106 | }) 107 | ``` 108 | 109 | Refer to [lru-cache](https://www.npmjs.com/package/lru-cache) documentation for `lruCacheOptions`. 110 | 111 | #### RedisStorage 112 | 113 | ``` 114 | miniprofiler.configure({ 115 | storage: miniprofiler.storage.RedisStorage(client); 116 | }) 117 | ``` 118 | 119 | Where `client` is an instance of [redis.createClient](https://www.npmjs.com/package/redis). 120 | -------------------------------------------------------------------------------- /examples/images/example0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiniProfiler/node/3d131ef909c7d73f809934f5b3027192f3024979/examples/images/example0.png -------------------------------------------------------------------------------- /examples/images/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiniProfiler/node/3d131ef909c7d73f809934f5b3027192f3024979/examples/images/example1.png -------------------------------------------------------------------------------- /examples/images/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MiniProfiler/node/3d131ef909c7d73f809934f5b3027192f3024979/examples/images/example2.png -------------------------------------------------------------------------------- /lib/async-context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const asyncHooks = require('async_hooks'); 4 | 5 | class AsyncContext { 6 | constructor() { 7 | this.map = new Map(); 8 | asyncHooks.createHook({ 9 | init: (id, _type, triggerId) => { 10 | if (this.map.has(triggerId)) 11 | this.map.set(id, this.map.get(triggerId)); 12 | }, 13 | destroy: (id) => this.map.delete(id) 14 | }).enable(); 15 | } 16 | 17 | get() { 18 | const id = asyncHooks.executionAsyncId(); 19 | if (this.map.has(id)) 20 | return this.map.get(id); 21 | } 22 | 23 | set(val) { 24 | this.map.set(asyncHooks.executionAsyncId(), val); 25 | } 26 | } 27 | 28 | module.exports = new AsyncContext(); 29 | -------------------------------------------------------------------------------- /lib/client-parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('./utils.js'); 4 | 5 | let insertInOrder = (array, timing) => { 6 | if (timing.Start <= 0) 7 | return; 8 | 9 | for(let key in array) { 10 | if (timing.Start <= array[key].Start) { 11 | return array.splice(key, 0, timing); 12 | } 13 | } 14 | 15 | return array.push(timing); 16 | }; 17 | 18 | module.exports = (postData) => { 19 | let preffix = 'clientPerformance[timing]['; 20 | 21 | let postDataTimings = { }; 22 | let clientTimings = [ ]; 23 | let navigationStart = 0; 24 | 25 | for(let postDataKey in postData) { 26 | if (postDataKey.startsWith(preffix)) { 27 | let key = postDataKey.substring(preffix.length, postDataKey.length - 1); 28 | if (key == 'navigationStart') 29 | navigationStart = parseInt(postData[postDataKey]); 30 | else 31 | postDataTimings[key] = parseInt(postData[postDataKey]); 32 | } 33 | } 34 | 35 | if (!navigationStart) 36 | return null; 37 | 38 | for(let key in postDataTimings) { 39 | if (key.endsWith('Start')) { 40 | 41 | let eventName = key.slice(0, -5); 42 | let eventStartTime = postDataTimings[`${eventName}Start`]; 43 | let eventEndTime = postDataTimings[`${eventName}End`]; 44 | 45 | let timing = { 46 | Name: _.toTitleCase(eventName), 47 | Start: eventStartTime - navigationStart, 48 | Duration: eventEndTime - eventStartTime 49 | }; 50 | 51 | if (!timing.Duration) { 52 | timing.Name = _.toTitleCase(`${eventName}Start`); 53 | timing.Duration = -1; 54 | } 55 | 56 | insertInOrder(clientTimings, timing); 57 | 58 | } else if (!key.endsWith('End')) { 59 | insertInOrder(clientTimings, { 60 | Name: _.toTitleCase(key), 61 | Start: postDataTimings[key] - navigationStart, 62 | Duration: -1 63 | }); 64 | } 65 | } 66 | 67 | return { 68 | RedirectCount: parseInt(postData['clientPerformance[navigation][redirectCount]']), 69 | Timings: clientTimings 70 | }; 71 | }; 72 | -------------------------------------------------------------------------------- /lib/middlewares/express.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const asyncContext = require('../async-context'); 4 | 5 | module.exports = { 6 | buildMiddleware: function(provider) { 7 | return function(req, res, next) { 8 | provider.handler(req, res, next); 9 | }; 10 | }, 11 | mainMiddleware: function(enable, authorize, handleRequest, cls) { 12 | return function(req, res, next) { 13 | handleRequest(enable, authorize, req, res).then((handled) => { 14 | res.locals.miniprofiler = req.miniprofiler; 15 | 16 | asyncContext.set(req.miniprofiler); 17 | Object.defineProperty(req, 'miniprofiler', { get: () => asyncContext.get() }); 18 | 19 | var render = res.render; 20 | res.render = function() { 21 | var renderArguments = arguments; 22 | req.miniprofiler.step(`Render: ${arguments[0]}`, function() { 23 | render.apply(res, renderArguments); 24 | }); 25 | }; 26 | 27 | if (!handled) 28 | next(); 29 | }).catch(next); 30 | }; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /lib/middlewares/hapi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const asyncContext = require('../async-context'); 4 | 5 | module.exports = { 6 | buildMiddleware: function(provider) { 7 | var plugin = { 8 | register: (server, options, next) => { 9 | server.ext('onRequest', function(request, reply) { 10 | provider.handler(request.raw.req, request.raw.res, () => { 11 | return reply.continue(); 12 | }); 13 | }); 14 | next(); 15 | } 16 | }; 17 | 18 | plugin.register.attributes = { 19 | name: `miniprofiler-hapi-${provider.name}`, 20 | version: require('../../package.json').version 21 | }; 22 | 23 | return plugin; 24 | }, 25 | mainMiddleware: function(enable, authorize, handleRequest) { 26 | var plugin = { 27 | register: (server, options, next) => { 28 | server.ext('onRequest', function(request, reply) { 29 | handleRequest(enable, authorize, request.raw.req, request.raw.res).then((handled) => { 30 | asyncContext.set(request.raw.req.miniprofiler); 31 | Object.defineProperty(request.app, 'miniprofiler', { get: () => asyncContext.get() }); 32 | Object.defineProperty(request.raw.req, 'miniprofiler', { get: () => asyncContext.get() }); 33 | 34 | if (!handled) 35 | reply.continue(); 36 | }); 37 | }); 38 | next(); 39 | } 40 | }; 41 | 42 | plugin.register.attributes = { 43 | name: 'miniprofiler-hapi', 44 | version: require('../../package.json').version 45 | }; 46 | 47 | //That's a bad monkey patch, didn't like it, needs refactor... 48 | plugin.vision = (server) => { 49 | var view = server._replier._decorations['view']; 50 | 51 | server._replier._decorations['view'] = function(template, context, options) { 52 | var viewArguments = arguments; 53 | this.request.raw.req.miniprofiler.step(`Render: ${template}`, () => { 54 | return view.apply(this, viewArguments); 55 | }); 56 | }; 57 | }; 58 | 59 | return plugin; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /lib/middlewares/koa.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const asyncContext = require('../async-context'); 4 | 5 | module.exports = { 6 | buildMiddleware: function(provider) { 7 | return function *(next) { 8 | yield new Promise((resolve, reject) => { 9 | provider.handler(this.req, this.res, resolve); 10 | }); 11 | yield next; 12 | }; 13 | }, 14 | mainMiddleware: function(enable, authorize, handleRequest) { 15 | return function *(next) { 16 | var handled = yield handleRequest(enable, authorize, this.req, this.res); 17 | 18 | asyncContext.set(this.req.miniprofiler); 19 | Object.defineProperty(this.state, 'miniprofiler', { get: () => asyncContext.get() }); 20 | Object.defineProperty(this.req, 'miniprofiler', { get: () => asyncContext.get() }); 21 | 22 | if (this.render) { 23 | var render = this.render; 24 | this.render = function() { 25 | return new Promise((resolve, reject) => { 26 | var renderArguments = arguments; 27 | this.req.miniprofiler.step(`Render: ${arguments[0]}`, function() { 28 | render.apply(this, renderArguments); 29 | resolve(); 30 | }); 31 | }); 32 | }; 33 | } 34 | 35 | if (!handled) 36 | yield next; 37 | }; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /lib/miniprofiler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * MiniProfiler implementation for node.js. 5 | * 6 | * Apache License, Version 2.0 7 | * 8 | * Kevin Montrose, 2013 @kevin-montrose 9 | * Matt Jibson, 2013 @mjibsonF 10 | * Guilherme Oenning, 2016 @goenning 11 | */ 12 | 13 | var _ = require('./utils.js'); 14 | var qs = require('querystring'); 15 | var url = require('url'); 16 | var ui = require('./ui.js'); 17 | var clientParser = require('./client-parser.js'); 18 | 19 | const hostname = require('os').hostname; 20 | var ignoredPaths = []; 21 | var trivialDurationThresholdMilliseconds = 2.5; 22 | var popupShowTimeWithChildren = false; 23 | var popupRenderPosition = 'left'; 24 | var resourcePath = '/mini-profiler-resources/'; 25 | 26 | exports.storage = { 27 | InMemoryStorage: require('./storages/inmemory.js'), 28 | RedisStorage: require('./storages/redis.js') 29 | }; 30 | 31 | var storage = new exports.storage.InMemoryStorage({ max: 100, maxAge: 1000 * 60 * 60 }); 32 | 33 | exports.configure = configure; 34 | configure(); 35 | 36 | for (let framework of ['koa', 'express', 'hapi']) { 37 | let func = require(`./middlewares/${framework}.js`); 38 | 39 | exports[framework] = function(options) { 40 | options = options || {}; 41 | 42 | if (!options.enable) options.enable = () => { return true; }; 43 | if (!options.authorize) options.authorize = () => { return true; }; 44 | 45 | return func.mainMiddleware(options.enable, options.authorize, handleRequest); 46 | }; 47 | 48 | exports[framework].for = func.buildMiddleware; 49 | } 50 | 51 | var version = require('../package.json').version; 52 | 53 | var contentTypes = { 54 | css: 'text/css', 55 | js: 'text/javascript', 56 | tmpl: 'text/html; charset=utf-8' 57 | }; 58 | 59 | function getPath(req) { 60 | return url.parse(req.url).path; 61 | } 62 | 63 | function handleRequest(enable, authorize, req, res) { 64 | return new Promise((resolve, reject) => { 65 | var enabled = enable(req, res); 66 | var authorized = authorize(req, res); 67 | var requestPath = url.parse(req.url).pathname; 68 | 69 | for (let ignoredPath of ignoredPaths) { 70 | if (requestPath.startsWith(ignoredPath)) { 71 | enabled = false; 72 | break; 73 | } 74 | } 75 | 76 | if (!requestPath.startsWith(resourcePath)) { 77 | var extension = startProfiling(req, enabled, authorized); 78 | if (enabled) { 79 | res.on('finish', () => { 80 | stopProfiling(extension, req); 81 | }); 82 | res.setHeader('X-MiniProfiler-Ids', `["${extension.id}"]`); 83 | } 84 | return resolve(false); 85 | } 86 | 87 | if (!authorized) { 88 | res.writeHead(401, { 'Content-Type': 'text/plain; charset=utf-8' }); 89 | res.end(''); 90 | return resolve(true); 91 | } 92 | 93 | if (!enabled) { 94 | res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); 95 | res.end('MiniProfiler is disabled'); 96 | return resolve(true); 97 | } 98 | 99 | var segments = _.compact(requestPath.split('/')); 100 | var lastPathSegment = segments[segments.length - 1]; 101 | var handler = (lastPathSegment == 'results') ? results : assets; 102 | handler(req, res, lastPathSegment, (result) => { 103 | res.writeHead(result.status, { 'Content-Type': result.type }); 104 | res.end(result.body); 105 | resolve(true); 106 | }); 107 | }); 108 | } 109 | 110 | function assets(req, res, lastPathSegment, done) { 111 | ui.readFile(lastPathSegment, function(err, data) { 112 | if (err) { 113 | done({ 114 | type: 'text/plain; charset=utf-8', 115 | status: 404, 116 | body: 'Resource unavailable.' 117 | }); 118 | } else { 119 | var rs = lastPathSegment.split('.'); 120 | res.setHeader('Cache-Control', 'public, max-age=31557600'); 121 | done({ 122 | type: contentTypes[rs[rs.length - 1]], 123 | status: 200, 124 | body: data 125 | }); 126 | } 127 | }); 128 | } 129 | 130 | function results(req, res, lastPathSegment, done) { 131 | var proc = function(post, done) { 132 | 133 | var query = url.parse(req.url, true).query; 134 | var id = post.id || query.id; 135 | var popup = post.popup || query.popup; 136 | var timing = (req.method === 'POST') ? clientParser(post) : null; 137 | 138 | storage.get(id, (err, data) => { 139 | 140 | if (!data) { 141 | done({ 142 | type: 'text/plain; charset=utf-8', 143 | status: 404, 144 | body: `Id '${id}' not found.` 145 | }); 146 | return; 147 | } 148 | 149 | var json = JSON.parse(data); 150 | if (timing) { 151 | json.ClientTimings = timing; 152 | data = JSON.stringify(json); 153 | storage.set(id, data); 154 | } 155 | 156 | if (popup == '1') { 157 | done({ 158 | type: 'application/json', 159 | status: 200, 160 | body: data 161 | }); 162 | return; 163 | } 164 | 165 | done({ 166 | type: 'text/html; charset=utf-8', 167 | status: 200, 168 | body: ui.share({ 169 | name: json.Name, 170 | duration: json.DurationMilliseconds, 171 | path: resourcePath, 172 | json: data, 173 | includes: include(id), 174 | version: version 175 | }) 176 | }); 177 | }); 178 | }; 179 | 180 | var body = ''; 181 | req.on('data', function(data) { 182 | body += data; 183 | }); 184 | 185 | req.on('end', function() { 186 | var post = qs.parse(body); 187 | proc(post, done); 188 | }); 189 | } 190 | 191 | function include(id) { 192 | return ui.partial({ 193 | path: resourcePath, 194 | position: popupRenderPosition, 195 | showChildren: popupShowTimeWithChildren, 196 | trivialMilliseconds: trivialDurationThresholdMilliseconds, 197 | 198 | version: version, 199 | currentId: id, 200 | ids: id, 201 | showTrivial: true, 202 | maxTracesToShow: 15, 203 | showControls: true, 204 | authorized: true, 205 | toggleShortcut: '', 206 | startHidden: false 207 | }); 208 | } 209 | 210 | /* 211 | * Setup profiling. This function may only be called once, subsequent calls are ignored. 212 | * 213 | * This must be called before the first call to startProfiling. 214 | * 215 | * options is an optional object, which can have the following fields: 216 | * - storage: InMemoryStorage or RedisStorage; used to store or fetch a string JSON blob of profiling information 217 | * - ignoredPaths: string array ; any request whose `url` property is in ignoredPaths will not be profiled 218 | * - trivialDurationThresholdMilliseconds: double ; any step lasting longer than this will be considered trivial, and hidden by default 219 | * - popupShowTimeWithChildren: boolean ; whether or not to include the "time with children" column 220 | * - popupRenderPosition: 'left', 'right', 'bottomLeft', 'bottomRight' ; which side of the screen to display timings on 221 | * - resourcePath: string ; if your site root is in a subdirectory, specify here, e.g., /siteroot 222 | */ 223 | function configure(options) { 224 | options = options || {}; 225 | 226 | ignoredPaths = options.ignoredPaths || ignoredPaths; 227 | trivialDurationThresholdMilliseconds = options.trivialDurationThresholdMilliseconds || trivialDurationThresholdMilliseconds; 228 | popupShowTimeWithChildren = options.popupShowTimeWithChildren || popupShowTimeWithChildren; 229 | popupRenderPosition = options.popupRenderPosition || popupRenderPosition; 230 | storage = options.storage || storage; 231 | resourcePath = `${options.resourcePath ? options.resourcePath.replace(/\/$/, '') : ''}${resourcePath}`; 232 | } 233 | 234 | /* 235 | * Begins profiling the given request. 236 | */ 237 | function startProfiling(request, enabled, authorized) { 238 | var currentRequestExtension = { 239 | enabled: enabled, 240 | authorized: authorized 241 | }; 242 | 243 | if (enabled) { 244 | var path = getPath(request); 245 | currentRequestExtension.id = _.uuid(); 246 | currentRequestExtension.startDate = Date.now(); 247 | currentRequestExtension.startTime = process.hrtime(); 248 | currentRequestExtension.stopTime = null; 249 | currentRequestExtension.stepGraph = makeStep(path, currentRequestExtension.startTime, null); 250 | currentRequestExtension.customTimings = {}; 251 | } 252 | 253 | currentRequestExtension.timeQuery = function() { 254 | var args = Array.prototype.slice.call(arguments, enabled ? 0 : 3); 255 | if (enabled) { 256 | args.unshift(currentRequestExtension); 257 | timeQuery.apply(this, args); 258 | } else { 259 | arguments[2].apply(this, args); 260 | } 261 | }; 262 | 263 | currentRequestExtension.startTimeQuery = function(type, query) { 264 | return startTimeQuery.call(this, currentRequestExtension, type, query); 265 | }; 266 | 267 | currentRequestExtension.stopTimeQuery = function(timing) { 268 | return stopTimeQuery.call(this, timing); 269 | }; 270 | 271 | currentRequestExtension.step = function(name, call) { 272 | if (enabled) { 273 | step(name, request, call); 274 | } else { 275 | call(); 276 | } 277 | }; 278 | 279 | currentRequestExtension.include = function() { 280 | return enabled && authorized ? include(currentRequestExtension.id) : ''; 281 | }; 282 | 283 | request.miniprofiler = currentRequestExtension; 284 | 285 | return currentRequestExtension; 286 | } 287 | 288 | /* 289 | * Stops profiling the given request. 290 | */ 291 | function stopProfiling(extension, request) { 292 | var time = process.hrtime(); 293 | 294 | extension.stopTime = time; 295 | extension.stepGraph.stopTime = time; 296 | 297 | var json = describePerformance(extension, request); 298 | storage.set(extension.id, JSON.stringify(json)); 299 | } 300 | 301 | /* 302 | * Wraps an invokation of `call` in a step named `name`. 303 | * 304 | * You should only use this method directly in cases when calls to addProfiling won't suffice. 305 | */ 306 | function step(name, request, call) { 307 | var time = process.hrtime(); 308 | 309 | var extension = request.miniprofiler; 310 | 311 | var newStep = makeStep(name, time, extension.stepGraph); 312 | extension.stepGraph.steps.push(newStep); 313 | extension.stepGraph = newStep; 314 | 315 | var result; 316 | if (call.length) { 317 | result = call(() => { 318 | unstep(name, request); 319 | }); 320 | } else { 321 | try { 322 | result = call(); 323 | } finally { 324 | unstep(name, request); 325 | } 326 | } 327 | 328 | return result; 329 | } 330 | 331 | /* 332 | * Called to time a query, like to SQL or Redis, that completes with a callback 333 | * 334 | * `type` can be any string, it is used to group query types in timings. 335 | * `query` is a string representing the query, this is what is recorded as having run. 336 | * 337 | * `executeFunction` is invoked with any additional parameters following it. 338 | * 339 | * Any function passed as a parameter to `executeFunction` will be instrumented to detect 340 | * when the query has completed. Implicitly, any execution of a callback is considered 341 | * to have ended the query. 342 | */ 343 | function timeQuery(extension, type, query, executeFunction) { 344 | var timing = startTimeQuery(extension, type, query); 345 | var params = Array.prototype.slice.call(arguments, 4); 346 | 347 | for (var i = 0; i < params.length; i++) { 348 | if (_.isFunction(params[i])) { 349 | var param = params[i]; 350 | params[i] = function() { 351 | extension.stopTimeQuery(timing); 352 | var ret = param.apply(this, arguments); 353 | return ret; 354 | }; 355 | } 356 | } 357 | 358 | var ret = executeFunction.apply(this, params); 359 | return ret; 360 | } 361 | 362 | function stopTimeQuery(timing) { 363 | timing.stopTime = process.hrtime(); 364 | } 365 | 366 | function startTimeQuery(extension, type, query) { 367 | var time = process.hrtime(); 368 | var startDate = Date.now(); 369 | 370 | extension.stepGraph.customTimings[type] = extension.stepGraph.customTimings[type] || []; 371 | 372 | var customTiming = { 373 | id: _.uuid(), 374 | executeType: type, 375 | commandString: _.escape(query), 376 | startTime: time, 377 | startDate: startDate, 378 | callStack: new Error().stack 379 | }; 380 | 381 | extension.stepGraph.customTimings[type].push(customTiming); 382 | 383 | return customTiming; 384 | } 385 | 386 | function unstep(name, request) { 387 | var time = process.hrtime(); 388 | var extension = request.miniprofiler; 389 | extension.stepGraph.stopTime = time; 390 | // step back up 391 | extension.stepGraph = extension.stepGraph.parent; 392 | } 393 | 394 | function describePerformance(root, request) { 395 | var ret = {}; 396 | 397 | ret.Id = root.id; 398 | ret.Name = getPath(request); 399 | ret.Started = root.startDate; 400 | ret.MachineName = hostname(); 401 | ret.Root = describeTimings(root.stepGraph, root.stepGraph); 402 | ret.ClientTimings = null; 403 | ret.DurationMilliseconds = ret.Root.DurationMilliseconds; 404 | 405 | return ret; 406 | } 407 | 408 | function diff(start, stop) { 409 | var deltaSecs = stop[0] - start[0]; 410 | var deltaNanoSecs = stop[1] - start[1]; 411 | 412 | var elapsedMs = deltaSecs * 1000 + deltaNanoSecs / 1000000; 413 | 414 | return elapsedMs; 415 | } 416 | 417 | function callStack(stack) { 418 | var sp = stack.split('\n'); 419 | var ret = []; 420 | for (var i = 2; i < sp.length; i++) { 421 | var st = sp[i].trim().split(' '); 422 | ret.push(st[1]); 423 | } 424 | return ret.join(' '); 425 | } 426 | 427 | function describeTimings(timing, root) { 428 | var id = _.uuid(); 429 | var name = timing.name; 430 | var elapsedMs = diff(timing.startTime, timing.stopTime); 431 | var sinceRootMs = diff(root.startTime, timing.startTime); 432 | var customTimings = describeCustomTimings(timing.customTimings, root); 433 | 434 | var children = []; 435 | for (var i = 0; i < timing.steps.length; i++) { 436 | var step = timing.steps[i]; 437 | children.push(describeTimings(step, root)); 438 | } 439 | 440 | return { 441 | Id: id, 442 | Name: name, 443 | DurationMilliseconds: elapsedMs, 444 | StartMilliseconds: sinceRootMs, 445 | Children: children, 446 | CustomTimings: customTimings 447 | }; 448 | } 449 | 450 | function describeCustomTimings(customTimings, root) { 451 | var ret = {}; 452 | for (var prop in customTimings) { 453 | 454 | var arr = customTimings[prop]; 455 | var retArr = []; 456 | 457 | for (var i = 0; i < arr.length; i++) { 458 | var timing = {}; 459 | timing.Id = arr[i].id; 460 | timing.ExecuteType = arr[i].executeType; 461 | timing.CommandString = arr[i].commandString; 462 | timing.StartMilliseconds = diff(root.startTime, arr[i].startTime); 463 | timing.DurationMilliseconds = diff(arr[i].startTime, arr[i].stopTime); 464 | timing.StackTraceSnippet = callStack(arr[i].callStack); 465 | 466 | retArr.push(timing); 467 | } 468 | 469 | ret[prop] = retArr; 470 | } 471 | 472 | return ret; 473 | } 474 | 475 | function makeStep(name, time, parent) { 476 | return { name: name, startTime: time, stopTime: null, parent: parent, steps: [], customTimings: {} }; 477 | } -------------------------------------------------------------------------------- /lib/storages/inmemory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var LRU = require('lru-cache'); 4 | let miniprofilerHashKey = '_miniprofiler_'; 5 | 6 | function InMemoryStorage(options) { 7 | this.key = function(id) { 8 | return `${miniprofilerHashKey}${id}`; 9 | }; 10 | 11 | this.cache = LRU(options); 12 | 13 | this.get = function(id, callback) { 14 | var data = this.cache.get(this.key(id)); 15 | if (data) 16 | callback(null, data); 17 | else 18 | callback(new Error(`Id '${id}' not found.`)); 19 | }; 20 | 21 | this.set = function(id, json) { 22 | this.cache.set(this.key(id), json); 23 | }; 24 | } 25 | 26 | module.exports = InMemoryStorage; -------------------------------------------------------------------------------- /lib/storages/redis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let miniprofilerHashKey = '_miniprofiler_'; 4 | 5 | function RedisStorage(redisClient, maxAge) { 6 | this.maxAge = maxAge || 3600; 7 | 8 | this.key = function(id) { 9 | return `${miniprofilerHashKey}${id}`; 10 | }; 11 | 12 | this.get = function(id, callback) { 13 | redisClient.get(this.key(id), callback); 14 | }; 15 | 16 | this.set = function(id, json) { 17 | let key = this.key(id); 18 | redisClient.set(key, json, (err, data) => { 19 | redisClient.expire(key, this.maxAge); 20 | }); 21 | }; 22 | } 23 | 24 | module.exports = RedisStorage; -------------------------------------------------------------------------------- /lib/ui.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fileStore = { }; 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const _ = require('./utils.js'); 8 | const includesDir = path.join(__dirname, '../ui'); 9 | 10 | const readFile = (name, callback) => { 11 | if (fileStore[name]) { 12 | callback(null, fileStore[name]); 13 | } else { 14 | fs.readFile(path.join(includesDir, name), 'utf-8', (err, text) => { 15 | fileStore[name] = text; 16 | callback(err, text); 17 | }); 18 | } 19 | }; 20 | 21 | const templates = { 22 | partial: _.template(fs.readFileSync(path.join(includesDir, 'include.partial.html')).toString()), 23 | share: _.template(fs.readFileSync(path.join(includesDir, 'share.html')).toString()) 24 | }; 25 | 26 | const partial = (options) => templates.partial(options); 27 | 28 | const share = (options) => templates.share(options); 29 | 30 | module.exports = { readFile, partial, share }; 31 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isFunction = (obj) => { 4 | return typeof obj == 'function' || false; 5 | }; 6 | 7 | const tagsToReplace = { 8 | '&': '&', 9 | '<': '<', 10 | '>': '>', 11 | '"': '"', 12 | "'": ''', 13 | '`': '`' 14 | }; 15 | 16 | const escape = (str) => { 17 | return str.replace(/[&<>]/g, function(tag) { 18 | return tagsToReplace[tag] || tag; 19 | }); 20 | }; 21 | 22 | const compact = (arr) => { 23 | return arr.filter((v) => v); 24 | }; 25 | 26 | const uuid = () => { 27 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 28 | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); 29 | return v.toString(16); 30 | }); 31 | }; 32 | 33 | const toTitleCase = (str) => { 34 | return str.replace(/ /g,'').split(/(?=[A-Z])/).join(' ').replace(/^.| ./g, (m) => { 35 | return m.toUpperCase(); 36 | }); 37 | }; 38 | 39 | const template = (content) => { 40 | return (options) => { 41 | return content.replace(/{(.+?)}/g, (match, key) => { 42 | return options[key]; 43 | }); 44 | }; 45 | }; 46 | 47 | module.exports = { 48 | isFunction, escape, compact, uuid, toTitleCase, template 49 | }; 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miniprofiler", 3 | "version": "2.0.0", 4 | "description": "A simple but effective mini-profiler.", 5 | "main": "lib/miniprofiler.js", 6 | "scripts": { 7 | "start-services": "docker run -d -p 6060:6379 redis", 8 | "lint": "eslint .", 9 | "test": "mocha tests/ -c", 10 | "coverage": "istanbul cover ./node_modules/mocha/bin/_mocha -- tests/ -R spec", 11 | "check-coverage": "istanbul check-coverage --statements 95 --branches 95 --functions 95 --lines 95", 12 | "update-coveralls": "cat coverage/lcov.info | node ./node_modules/coveralls/bin/coveralls.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/MiniProfiler/node.git" 17 | }, 18 | "bugs": { 19 | "url": "http://github.com/MiniProfiler/node/issues" 20 | }, 21 | "author": "Guilherme Oenning (http://goenning.net/)", 22 | "contributors": [ 23 | "Matt Jibson (https://mattjibson.com/)", 24 | "Kevin Montrose" 25 | ], 26 | "license": "Apache-2.0", 27 | "readmeFilename": "README.md", 28 | "dependencies": { 29 | "lru-cache": "^4.0.1" 30 | }, 31 | "tags": [ 32 | "profiler", 33 | "performance", 34 | "profiling", 35 | "timing", 36 | "web profiling" 37 | ], 38 | "devDependencies": { 39 | "chai": "^3.5.0", 40 | "coveralls": "^2.11.11", 41 | "docker-ip": "^2.0.1", 42 | "eslint": "^6.6.0", 43 | "express": "^4.13.4", 44 | "hapi": "^13.5.0", 45 | "istanbul": "^0.4.3", 46 | "koa": "^1.2.1", 47 | "koa-route": "^2.4.2", 48 | "koa-views": "^4.1.0", 49 | "debug": "^2.6.1", 50 | "mocha": "^2.5.3", 51 | "pug": "^2.0.0-beta2", 52 | "redis": "^3.1.1", 53 | "request": "^2.73.0", 54 | "vision": "^4.1.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/assets-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var fs = require('fs'); 5 | 6 | module.exports = function(server) { 7 | describe('Assets Tests', function() { 8 | before(server.setUp.bind(null, 'default')); 9 | after(server.tearDown); 10 | 11 | var files = [ 12 | 'includes.css', 13 | 'includes.css', 14 | 'includes.tmpl', 15 | 'includes.js' 16 | ]; 17 | 18 | files.forEach((file) => { 19 | it(`Should return ${file} file`, function(done) { 20 | server.get(`/mini-profiler-resources/${file}`, (err, response, body) => { 21 | fs.readFile(`./ui/${file}`, 'utf-8', (err, content) => { 22 | expect(body).to.be.equal(content); 23 | done(); 24 | }); 25 | }); 26 | }); 27 | }); 28 | 29 | it('Unknown file should return 404', function(done) { 30 | server.get('/mini-profiler-resources/unknown.js', (err, response, body) => { 31 | expect(response.statusCode).to.be.equal(404); 32 | expect(body).to.be.equal('Resource unavailable.'); 33 | expect(response.headers['content-type']).to.be.equal('text/plain; charset=utf-8'); 34 | done(); 35 | }); 36 | }); 37 | 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /tests/basic-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var pkg = require('../package.json'); 5 | 6 | module.exports = function(server) { 7 | describe('Basic Tests', function() { 8 | before(server.setUp.bind(null, 'default')); 9 | after(server.tearDown); 10 | 11 | it('Profiled routes should always return Profiler ID', function(done) { 12 | server.get('/', (err, response) => { 13 | expect(response.headers).to.include.keys('x-miniprofiler-ids'); 14 | done(); 15 | }); 16 | }); 17 | 18 | it('Index page should include MiniProfiler javascript', function(done) { 19 | server.get('/', (err, response, body) => { 20 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 21 | expect(ids).to.have.lengthOf(1); 22 | 23 | expect(body.trim()).to.be.equal(``); 24 | done(); 25 | }); 26 | }); 27 | 28 | it('Should return url parameters on results response', function(done) { 29 | server.get('/?key1=value1&key2=value2', (err, response, body) => { 30 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 31 | expect(ids).to.have.lengthOf(1); 32 | 33 | server.get(`/mini-profiler-resources/results?id=${ids[0]}&popup=1`, (err, response, body) => { 34 | var result = JSON.parse(body); 35 | expect(result.Id).to.equal(ids[0]); 36 | expect(result.Name).to.equal('/?key1=value1&key2=value2'); 37 | expect(result.Root.Children).to.be.empty; 38 | 39 | done(); 40 | }); 41 | }); 42 | }); 43 | 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /tests/client-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | 5 | module.exports = function(server) { 6 | describe('Client Timing Tests', function() { 7 | before(server.setUp.bind(null, 'default')); 8 | after(server.tearDown); 9 | 10 | it('should return client timing data that is sent on POST', function(done) { 11 | server.get('/', (err, response) => { 12 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 13 | 14 | server.post('/mini-profiler-resources/results', { 15 | id: ids[0], 16 | popup: 1, 17 | clientPerformance: { 18 | navigation: { 19 | redirectCount: 0 20 | }, 21 | timing: { 22 | navigationStart: 1000, 23 | responseEnd: 1014, 24 | loadEventStart: 1080, 25 | requestStart: 1001, 26 | secureConnectionStart: 0, 27 | loadEventEnd: 1112, 28 | responseStart: 1002, 29 | 'First Paint Time':1200 30 | } 31 | } 32 | }, (err, response, body) => { 33 | var data = JSON.parse(body); 34 | expect(data.ClientTimings).to.be.deep.equal({ 35 | RedirectCount: 0, 36 | Timings: [{ 37 | Name: 'Request Start', 38 | Start: 1, 39 | Duration: -1 40 | },{ 41 | Name: 'Response', 42 | Start: 2, 43 | Duration: 12 44 | },{ 45 | Name: 'Load Event', 46 | Start: 80, 47 | Duration: 32 48 | },{ 49 | Name: 'First Paint Time', 50 | Start: 200, 51 | Duration: -1 52 | }] 53 | }); 54 | done(); 55 | }); 56 | }); 57 | }); 58 | 59 | it('should not return client timing when data is not sent via POST', function(done) { 60 | server.get('/', (err, response) => { 61 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 62 | 63 | server.post('/mini-profiler-resources/results', { 64 | id: ids[0], 65 | popup: 1 66 | }, (err, response, body) => { 67 | var data = JSON.parse(body); 68 | expect(data.ClientTimings).to.be.null; 69 | done(); 70 | }); 71 | }); 72 | }); 73 | 74 | }); 75 | 76 | }; 77 | -------------------------------------------------------------------------------- /tests/concurrent-async-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | 5 | module.exports = function(server) { 6 | describe('Concurrent Async Requests', function() { 7 | before(server.setUp.bind(null, 'async')); 8 | after(server.tearDown); 9 | 10 | it('Each profile runs on its own context', function(done) { 11 | let countDone = 0; 12 | const partialDone = () => { if (++countDone === 2) done(); }; 13 | 14 | server.get('/', (err, response) => { 15 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 16 | expect(ids).to.have.lengthOf(1); 17 | 18 | server.post('/mini-profiler-resources/results/', { id: ids[0], popup: 1 }, (err, response, body) => { 19 | var result = JSON.parse(body); 20 | expect(result.Root.CustomTimings.async).to.have.lengthOf(2); 21 | partialDone(); 22 | }); 23 | }); 24 | 25 | server.get('/?once=true', (err, response) => { 26 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 27 | expect(ids).to.have.lengthOf(1); 28 | 29 | server.post('/mini-profiler-resources/results/', { id: ids[0], popup: 1 }, (err, response, body) => { 30 | var result = JSON.parse(body); 31 | expect(result.Root.CustomTimings.async).to.have.lengthOf(1); 32 | partialDone(); 33 | }); 34 | }); 35 | }); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /tests/custom-config-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var pkg = require('../package.json'); 5 | 6 | module.exports = function(server) { 7 | describe('Custom Configuration Tests', function() { 8 | this.timeout(5000); 9 | 10 | before(server.setUp.bind(null, 'custom-config')); 11 | after(server.tearDown); 12 | 13 | it('should include MiniProfiler javascript with custom settings', function(done) { 14 | server.get('/', (err, response, body) => { 15 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 16 | expect(ids).to.have.lengthOf(1); 17 | 18 | expect(body.trim()).to.be.equal(``); 19 | done(); 20 | }); 21 | }); 22 | 23 | it('should get/set timing from redis storage', function(done) { 24 | server.get('/?key=value', (err, response, body) => { 25 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 26 | expect(ids).to.have.lengthOf(1); 27 | 28 | server.post('/mini-profiler-resources/results', { id: ids[0], popup: 1 }, (err, response, body) => { 29 | var result = JSON.parse(body); 30 | expect(result.Id).to.equal(ids[0]); 31 | expect(result.Name).to.equal('/?key=value'); 32 | expect(result.Root.Children).to.be.empty; 33 | 34 | done(); 35 | }); 36 | }); 37 | }); 38 | 39 | it('should not get timing about from ignored paths', function(done) { 40 | server.get('/hidden', (err, response, body) => { 41 | expect(response.headers).to.not.include.keys('x-miniprofiler-ids'); 42 | done(); 43 | }); 44 | }); 45 | 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var servers = require('./servers'); 5 | 6 | var testCases = fs.readdirSync('./tests').filter((file) => file.endsWith('-test.js')); 7 | 8 | for (var server of servers) { 9 | describe(`[${server.framework}]`, function() { 10 | for (var testCase of testCases) { 11 | require(`./${testCase}`)(server); 12 | } 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /tests/render-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | 5 | module.exports = function(server) { 6 | 7 | describe('Render Tests', function() { 8 | before(server.setUp.bind(null, 'render')); 9 | after(server.tearDown); 10 | 11 | it('Should add render step', function(done) { 12 | server.get('/', (err, response) => { 13 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 14 | expect(ids).to.have.lengthOf(1); 15 | 16 | server.post('/mini-profiler-resources/results', { id: ids[0], popup: 1 }, (err, response, body) => { 17 | var result = JSON.parse(body); 18 | expect(result.Id).to.equal(ids[0]); 19 | expect(result.Name).to.equal('/'); 20 | expect(result.Root.Children).to.have.lengthOf(1); 21 | 22 | expect(result.Root.Children[0].Name).to.equal('Render: index'); 23 | expect(result.Root.Children[0].Children).to.be.empty; 24 | 25 | done(); 26 | }); 27 | }); 28 | }); 29 | 30 | it('Should add render step inside another step', function(done) { 31 | server.get('/inside-step', (err, response) => { 32 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 33 | expect(ids).to.have.lengthOf(1); 34 | 35 | server.post('/mini-profiler-resources/results', { id: ids[0], popup: 1 }, (err, response, body) => { 36 | var result = JSON.parse(body); 37 | 38 | expect(result.Id).to.equal(ids[0]); 39 | expect(result.Name).to.equal('/inside-step'); 40 | expect(result.Root.Children).to.have.lengthOf(1); 41 | 42 | expect(result.Root.Children[0].Name).to.equal('Step 1'); 43 | expect(result.Root.Children[0].Children).to.have.lengthOf(1); 44 | expect(result.Root.Children[0].CustomTimings).to.have.property('custom'); 45 | expect(result.Root.Children[0].CustomTimings.custom).to.have.lengthOf(1); 46 | 47 | expect(result.Root.Children[0].Children[0].Name).to.be.equal('Render: index'); 48 | expect(result.Root.Children[0].Children[0].Children).to.be.empty; 49 | 50 | done(); 51 | }); 52 | }); 53 | }); 54 | 55 | }); 56 | 57 | }; 58 | -------------------------------------------------------------------------------- /tests/servers/async-provider.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(obj) { 4 | return { 5 | name: 'dummy-async', 6 | handler: function(req, res, next) { 7 | obj.asyncFn = function() { 8 | const timing = req.miniprofiler.startTimeQuery('async', 'dummy call'); 9 | 10 | return new Promise(resolve => { 11 | setTimeout(() => { 12 | req.miniprofiler.stopTimeQuery(timing); 13 | resolve(); 14 | }, 25); 15 | }); 16 | }; 17 | 18 | next(); 19 | } 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /tests/servers/dummy-module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | asyncFn: () => Promise.resolve() 5 | }; 6 | -------------------------------------------------------------------------------- /tests/servers/dummy-provider.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function() { 4 | return { 5 | name: 'dummy', 6 | handler: function(req, res, next) { 7 | next(); 8 | } 9 | }; 10 | }; -------------------------------------------------------------------------------- /tests/servers/express/async.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | var dummyModule = require('../dummy-module'); 5 | var express = require('express'); 6 | 7 | var app = express(); 8 | 9 | app.use(miniprofiler.express()); 10 | app.use(miniprofiler.express.for(require('../async-provider.js')(dummyModule))); 11 | 12 | app.get('/', (req, res) => { 13 | dummyModule.asyncFn().then(() => { 14 | Promise.resolve(req.query.once ? undefined : dummyModule.asyncFn()) 15 | .then(() => res.send(res.locals.miniprofiler.include())); 16 | }); 17 | }); 18 | 19 | module.exports = app; 20 | -------------------------------------------------------------------------------- /tests/servers/express/custom-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | var express = require('express'); 5 | var ip = require('docker-ip'); 6 | var redis = require('redis'); 7 | var client = redis.createClient(6060, ip()); 8 | 9 | var app = express(); 10 | 11 | miniprofiler.configure({ 12 | popupRenderPosition: 'right', 13 | storage: new miniprofiler.storage.RedisStorage(client), 14 | ignoredPaths: [ '/hidden' ] 15 | }); 16 | 17 | app.use(miniprofiler.express()); 18 | 19 | app.get('/', (req, res) => { 20 | res.send(res.locals.miniprofiler.include()); 21 | }); 22 | 23 | app.get('/hidden', (req, res) => { 24 | res.send('This won\'t be profiled.'); 25 | }); 26 | 27 | module.exports = app; 28 | -------------------------------------------------------------------------------- /tests/servers/express/default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | var express = require('express'); 5 | 6 | var app = express(); 7 | 8 | app.use(miniprofiler.express()); 9 | app.use(miniprofiler.express.for(require('../dummy-provider.js')())); 10 | 11 | app.get('/', (req, res) => { 12 | res.send(res.locals.miniprofiler.include()); 13 | }); 14 | 15 | app.get('/step', (req, res) => { 16 | req.miniprofiler.step('Step', () => { 17 | res.send(res.locals.miniprofiler.include()); 18 | }); 19 | }); 20 | 21 | app.get('/step-two', (req, res) => { 22 | req.miniprofiler.step('Step 1', () => { 23 | req.miniprofiler.step('Step 2', () => { 24 | res.send(res.locals.miniprofiler.include()); 25 | }); 26 | }); 27 | }); 28 | 29 | app.get('/step-parallel', (req, res) => { 30 | var count = 0; 31 | var finish = () => { 32 | if (++count == 2) 33 | res.send(res.locals.miniprofiler.include()); 34 | }; 35 | 36 | req.miniprofiler.step('Step 1', finish); 37 | req.miniprofiler.step('Step 2', finish); 38 | }); 39 | 40 | app.get('/js-sleep', function(req, res) { 41 | req.miniprofiler.timeQuery('custom', 'Sleeping...', setTimeout, function() { 42 | res.send(res.locals.miniprofiler.include()); 43 | }, 50); 44 | }); 45 | 46 | app.get('/js-sleep-start-stop', function(req, res) { 47 | var timing = req.miniprofiler.startTimeQuery('custom', 'Sleeping...'); 48 | setTimeout(function() { 49 | req.miniprofiler.stopTimeQuery(timing); 50 | res.send(res.locals.miniprofiler.include()); 51 | }, 50); 52 | }); 53 | 54 | module.exports = app; 55 | -------------------------------------------------------------------------------- /tests/servers/express/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var server; 4 | 5 | module.exports = { 6 | start: function(name, port, done) { 7 | var app = require(`./${name}.js`); 8 | server = app.listen(port, done); 9 | }, 10 | stop: function(done) { 11 | server.close(done); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /tests/servers/express/render.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | var express = require('express'); 5 | 6 | var app = express(); 7 | 8 | app.use(miniprofiler.express()); 9 | app.set('view engine', 'pug'); 10 | app.set('views', './tests/servers/views'); 11 | 12 | app.get('/', (req, res) => { 13 | res.render('index', { title: 'Hey', message: 'Hello there!' }); 14 | }); 15 | 16 | app.get('/inside-step', (req, res) => { 17 | req.miniprofiler.step('Step 1', (unstep) => { 18 | req.miniprofiler.timeQuery('custom', 'Sleeping...', setTimeout, function() { 19 | res.render('index', { title: 'Hey', message: 'Hello there!' }); 20 | unstep(); 21 | }, 50); 22 | }); 23 | }); 24 | 25 | module.exports = app; 26 | -------------------------------------------------------------------------------- /tests/servers/express/unauthorized.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | var express = require('express'); 5 | 6 | var app = express(); 7 | 8 | var options = { 9 | authorize: (req) => { 10 | return false; 11 | } 12 | }; 13 | 14 | app.use(miniprofiler.express(options)); 15 | 16 | app.get('/', (req, res) => { 17 | res.send(res.locals.miniprofiler.include()); 18 | }); 19 | 20 | module.exports = app; 21 | -------------------------------------------------------------------------------- /tests/servers/express/unprofiled.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | var express = require('express'); 5 | 6 | var app = express(); 7 | 8 | var options = { 9 | enable: (req) => { 10 | return false; 11 | } 12 | }; 13 | 14 | app.use(miniprofiler.express(options)); 15 | 16 | app.get('/', (req, res) => { 17 | req.miniprofiler.timeQuery('custom', 'Sleeping...', setTimeout, function() { 18 | req.miniprofiler.step('Step 1', () => { 19 | res.send(res.locals.miniprofiler.include()); 20 | }); 21 | }, 50); 22 | }); 23 | 24 | module.exports = app; 25 | -------------------------------------------------------------------------------- /tests/servers/hapi/async.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | var dummyModule = require('../dummy-module'); 5 | const Hapi = require('hapi'); 6 | 7 | const server = new Hapi.Server(); 8 | server.connection({ port: 8083 }); 9 | 10 | server.register(miniprofiler.hapi(), (err) => { 11 | if (err) throw err; 12 | }); 13 | 14 | server.register(miniprofiler.hapi.for(require('../async-provider.js')(dummyModule)), (err) => { 15 | if (err) throw err; 16 | }); 17 | 18 | server.route({ 19 | method: 'GET', 20 | path:'/', 21 | handler: function(request, reply) { 22 | dummyModule.asyncFn().then(() => { 23 | Promise.resolve(request.query.once ? undefined : dummyModule.asyncFn()) 24 | .then(() => reply(request.app.miniprofiler.include())); 25 | }); 26 | } 27 | }); 28 | 29 | module.exports = server; 30 | -------------------------------------------------------------------------------- /tests/servers/hapi/custom-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | 5 | const Hapi = require('hapi'); 6 | 7 | var ip = require('docker-ip'); 8 | var redis = require('redis'); 9 | var client = redis.createClient(6060, ip()); 10 | 11 | const server = new Hapi.Server(); 12 | server.connection({ port: 8083 }); 13 | 14 | miniprofiler.configure({ 15 | popupRenderPosition: 'right', 16 | storage: new miniprofiler.storage.RedisStorage(client), 17 | ignoredPaths: [ '/hidden' ] 18 | }); 19 | 20 | server.register(miniprofiler.hapi(), (err) => { 21 | if (err) throw (err); 22 | }); 23 | 24 | server.route({ 25 | method: 'GET', 26 | path:'/', 27 | handler: function(request, reply) { 28 | return reply(request.app.miniprofiler.include()); 29 | } 30 | }); 31 | 32 | server.route({ 33 | method: 'GET', 34 | path:'/hidden', 35 | handler: function(request, reply) { 36 | return reply('This won\'t be profiled.'); 37 | } 38 | }); 39 | 40 | module.exports = server; 41 | -------------------------------------------------------------------------------- /tests/servers/hapi/default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | const Hapi = require('hapi'); 5 | 6 | const server = new Hapi.Server(); 7 | server.connection({ port: 8083 }); 8 | 9 | server.register(miniprofiler.hapi(), (err) => { 10 | if (err) throw err; 11 | }); 12 | 13 | server.register(miniprofiler.hapi.for(require('../dummy-provider.js')()), (err) => { 14 | if (err) throw err; 15 | }); 16 | 17 | server.route({ 18 | method: 'GET', 19 | path:'/', 20 | handler: function(request, reply) { 21 | return reply(request.app.miniprofiler.include()); 22 | } 23 | }); 24 | 25 | server.route({ 26 | method: 'GET', 27 | path:'/step', 28 | handler: function(request, reply) { 29 | request.raw.req.miniprofiler.step('Step', () => { 30 | return reply(request.app.miniprofiler.include()); 31 | }); 32 | } 33 | }); 34 | 35 | server.route({ 36 | method: 'GET', 37 | path:'/step-two', 38 | handler: function(request, reply) { 39 | request.raw.req.miniprofiler.step('Step 1', () => { 40 | request.raw.req.miniprofiler.step('Step 2', () => { 41 | return reply(request.app.miniprofiler.include()); 42 | }); 43 | }); 44 | } 45 | }); 46 | 47 | server.route({ 48 | method: 'GET', 49 | path:'/step-parallel', 50 | handler: function(request, reply) { 51 | var count = 0; 52 | var finish = () => { 53 | if (++count == 2) 54 | return reply(request.app.miniprofiler.include()); 55 | }; 56 | 57 | request.raw.req.miniprofiler.step('Step 1', finish); 58 | request.raw.req.miniprofiler.step('Step 2', finish); 59 | } 60 | }); 61 | 62 | server.route({ 63 | method: 'GET', 64 | path:'/js-sleep', 65 | handler: function(request, reply) { 66 | request.raw.req.miniprofiler.timeQuery('custom', 'Sleeping...', setTimeout, () => { 67 | return reply(request.app.miniprofiler.include()); 68 | }, 50); 69 | } 70 | }); 71 | 72 | server.route({ 73 | method: 'GET', 74 | path:'/js-sleep-start-stop', 75 | handler: function(request, reply) { 76 | var timing = request.raw.req.miniprofiler.startTimeQuery('custom', 'Sleeping...'); 77 | setTimeout(function() { 78 | request.raw.req.miniprofiler.stopTimeQuery(timing); 79 | return reply(request.app.miniprofiler.include()); 80 | }, 50); 81 | } 82 | }); 83 | 84 | module.exports = server; 85 | -------------------------------------------------------------------------------- /tests/servers/hapi/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var server; 4 | 5 | module.exports = { 6 | start: function(name, port, done) { 7 | server = require(`./${name}.js`); 8 | server.start(done); 9 | }, 10 | stop: function(done) { 11 | server.stop({ }, done); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /tests/servers/hapi/render.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | const Hapi = require('hapi'); 5 | const vision = require('vision'); 6 | 7 | const server = new Hapi.Server(); 8 | server.connection({ port: 8083 }); 9 | 10 | server.register(miniprofiler.hapi(), (err) => { 11 | if (err) throw (err); 12 | }); 13 | 14 | server.register(vision, (err) => { 15 | if (err) throw (err); 16 | 17 | server.views({ 18 | engines: { pug: require('pug') }, 19 | path: './tests/servers/views' 20 | }); 21 | 22 | miniprofiler.hapi().vision(server); 23 | }); 24 | 25 | server.route({ 26 | method: 'GET', 27 | path:'/', 28 | handler: function(request, reply) { 29 | reply.view('index', { title: 'Hey', message: 'Hello there!' }); 30 | } 31 | }); 32 | 33 | server.route({ 34 | method: 'GET', 35 | path:'/inside-step', 36 | handler: function(request, reply) { 37 | request.app.miniprofiler.step('Step 1', (unstep) => { 38 | request.app.miniprofiler.timeQuery('custom', 'Sleeping...', setTimeout, function() { 39 | reply.view('index', { title: 'Hey', message: 'Hello there!' }); 40 | unstep(); 41 | }, 50); 42 | }); 43 | } 44 | }); 45 | 46 | module.exports = server; 47 | -------------------------------------------------------------------------------- /tests/servers/hapi/unauthorized.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | const Hapi = require('hapi'); 5 | 6 | const server = new Hapi.Server(); 7 | server.connection({ port: 8083 }); 8 | 9 | var options = { 10 | authorize: (req) => { 11 | return false; 12 | } 13 | }; 14 | 15 | server.register(miniprofiler.hapi(options), (err) => { 16 | if (err) throw (err); 17 | }); 18 | 19 | server.route({ 20 | method: 'GET', 21 | path:'/', 22 | handler: function(request, reply) { 23 | return reply(request.app.miniprofiler.include()); 24 | } 25 | }); 26 | 27 | module.exports = server; 28 | -------------------------------------------------------------------------------- /tests/servers/hapi/unprofiled.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | const Hapi = require('hapi'); 5 | 6 | const server = new Hapi.Server(); 7 | server.connection({ port: 8083 }); 8 | 9 | var options = { 10 | enable: (req) => { 11 | return false; 12 | } 13 | }; 14 | 15 | server.register(miniprofiler.hapi(options), (err) => { 16 | if (err) throw (err); 17 | }); 18 | 19 | server.route({ 20 | method: 'GET', 21 | path:'/', 22 | handler: function(request, reply) { 23 | request.raw.req.miniprofiler.timeQuery('custom', 'Sleeping...', setTimeout, () => { 24 | request.raw.req.miniprofiler.step('Step 1', () => { 25 | return reply(request.app.miniprofiler.include()); 26 | }); 27 | }, 50); 28 | } 29 | }); 30 | 31 | module.exports = server; 32 | -------------------------------------------------------------------------------- /tests/servers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('request'); 4 | var frameworks = { 5 | 'koa': { 'port': 8081 }, 6 | 'express': { 'port': 8082 }, 7 | 'hapi': { 'port': 8083 } 8 | }; 9 | 10 | var all = [ ]; 11 | 12 | for (let fw in frameworks) { 13 | var server = require(`./${fw}`); 14 | server.framework = fw; 15 | frameworks[fw].server = server; 16 | 17 | server.setUp = function(name, done) { 18 | Object.keys(require.cache).forEach((key) => { delete require.cache[key]; }); 19 | frameworks[fw].server.start(name, frameworks[fw].port, done); 20 | }; 21 | 22 | server.tearDown = function(done) { 23 | frameworks[fw].server.stop(done); 24 | }; 25 | 26 | server.get = (path, cb) => { 27 | request.get(`http://localhost:${frameworks[fw].port}${path}`, (err, response, body) => { 28 | cb(err, response, body); 29 | }); 30 | }; 31 | 32 | server.post = (path, params, cb) => { 33 | request.post({url: `http://localhost:${frameworks[fw].port}${path}`, form: params }, (err, response, body) => { 34 | cb(err, response, body); 35 | }); 36 | }; 37 | 38 | all.push(server); 39 | } 40 | 41 | module.exports = all; 42 | -------------------------------------------------------------------------------- /tests/servers/koa/async.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | var dummyModule = require('../dummy-module'); 5 | var koa = require('koa'); 6 | var route = require('koa-route'); 7 | var app = koa(); 8 | 9 | app.use(miniprofiler.koa()); 10 | app.use(miniprofiler.koa.for(require('../async-provider.js')(dummyModule))); 11 | 12 | app.use(route.get('/', function *(){ 13 | yield dummyModule.asyncFn().then(() => { 14 | return Promise.resolve(this.query.once ? undefined : dummyModule.asyncFn()) 15 | .then(() => { this.body = this.state.miniprofiler.include(); }); 16 | }); 17 | })); 18 | 19 | module.exports = app; 20 | -------------------------------------------------------------------------------- /tests/servers/koa/custom-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | var koa = require('koa'); 5 | var route = require('koa-route'); 6 | var app = koa(); 7 | var ip = require('docker-ip'); 8 | var redis = require('redis'); 9 | 10 | var client = redis.createClient(6060, ip()); 11 | 12 | miniprofiler.configure({ 13 | popupRenderPosition: 'right', 14 | storage: new miniprofiler.storage.RedisStorage(client), 15 | ignoredPaths: [ '/hidden' ] 16 | }); 17 | 18 | app.use(miniprofiler.koa()); 19 | 20 | app.use(route.get('/', function *(){ 21 | this.body = this.state.miniprofiler.include(); 22 | })); 23 | 24 | app.use(route.get('/hidden', function *(){ 25 | this.body = 'This won\'t be profiled.'; 26 | })); 27 | 28 | module.exports = app; 29 | -------------------------------------------------------------------------------- /tests/servers/koa/default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | var koa = require('koa'); 5 | var route = require('koa-route'); 6 | var app = koa(); 7 | 8 | app.use(miniprofiler.koa()); 9 | app.use(miniprofiler.koa.for(require('../dummy-provider.js')())); 10 | 11 | app.use(route.get('/', function *(){ 12 | this.body = this.state.miniprofiler.include(); 13 | })); 14 | 15 | app.use(route.get('/step', function *(){ 16 | this.req.miniprofiler.step('Step', () => { 17 | this.body = this.state.miniprofiler.include(); 18 | }); 19 | })); 20 | 21 | app.use(route.get('/step-two', function *(){ 22 | this.req.miniprofiler.step('Step 1', () => { 23 | this.req.miniprofiler.step('Step 2', () => { 24 | this.body = this.state.miniprofiler.include(); 25 | }); 26 | }); 27 | })); 28 | 29 | app.use(route.get('/step-parallel', function *(){ 30 | var count = 0; 31 | var finish = () => { 32 | if (++count == 2) 33 | this.body = this.state.miniprofiler.include(); 34 | }; 35 | 36 | this.req.miniprofiler.step('Step 1', finish); 37 | this.req.miniprofiler.step('Step 2', finish); 38 | })); 39 | 40 | app.use(route.get('/js-sleep', function *(){ 41 | yield new Promise((resolve, reject) => { 42 | this.req.miniprofiler.timeQuery('custom', 'Sleeping...', setTimeout, () => { 43 | this.body = this.state.miniprofiler.include(); 44 | resolve(); 45 | }, 50); 46 | }); 47 | })); 48 | 49 | app.use(route.get('/js-sleep-start-stop', function *(){ 50 | yield new Promise((resolve, reject) => { 51 | var timing = this.req.miniprofiler.startTimeQuery('custom', 'Sleeping...'); 52 | setTimeout(() => { 53 | this.req.miniprofiler.stopTimeQuery(timing); 54 | this.body = this.state.miniprofiler.include(); 55 | resolve(); 56 | }, 50); 57 | }); 58 | })); 59 | 60 | module.exports = app; 61 | -------------------------------------------------------------------------------- /tests/servers/koa/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var server; 4 | 5 | module.exports = { 6 | start: function(name, port, done) { 7 | var app = require(`./${name}.js`); 8 | server = app.listen(port, done); 9 | }, 10 | stop: function(done) { 11 | server.close(done); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /tests/servers/koa/render.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | var koa = require('koa'); 5 | var route = require('koa-route'); 6 | var views = require('koa-views'); 7 | var app = koa(); 8 | 9 | app.use(views('./tests/servers/views', { extension: 'pug' })); 10 | 11 | app.use(miniprofiler.koa()); 12 | 13 | app.use(route.get('/', function *(){ 14 | yield this.render('index', { title: 'Hey', message: 'Hello there!' }); 15 | })); 16 | 17 | app.use(route.get('/inside-step', function *(){ 18 | yield new Promise((resolve, reject) => { 19 | this.req.miniprofiler.step('Step 1', (unstep) => { 20 | this.req.miniprofiler.timeQuery('custom', 'Sleeping...', setTimeout, () => { 21 | this.render('index', { title: 'Hey', message: 'Hello there!' }).then(() => { 22 | unstep(); 23 | resolve(); 24 | }); 25 | }, 50); 26 | }); 27 | }); 28 | })); 29 | 30 | module.exports = app; 31 | -------------------------------------------------------------------------------- /tests/servers/koa/unauthorized.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | var koa = require('koa'); 5 | var route = require('koa-route'); 6 | 7 | var app = koa(); 8 | 9 | var options = { 10 | authorize: (req) => { 11 | return false; 12 | } 13 | }; 14 | 15 | app.use(miniprofiler.koa(options)); 16 | 17 | app.use(route.get('/', function *(){ 18 | this.body = this.state.miniprofiler.include(); 19 | })); 20 | 21 | module.exports = app; 22 | -------------------------------------------------------------------------------- /tests/servers/koa/unprofiled.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var miniprofiler = require('../../../lib/miniprofiler.js'); 4 | var koa = require('koa'); 5 | var route = require('koa-route'); 6 | 7 | var app = koa(); 8 | 9 | var options = { 10 | enable: (req) => { 11 | return false; 12 | } 13 | }; 14 | 15 | app.use(miniprofiler.koa(options)); 16 | 17 | app.use(route.get('/', function *(){ 18 | yield new Promise((resolve, reject) => { 19 | this.req.miniprofiler.timeQuery('custom', 'Sleeping...', setTimeout, () => { 20 | this.req.miniprofiler.step('Step 1', () => { 21 | this.body = this.state.miniprofiler.include(); 22 | resolve(); 23 | }); 24 | }, 50); 25 | }); 26 | })); 27 | 28 | module.exports = app; 29 | -------------------------------------------------------------------------------- /tests/servers/views/index.pug: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title= title 4 | body 5 | h1= message -------------------------------------------------------------------------------- /tests/share-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | 5 | module.exports = function(server) { 6 | describe('Share Tests', function() { 7 | before(server.setUp.bind(null, 'default')); 8 | after(server.tearDown); 9 | 10 | var expectOkResponse = (done) => (err, response, body) => { 11 | expect(response.statusCode).to.be.equal(200); 12 | expect(response.headers['content-type']).to.be.equal('text/html; charset=utf-8'); 13 | done(); 14 | }; 15 | 16 | var expectNotFoundResponse = (done) => (err, response, body) => { 17 | expect(response.statusCode).to.be.equal(404); 18 | expect(response.headers['content-type']).to.be.equal('text/plain; charset=utf-8'); 19 | done(); 20 | }; 21 | 22 | it('[GET] Valid profiled id should render share page', function(done) { 23 | server.get('/', (err, response) => { 24 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 25 | 26 | server.get(`/mini-profiler-resources/results?id=${ids[0]}`, expectOkResponse(done)); 27 | 28 | }); 29 | }); 30 | 31 | it('[POST] Valid profiled id should render share page', function(done) { 32 | server.get('/', (err, response) => { 33 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 34 | 35 | server.post('/mini-profiler-resources/results', { id: ids[0] }, expectOkResponse(done)); 36 | 37 | }); 38 | }); 39 | 40 | it('[GET] Invalid profiled id should render 404', function(done) { 41 | server.get('/mini-profiler-resources/results?id=123', expectNotFoundResponse(done)); 42 | }); 43 | 44 | it('[POST] Invalid profiled id should render 404', function(done) { 45 | server.post('/mini-profiler-resources/results', { id: 123 }, expectNotFoundResponse(done)); 46 | }); 47 | 48 | }); 49 | 50 | }; 51 | -------------------------------------------------------------------------------- /tests/step-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | 5 | module.exports = function(server) { 6 | describe('Step Tests', function() { 7 | before(server.setUp.bind(null, 'default')); 8 | after(server.tearDown); 9 | 10 | it('Index route should not profile any step', function(done) { 11 | server.get('/', (err, response) => { 12 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 13 | expect(ids).to.have.lengthOf(1); 14 | 15 | server.post('/mini-profiler-resources/results', { id: ids[0], popup: 1 }, (err, response, body) => { 16 | var result = JSON.parse(body); 17 | expect(result.Id).to.equal(ids[0]); 18 | expect(result.Name).to.equal('/'); 19 | expect(result.Root.Children).to.be.empty; 20 | 21 | done(); 22 | }); 23 | }); 24 | }); 25 | 26 | it('/step route should profile one step', function(done) { 27 | server.get('/step', (err, response) => { 28 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 29 | expect(ids).to.have.lengthOf(1); 30 | 31 | server.post('/mini-profiler-resources/results', { id: ids[0], popup: 1 }, (err, response, body) => { 32 | var result = JSON.parse(body); 33 | expect(result.Id).to.equal(ids[0]); 34 | expect(result.Name).to.equal('/step'); 35 | expect(result.Root.Children).to.have.lengthOf(1); 36 | 37 | expect(result.Root.Children[0].Name).to.equal('Step'); 38 | expect(result.Root.Children[0].Children).to.be.empty; 39 | 40 | done(); 41 | }); 42 | }); 43 | }); 44 | 45 | it('step-two route should profile two nested step', function(done) { 46 | server.get('/step-two', (err, response) => { 47 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 48 | expect(ids).to.have.lengthOf(1); 49 | 50 | server.post('/mini-profiler-resources/results', { id: ids[0], popup: 1 }, (err, response, body) => { 51 | var result = JSON.parse(body); 52 | expect(result.Id).to.equal(ids[0]); 53 | expect(result.Name).to.equal('/step-two'); 54 | expect(result.Root.Children).to.have.lengthOf(1); 55 | 56 | expect(result.Root.Children[0].Name).to.equal('Step 1'); 57 | expect(result.Root.Children[0].Children).to.have.lengthOf(1); 58 | 59 | expect(result.Root.Children[0].Children[0].Name).to.equal('Step 2'); 60 | expect(result.Root.Children[0].Children[0].Children).to.be.empty; 61 | done(); 62 | }); 63 | }); 64 | }); 65 | 66 | it('/step-parallel route should profile two nested step', function(done) { 67 | server.get('/step-parallel', (err, response) => { 68 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 69 | expect(ids).to.have.lengthOf(1); 70 | 71 | server.post('/mini-profiler-resources/results', { id: ids[0], popup: 1 }, (err, response, body) => { 72 | var result = JSON.parse(body); 73 | expect(result.Id).to.equal(ids[0]); 74 | expect(result.Name).to.equal('/step-parallel'); 75 | expect(result.Root.Children).to.have.lengthOf(2); 76 | 77 | expect(result.Root.Children[0].Name).to.equal('Step 1'); 78 | expect(result.Root.Children[0].Children).to.be.empty; 79 | 80 | expect(result.Root.Children[1].Name).to.equal('Step 2'); 81 | expect(result.Root.Children[1].Children).to.be.empty; 82 | done(); 83 | }); 84 | }); 85 | }); 86 | }); 87 | }; 88 | -------------------------------------------------------------------------------- /tests/timequery-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | 5 | module.exports = function(server) { 6 | describe('Time Query Tests', function() { 7 | before(server.setUp.bind(null, 'default')); 8 | after(server.tearDown); 9 | 10 | for(let url of ['/js-sleep', '/js-sleep-start-stop']) { 11 | it(`Custom timed query should be profiled for url '${url}'`, function(done) { 12 | server.get(url, (err, response) => { 13 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 14 | expect(ids).to.have.lengthOf(1); 15 | 16 | server.post('/mini-profiler-resources/results/', { id: ids[0], popup: 1 }, (err, response, body) => { 17 | var result = JSON.parse(body); 18 | 19 | expect(result.Id).to.equal(ids[0]); 20 | expect(result.Name).to.equal(url); 21 | expect(result.DurationMilliseconds).to.be.above(40); 22 | expect(result.Root.Children).to.be.empty; 23 | expect(result.Root.CustomTimings).to.have.property('custom'); 24 | expect(result.Root.CustomTimings.custom).to.have.lengthOf(1); 25 | 26 | expect(result.Root.CustomTimings.custom[0].ExecuteType).to.be.equal('custom'); 27 | expect(result.Root.CustomTimings.custom[0].CommandString).to.be.equal('Sleeping...'); 28 | expect(result.Root.CustomTimings.custom[0].DurationMilliseconds).to.be.below(result.DurationMilliseconds); 29 | done(); 30 | }); 31 | }); 32 | 33 | }); 34 | } 35 | 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /tests/unauthorized-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | 5 | module.exports = function(server) { 6 | describe('Unauthorized Tests', function() { 7 | before(server.setUp.bind(null, 'unauthorized')); 8 | after(server.tearDown); 9 | 10 | it('should return profile ID', function(done) { 11 | server.get('/', (err, response, body) => { 12 | expect(response.headers).to.include.keys('x-miniprofiler-ids'); 13 | expect(body).to.be.equal(''); 14 | done(); 15 | }); 16 | }); 17 | 18 | it('should not allow user to get timing information from ID', function(done) { 19 | server.get('/', (err, response) => { 20 | var ids = JSON.parse(response.headers['x-miniprofiler-ids']); 21 | 22 | server.post('/mini-profiler-resources/results', { id: ids[0], popup: 1 }, (err, response, body) => { 23 | expect(response.statusCode).to.be.equal(401); 24 | expect(body).to.be.equal(''); 25 | done(); 26 | }); 27 | }); 28 | }); 29 | 30 | }); 31 | 32 | }; 33 | -------------------------------------------------------------------------------- /tests/unprofiled-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | 5 | module.exports = function(server) { 6 | describe('Unprofiled Tests', function() { 7 | before(server.setUp.bind(null, 'unprofiled')); 8 | after(server.tearDown); 9 | 10 | it('should not return profile ID', function(done) { 11 | server.get('/', (err, response) => { 12 | expect(response.headers).to.not.include.keys('x-miniprofiler-ids'); 13 | done(); 14 | }); 15 | }); 16 | 17 | it('should not include asset', function(done) { 18 | server.get('/', (err, response, body) => { 19 | expect(body).to.be.equal(''); 20 | done(); 21 | }); 22 | }); 23 | 24 | var paths = [ 25 | '/', 26 | '/includes.css', 27 | '/results', 28 | '/results?id=2' 29 | ]; 30 | 31 | paths.forEach((path) => { 32 | it(`should not respond for '${path}'`, function(done) { 33 | server.get(`/mini-profiler-resources${path}`, (err, response, body) => { 34 | expect(response.statusCode).to.be.equal(404); 35 | expect(response.headers['content-type']).to.be.equal('text/plain; charset=utf-8'); 36 | expect(body).to.be.equal('MiniProfiler is disabled'); 37 | done(); 38 | }); 39 | }); 40 | }); 41 | }); 42 | 43 | }; 44 | --------------------------------------------------------------------------------