@contrast/route-coverage 1.20.7 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,99 +12,116 @@
12
12
  * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
13
  * way not consistent with the End User License Agreement.
14
14
  */
15
- // @ts-check
16
15
  'use strict';
17
16
 
18
- const { createSignature } = require('./../utils/route-info');
19
- const { StringPrototypeToLowerCase } = require('@contrast/common');
17
+ const { StringPrototypeToLowerCase, isString } = require('@contrast/common');
18
+ const { getFastifyMethods } = require('./../utils/methods');
19
+ const { patchType } = require('./../utils/route-info');
20
20
 
21
- /** @typedef {Parameters<import('fastify-3.0.0').onRouteHookHandler>[0]} RouteOptions */
22
-
23
- /**
24
- * @param {import('..').Core & {
25
- * routeCoverage: import('..').RouteCoverage & {
26
- * fastify?: import('@contrast/common').Installable
27
- * }
28
- * }} core
29
- */
21
+ // Spec: https://contrast.atlassian.net/wiki/spaces/NOD/pages/3454861621/Node.js+Agent+Route+Signatures#Fastify
30
22
  module.exports = function init(core) {
23
+ /*
24
+ Fastify has two ways of defining routes:
25
+ 1. fastify.route({ method: '<method>', url: '<url>', handler: <fn> })
26
+ 2. fastify.<method>('<url>', <fn>)
27
+ (See NODE-3483 for more detail)
28
+ We need a way to keep track of which routes were fully declared using .route
29
+ So, we instrument the route method below and add identifying info to this array
30
+ */
31
+ const fullyDeclaredRoutes = [];
31
32
  const { patcher, depHooks, routeCoverage } = core;
32
33
 
33
- /** @param {RouteOptions} routeOptions */
34
- function registerRouteHandler(routeOptions) {
35
- if (!routeOptions || !routeOptions.method || !routeOptions.url) return;
36
-
37
- const { url, method } = routeOptions;
38
- if (Array.isArray(method)) {
39
- method.forEach((method) => {
40
- emitRouteCoverage(url, method);
41
- });
42
- } else {
43
- emitRouteCoverage(url, method);
34
+ const isFullyDeclared = (method, url) => isString(method) && fullyDeclaredRoutes.includes(method + url);
35
+ function fullyDeclare(method, url) {
36
+ if (isString(method)) {
37
+ fullyDeclaredRoutes.push(method + url);
38
+ if (method === 'GET') fullyDeclaredRoutes.push(`HEAD${url}`);
44
39
  }
45
-
46
- // TODO
47
- observationListener(routeOptions, url);
48
- }
49
-
50
- /**
51
- * @param {RouteOptions} routeOptions
52
- * @param {string} url
53
- */
54
- function observationListener(routeOptions, url) {
55
- patcher.patch(routeOptions, 'handler', {
56
- name: 'fastify.routeOptions.handler',
57
- patchType: 'route-coverage',
58
- pre(data) {
59
- const [request] = data.args;
60
- const { method } = request.raw;
61
- const [parsedUrl] = request.url.split(/\?/);
62
- emitObservation(parsedUrl, url, method);
63
- },
64
- });
65
40
  }
66
41
 
67
- /**
68
- * @param {string} url
69
- * @param {string} method
70
- */
71
- function emitRouteCoverage(url, method) {
42
+ function createRouteInfo(method, url, routePath) {
43
+ const fullyDeclared = isFullyDeclared(method, routePath);
72
44
  method = StringPrototypeToLowerCase.call(method);
73
- const event = { signature: createSignature(url, method), url, method, normalizedUrl: url, framework: 'fastify' };
74
- routeCoverage.discover(event);
75
- }
76
45
 
77
- /**
78
- * @param {string} url
79
- * @param {string} normalizedUrl
80
- * @param {string=} method
81
- */
82
- function emitObservation(url, normalizedUrl, method) {
83
- method = method && StringPrototypeToLowerCase.call(method);
84
- routeCoverage.observe({ method, url, normalizedUrl });
46
+ const signature = fullyDeclared
47
+ ? `fastify.route({ method: '${method}', url: '${url}', handler: [Function] })`
48
+ : `fastify.${method}('${url}', [Function])`;
49
+
50
+ const routeInfo = {
51
+ signature,
52
+ url,
53
+ method,
54
+ normalizedUrl: url,
55
+ framework: 'fastify'
56
+ };
57
+ return routeInfo;
85
58
  }
86
59
 
87
60
  return core.routeCoverage.fastify = {
88
61
  install() {
89
- depHooks.resolve(
90
- { name: 'fastify', version: '>=3.0.0' },
91
- /** @param {import("fastify-3.0.0").fastify} fastify */
92
- (fastify) =>
93
- patcher.patch(fastify, {
94
- name: 'fastify.build',
95
- patchType: 'route-coverage',
96
- post({ result: server }) {
97
- server.addHook('onRoute', (routeOptions) => {
98
- registerRouteHandler(routeOptions);
99
- });
62
+ // The routePath property used below was introduced in 3.2.0
63
+ return depHooks.resolve({ name: 'fastify', version: '>=3.2.0' }, (fastify) => patcher.patch(fastify, {
64
+ name: 'fastify',
65
+ patchType,
66
+ post({ result: server }) {
67
+ patcher.patch(server, 'route', {
68
+ name: 'server.route',
69
+ patchType,
70
+ pre({ args }) {
71
+ const [{ method, url }] = args;
72
+ if (!isString(url)) return;
73
+
74
+ if (Array.isArray(method)) {
75
+ method.forEach((verb) => {
76
+ fullyDeclare(verb, url);
77
+ });
78
+ } else {
79
+ fullyDeclare(method, url);
80
+ }
81
+ }
82
+ });
83
+ server.addHook('onRoute', (routeOptions) => {
84
+ if (!routeOptions) return;
85
+
86
+ const { method, url, routePath } = routeOptions;
87
+ if (!method || !url || !routePath || !isString(routePath)) return;
88
+
89
+ let routeInfo;
90
+ const FASTIFY_METHODS = getFastifyMethods(server.version);
91
+ if (Array.isArray(method)) {
92
+ // If a route was defined using .all this method property will be an
93
+ // array of all methods supported by Fastify
94
+ if (FASTIFY_METHODS.every(m => method.includes(m))) {
95
+ routeInfo = createRouteInfo('all', url, routePath);
96
+ routeCoverage.discover(routeInfo);
97
+ } else {
98
+ method.forEach((verb) => {
99
+ routeInfo = createRouteInfo(verb, url, routePath);
100
+ routeCoverage.discover(routeInfo);
101
+ });
102
+ }
103
+ } else {
104
+ routeInfo = createRouteInfo(method, url, routePath);
105
+ routeCoverage.discover(routeInfo);
106
+ }
100
107
 
101
- server.addHook('onReady', (done) => {
102
- routeCoverage.discoveryFinished();
103
- return done();
104
- });
105
- },
106
- }),
107
- );
108
+ patcher.patch(routeOptions, 'handler', {
109
+ name: 'fastify.routeOptions.handler',
110
+ patchType,
111
+ pre({ args }) {
112
+ const [req] = args;
113
+ const method = StringPrototypeToLowerCase.call(req.raw?.method);
114
+ const [url] = req.url.split(/\?/);
115
+ routeCoverage.observe({ ...routeInfo, url, method });
116
+ },
117
+ });
118
+ });
119
+ server.addHook('onReady', (done) => {
120
+ routeCoverage.discoveryFinished();
121
+ return done();
122
+ });
123
+ }
124
+ }));
108
125
  }
109
126
  };
110
127
  };
@@ -0,0 +1,203 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const sinon = require('sinon');
5
+ const scopes = require('@contrast/scopes');
6
+ const patcher = require('@contrast/patcher');
7
+ const mocks = require('@contrast/test/mocks');
8
+ const { getFastifyMethods } = require('../utils/methods');
9
+
10
+ describe('route-coverage fastify', function () {
11
+ let core, serverMock, framework;
12
+
13
+ beforeEach(function () {
14
+ core = mocks.core();
15
+ core.logger = mocks.logger();
16
+ core.routeCoverage = mocks.routeCoverage();
17
+ core.scopes = scopes(core);
18
+ core.depHooks = mocks.depHooks();
19
+ core.patcher = patcher(core);
20
+
21
+ serverMock = {
22
+ addHook: sinon.stub(),
23
+ route: sinon.stub(),
24
+ version: '4.4.0'
25
+ };
26
+
27
+ let fastify;
28
+ framework = 'fastify';
29
+ core.depHooks.resolve.callsFake((desc, cb) => {
30
+ fastify = cb(() => serverMock);
31
+ });
32
+
33
+ require('./fastify')(core).install();
34
+
35
+ fastify();
36
+ });
37
+
38
+ it('skips instrumenting if route options does not exist', function () {
39
+ serverMock.addHook.withArgs('onRoute').yield();
40
+ expect(core.routeCoverage.discover).not.to.have.been.called;
41
+ });
42
+
43
+ it('skips instrumenting if there is no method provided', function () {
44
+ serverMock.addHook.withArgs('onRoute').yield({});
45
+ expect(core.routeCoverage.discover).not.to.have.been.called;
46
+ });
47
+
48
+ it('skips instrumenting if there is no url provided', function () {
49
+ serverMock.addHook.withArgs('onRoute').yield({ method: 'GET' });
50
+ expect(core.routeCoverage.discover).not.to.have.been.called;
51
+ });
52
+
53
+ it('discovers a route with a single method', function () {
54
+ serverMock.addHook
55
+ .withArgs('onRoute')
56
+ .yield({ method: 'GET', url: '/test/route', routePath: '/test/route' });
57
+
58
+ expect(core.routeCoverage.discover).to.have.been.calledWith({
59
+ signature: "fastify.get('/test/route', [Function])",
60
+ url: '/test/route',
61
+ normalizedUrl: '/test/route',
62
+ method: 'get',
63
+ framework
64
+ });
65
+ expect(core.routeCoverage.discover).to.have.been.called;
66
+ expect(core.routeCoverage.discover).to.have.callCount(1);
67
+ });
68
+
69
+ it('discovers a fully declared (.route) route with a single method', function () {
70
+ serverMock.route({ method: 'GET', url: '/test/route' });
71
+ serverMock.addHook
72
+ .withArgs('onRoute')
73
+ .yield({ method: 'GET', url: '/test/route', routePath: '/test/route' });
74
+
75
+ expect(core.routeCoverage.discover).to.have.been.calledWith({
76
+ signature: "fastify.route({ method: 'get', url: '/test/route', handler: [Function] })",
77
+ url: '/test/route',
78
+ normalizedUrl: '/test/route',
79
+ method: 'get',
80
+ framework
81
+ });
82
+ expect(core.routeCoverage.discover).to.have.been.called;
83
+ expect(core.routeCoverage.discover).to.have.callCount(1);
84
+ });
85
+
86
+ it('discovers a route with multiple methods', function () {
87
+ serverMock.addHook.withArgs('onRoute').yield({
88
+ method: ['GET', 'POST'],
89
+ url: '/test/route',
90
+ routePath: '/test/route'
91
+ });
92
+
93
+ expect(core.routeCoverage.discover).to.have.been.calledWith({
94
+ signature: "fastify.get('/test/route', [Function])",
95
+ url: '/test/route',
96
+ normalizedUrl: '/test/route',
97
+ method: 'get',
98
+ framework
99
+ });
100
+
101
+ expect(core.routeCoverage.discover).to.have.been.calledWith({
102
+ signature: "fastify.post('/test/route', [Function])",
103
+ url: '/test/route',
104
+ normalizedUrl: '/test/route',
105
+ method: 'post',
106
+ framework
107
+ });
108
+
109
+ expect(core.routeCoverage.discover).to.have.callCount(2);
110
+ });
111
+
112
+ it('discovers a route with all methods', function () {
113
+ const FASTIFY_METHODS = getFastifyMethods('4.4.0');
114
+ serverMock.addHook.withArgs('onRoute').yield({
115
+ method: FASTIFY_METHODS,
116
+ url: '/test/route',
117
+ routePath: '/test/route'
118
+ });
119
+
120
+ expect(core.routeCoverage.discover).to.have.been.calledWith({
121
+ signature: "fastify.all('/test/route', [Function])",
122
+ url: '/test/route',
123
+ normalizedUrl: '/test/route',
124
+ method: 'all',
125
+ framework
126
+ });
127
+ });
128
+
129
+ it('discovers a fully declared route (.route) with multiple methods', function () {
130
+ serverMock.route({ method: ['GET', 'POST'], url: '/test/route' });
131
+
132
+ serverMock.addHook.withArgs('onRoute').yield({
133
+ method: ['GET', 'POST'],
134
+ url: '/test/route',
135
+ routePath: '/test/route'
136
+ });
137
+
138
+ expect(core.routeCoverage.discover).to.have.been.calledWith({
139
+ signature: "fastify.route({ method: 'get', url: '/test/route', handler: [Function] })",
140
+ url: '/test/route',
141
+ normalizedUrl: '/test/route',
142
+ method: 'get',
143
+ framework
144
+ });
145
+
146
+ expect(core.routeCoverage.discover).to.have.been.calledWith({
147
+ signature: "fastify.route({ method: 'post', url: '/test/route', handler: [Function] })",
148
+ url: '/test/route',
149
+ normalizedUrl: '/test/route',
150
+ method: 'post',
151
+ framework
152
+ });
153
+
154
+ expect(core.routeCoverage.discover).to.have.callCount(2);
155
+ });
156
+
157
+ it('signals that discovery is finished when the onReady hook is triggered', function () {
158
+ serverMock.addHook.withArgs('onReady').yield(sinon.stub());
159
+
160
+ expect(core.routeCoverage.discoveryFinished).to.have.been.calledOnce;
161
+ });
162
+
163
+ it('observes a route when the route handler is called', function () {
164
+ const routeOptions = {
165
+ method: 'GET',
166
+ url: '/test/route',
167
+ routePath: '/test/route',
168
+ handler: sinon.stub(),
169
+ };
170
+
171
+ serverMock.addHook.withArgs('onRoute').yield(routeOptions);
172
+ routeOptions.handler({ raw: { method: 'GET' }, url: '/test/route' });
173
+
174
+ expect(core.routeCoverage.observe).to.have.been.calledWith({
175
+ signature: "fastify.get('/test/route', [Function])",
176
+ url: '/test/route',
177
+ method: 'get',
178
+ normalizedUrl: '/test/route',
179
+ framework: 'fastify'
180
+ });
181
+ });
182
+
183
+ it('removes query string from url before reporting observation', function () {
184
+ const routeOptions = {
185
+ method: 'GET',
186
+ url: '/test/route',
187
+ routePath: '/test/route',
188
+ handler: sinon.stub(),
189
+ };
190
+
191
+ serverMock.addHook.withArgs('onRoute').yield(routeOptions);
192
+ routeOptions.handler({ raw: { method: 'GET' }, url: '/test/route?input=foo' });
193
+
194
+ expect(core.routeCoverage.observe).to.have.been.calledWith({
195
+ signature: "fastify.get('/test/route', [Function])",
196
+ url: '/test/route',
197
+ method: 'get',
198
+ normalizedUrl: '/test/route',
199
+ framework: 'fastify'
200
+ });
201
+ });
202
+
203
+ });
@@ -15,14 +15,21 @@
15
15
  'use strict';
16
16
 
17
17
  const { StringPrototypeToLowerCase } = require('@contrast/common');
18
- const { createSignature, patchType } = require('./../utils/route-info');
18
+ const { patchType } = require('./../utils/route-info');
19
19
 
20
+ // Spec: https://contrast.atlassian.net/wiki/spaces/NOD/pages/3454861621/Node.js+Agent+Route+Signatures#Hapi
20
21
  module.exports = function init(core) {
21
22
  const { patcher, depHooks, routeCoverage } = core;
22
23
 
23
24
  function emitRouteCoverage(url, method) {
24
25
  method = StringPrototypeToLowerCase.call(method);
25
- const event = { signature: createSignature(url, method), url, method, normalizedUrl: url, framework: 'hapi' };
26
+ const event = {
27
+ signature: `server.route({ method: '${method}', path: '${url}', handler: [Function] })`,
28
+ url,
29
+ method,
30
+ normalizedUrl: url,
31
+ framework: 'hapi'
32
+ };
26
33
  routeCoverage.discover(event);
27
34
  }
28
35
 
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const sinon = require('sinon');
5
+ const scopes = require('@contrast/scopes');
6
+ const patcher = require('@contrast/patcher');
7
+ const mocks = require('@contrast/test/mocks');
8
+
9
+ describe('route-coverage hapi', function () {
10
+ let core, route, _core, hapi, framework;
11
+
12
+ beforeEach(function () {
13
+ core = mocks.core();
14
+ core.logger = mocks.logger();
15
+ core.routeCoverage = mocks.routeCoverage();
16
+ core.scopes = scopes(core);
17
+ core.depHooks = mocks.depHooks();
18
+ core.patcher = patcher(core);
19
+
20
+ route = {
21
+ settings: {
22
+ handler: sinon.stub()
23
+ }
24
+ };
25
+
26
+ _core = {
27
+ router: {
28
+ add() {
29
+ return { route };
30
+ }
31
+ }
32
+ };
33
+
34
+ hapi = {
35
+ server() {
36
+ return { _core };
37
+ },
38
+ Server() {
39
+ return { _core };
40
+ }
41
+ };
42
+ framework = 'hapi';
43
+
44
+ core.depHooks.resolve.withArgs({ name: '@hapi/hapi', version: '>=18 <22' }).yields(hapi);
45
+ require('./hapi')(core).install();
46
+ });
47
+
48
+
49
+ ['server', 'Server'].forEach((server) => {
50
+
51
+ it('does not report a non-existent route', function () {
52
+ const hapiServer = hapi[server]();
53
+ hapiServer._core.router.add();
54
+ expect(core.routeCoverage.discover).not.to.have.been.called;
55
+ });
56
+
57
+ it('does not report a route missing a method', function () {
58
+ const hapiServer = hapi[server]();
59
+ hapiServer._core.router.add({ path: '/foo' });
60
+ expect(core.routeCoverage.discover).not.to.have.been.called;
61
+ });
62
+
63
+ it('does not report a route missing a path', function () {
64
+ const hapiServer = hapi[server]();
65
+ hapiServer._core.router.add({ method: 'GET' });
66
+ expect(core.routeCoverage.discover).not.to.have.been.called;
67
+ });
68
+
69
+ it('discovers a route with a single method', function () {
70
+ const hapiServer = hapi[server]();
71
+ hapiServer._core.router.add({ method: 'GET', path: '/foo' });
72
+
73
+ expect(core.routeCoverage.discover).to.have.been.calledWith({
74
+ signature: "server.route({ method: 'get', path: '/foo', handler: [Function] })",
75
+ url: '/foo',
76
+ normalizedUrl: '/foo',
77
+ method: 'get',
78
+ framework
79
+ });
80
+ });
81
+
82
+ it('discovers a route with multiple methods', function () {
83
+ const hapiServer = hapi[server]();
84
+ hapiServer._core.router.add({ method: ['GET', 'PUT'], path: '/foo' });
85
+
86
+ expect(core.routeCoverage.discover).to.have.been.calledWith({
87
+ signature: "server.route({ method: 'get', path: '/foo', handler: [Function] })",
88
+ url: '/foo',
89
+ normalizedUrl: '/foo',
90
+ method: 'get',
91
+ framework
92
+ });
93
+
94
+ expect(core.routeCoverage.discover).to.have.been.calledWith({
95
+ signature: "server.route({ method: 'put', path: '/foo', handler: [Function] })",
96
+ url: '/foo',
97
+ normalizedUrl: '/foo',
98
+ method: 'put',
99
+ framework
100
+ });
101
+ });
102
+
103
+ it('observes a route when a route handler is exercised', async function () {
104
+ const hapiServer = hapi[server]();
105
+ const { route } = hapiServer._core.router.add({ method: 'get', path: '/foo' });
106
+ route.settings.handler({ method: 'get', path: '/foo', route: { path: '/foo' } });
107
+ expect(core.routeCoverage.observe).to.have.been.calledWith({
108
+ url: '/foo',
109
+ normalizedUrl: '/foo',
110
+ method: 'get'
111
+ });
112
+ });
113
+
114
+ it('observes a route when a route contains parameters', async function () {
115
+ const hapiServer = hapi[server]();
116
+ const { route } = hapiServer._core.router.add({ method: 'get', path: '/foo/{id}' });
117
+ route.settings.handler({ method: 'get', path: '/foo/1', route: { path: '/foo/{id}' } });
118
+ expect(core.routeCoverage.observe).to.have.been.calledWith({
119
+ url: '/foo/1',
120
+ normalizedUrl: '/foo/{id}',
121
+ method: 'get'
122
+ });
123
+ });
124
+ });
125
+ });
@@ -14,56 +14,76 @@
14
14
  */
15
15
  'use strict';
16
16
 
17
- const { StringPrototypeToLowerCase } = require('@contrast/common');
17
+ const { METHODS } = require('./../utils/methods');
18
+ const { StringPrototypeToLowerCase, isString } = require('@contrast/common');
18
19
  const { createSignature, patchType } = require('./../utils/route-info');
19
20
 
21
+ // Spec: https://contrast.atlassian.net/wiki/spaces/NOD/pages/3454861621/Node.js+Agent+Route+Signatures#Koa
20
22
  module.exports = function init(core) {
21
23
  const { patcher, depHooks, routeCoverage } = core;
22
24
 
23
- function emitRouteCoverage(url, method) {
24
- const event = { signature: createSignature(url, method), url, method, normalizedUrl: url, framework: 'koa' };
25
- routeCoverage.discover(event);
26
- }
27
-
28
- const routeObservationPathClosure = ({ path }) =>
29
- async function routeObservationMiddleware(ctx, next) {
30
- const req = ctx.request;
31
-
32
- if (req) {
33
- const { url: reqUrl, method } = req;
34
- const [url] = reqUrl.split(/\?/);
35
- routeCoverage.observe({ url, method: StringPrototypeToLowerCase.call(method || ''), normalizedUrl: path });
36
- }
37
-
38
- await next();
25
+ function createRouteInfo(method, url) {
26
+ method = StringPrototypeToLowerCase.call(method);
27
+ const routeInfo = {
28
+ signature: createSignature(url, method),
29
+ url,
30
+ method,
31
+ normalizedUrl: url,
32
+ framework: 'koa'
39
33
  };
34
+ return routeInfo;
35
+ }
40
36
 
41
37
  return core.routeCoverage.koa = {
42
38
  install() {
43
- ['@koa/router', 'koa-router'].forEach((name) => {
44
- depHooks.resolve({ name }, (_export) => {
45
- if (!_export?.prototype?.register) return;
39
+ depHooks.resolve({ name: 'koa', version: '>=2.3.0' }, (Koa) => {
40
+ // Koa uses its own routing library @koa/router to define routes before
41
+ // mounting them on the app with .use so instrumenting use and traversing
42
+ // the constructed routes is the more technically correct approach than
43
+ // instrumenting the routing library
44
+ patcher.patch(Koa.prototype, 'use', {
45
+ name: 'Koa.prototype.use',
46
+ patchType,
47
+ pre: ({ args }) => {
48
+ if (args?.length === 0) return;
49
+ const [router] = args;
46
50
 
47
- patcher.patch(_export.prototype, 'register', {
48
- name: 'koaRouter.prototype',
49
- patchType,
50
- post: ({ args, result: layer }) => {
51
- const [path, methods] = args;
52
- if (!path || !methods || Array.isArray(path)) {
53
- return;
54
- }
51
+ if (!router?.router) return;
55
52
 
53
+ router.router.stack.forEach((Layer) => {
54
+ const { methods, path } = Layer;
55
+ if (!path || !isString(path)) return;
56
+
57
+ let routeInfo;
56
58
  if (methods.length === 0) {
57
- emitRouteCoverage(path, 'use');
59
+ routeInfo = createRouteInfo('use', path);
60
+ routeCoverage.discover(routeInfo);
61
+ } else if (METHODS.every(m => methods.includes(m))) {
62
+ // If a route was defined using .all this methods property will be an
63
+ // array of all methods supported by Koa
64
+ routeInfo = createRouteInfo('all', path);
65
+ routeCoverage.discover(routeInfo);
58
66
  } else {
59
67
  methods.forEach((method) => {
60
- emitRouteCoverage(path, StringPrototypeToLowerCase.call(method || ''));
68
+ routeInfo = createRouteInfo(method, path);
69
+ routeCoverage.discover(routeInfo);
61
70
  });
62
71
  }
63
72
 
64
- layer.stack.unshift(routeObservationPathClosure({ path }).bind(this));
65
- }
66
- });
73
+ if (!Layer.stack || Layer.stack.length === 0) return;
74
+ async function observationMiddleware(ctx, next) {
75
+ if (!ctx.request) return;
76
+ const { url: reqUrl, method } = ctx.request;
77
+ const [url] = reqUrl.split(/\?/);
78
+ routeCoverage.observe({ ...routeInfo, url, method: StringPrototypeToLowerCase.call(method) });
79
+ await next();
80
+ }
81
+ // If two routes share middleware, the same stack is used
82
+ // To add our observation middleware without adding them to all routes
83
+ // we need to create a shallow copy
84
+ Layer.stack = [observationMiddleware, ...Layer.stack];
85
+ });
86
+ }
67
87
  });
68
88
  });
69
89
  }