diff --git a/.tav.yml b/.tav.yml index c82a74cfd2..c22b45fb4d 100644 --- a/.tav.yml +++ b/.tav.yml @@ -330,8 +330,8 @@ pug: # @hapi/hapi # - Package name: Starting with v17.9.0 and v18.2.0 the name changed from -# 'hapi' to '@hapi/hapi'. Instrumentation for the old 'hapi' is now deprecated -# and untested; it will be dropped in the next major version of the agent. +# 'hapi' to '@hapi/hapi'. Starting in elastic-apm-node@4, only the newer +# '@hapi/hapi' is instrumented. # - Node version compat: # - @hapi/hapi@19: supports node >=v12 (judging from commit 50d8d7d) # - @hapi/hapi@20: appears (from travis template refs) to support node >=v12 @@ -345,29 +345,29 @@ pug: versions: '>=17.0.0 <19.0.0' node: '>=8.12.0 <16.0.0' commands: - - node test/instrumentation/modules/hapi/basic.test.js - - node test/instrumentation/modules/hapi/set-framework-hapihapi.test.js + - node test/instrumentation/modules/hapi/hapi.test.js + - node test/instrumentation/modules/hapi/set-framework.test.js '@hapi/hapi-v19-v20.1.2': name: '@hapi/hapi' versions: '>=19.0.0 <20.1.2' node: '>=12.0.0 <16.0.0' commands: - - node test/instrumentation/modules/hapi/basic.test.js - - node test/instrumentation/modules/hapi/set-framework-hapihapi.test.js + - node test/instrumentation/modules/hapi/hapi.test.js + - node test/instrumentation/modules/hapi/set-framework.test.js '@hapi/hapi-v20.1.2-v21': name: '@hapi/hapi' versions: '>=20.1.2 <21.0.0' node: '>=12.0.0' commands: - - node test/instrumentation/modules/hapi/basic.test.js - - node test/instrumentation/modules/hapi/set-framework-hapihapi.test.js + - node test/instrumentation/modules/hapi/hapi.test.js + - node test/instrumentation/modules/hapi/set-framework.test.js '@hapi/hapi-v21-': name: '@hapi/hapi' versions: '>=21.0.0' node: '>=14.10.0' commands: - - node test/instrumentation/modules/hapi/basic.test.js - - node test/instrumentation/modules/hapi/set-framework-hapihapi.test.js + - node test/instrumentation/modules/hapi/hapi.test.js + - node test/instrumentation/modules/hapi/set-framework.test.js tedious-v1-v11: name: tedious diff --git a/CHANGELOG4.asciidoc b/CHANGELOG4.asciidoc index 407dd498f6..a079ab76e4 100644 --- a/CHANGELOG4.asciidoc +++ b/CHANGELOG4.asciidoc @@ -22,6 +22,9 @@ * The config option <> is now *removed*. ({pull}3539[#3539]) +* Remove instrumentation support for the old 'hapi' package -- the current + '@hapi/hapi' package is still instrumented. ({issues}2691[#2691]) + [float] ===== Features diff --git a/dev-utils/bitrot.js b/dev-utils/bitrot.js index 4a8f738a63..a00c9c738f 100755 --- a/dev-utils/bitrot.js +++ b/dev-utils/bitrot.js @@ -40,7 +40,6 @@ const EXCUSE_FROM_SUPPORTED_TECHNOLOGIES_DOC = { const EXCUSE_FROM_TAV = { '@elastic/elasticsearch-canary': true, got: true, // got@12 is pure ESM so we state support up to got@11 only - hapi: true, // we deprecated 'hapi' (in favour of '@hapi/hapi') jade: true, // we deprecated 'jade' (in favour of 'pug') 'mimic-response': true, // we instrument a single old version to indirectly support an old version of 'got' mongojs: true, // last release was in 2019, we aren't going to add effort to this module now @@ -169,7 +168,6 @@ function loadSupportedDoc() { // The tables in supported-technologies.asciidoc have the module // name in the first column, and version range in the second. There // are two forms of the first cell to parse: - // [ '<>', '>=9.0.0 <19.0.0' ], // [ '<>', '>=17.9.0 <20.0.0' ], // [ '<> via koa-router or @koa/router', '>=5.2.0 <10.0.0' ], // [ '<>', '>=5.2.0' ], diff --git a/docs/agent-api.asciidoc b/docs/agent-api.asciidoc index 411d4e1300..b78b630d65 100644 --- a/docs/agent-api.asciidoc +++ b/docs/agent-api.asciidoc @@ -813,21 +813,6 @@ apm.addPatch('timers', (exports, agent, { version, enabled }) => { // or ... -apm.addPatch(['hapi', '@hapi/hapi'], (exports, agent, { version, enabled }) => { - const setTimeout = exports.setTimeout - exports.setTimeout = (fn, ms) => { - const span = agent.startSpan('set-timeout') - return setTimeout(() => { - span.end() - fn() - }, ms) - } - - return exports -}) - -// or ... - apm.addPatch('timers', './timer-patch') ---- diff --git a/docs/hapi.asciidoc b/docs/hapi.asciidoc index 06b70ce7ec..54153c1881 100644 --- a/docs/hapi.asciidoc +++ b/docs/hapi.asciidoc @@ -26,7 +26,7 @@ npm install elastic-apm-node --save [[hapi-initialization]] ==== Initialization -It's important that the agent is started before you require *any* other modules in your Node.js application - i.e. before `hapi`, `http`, etc. +It's important that the agent is started before you require *any* other modules in your Node.js application - i.e. before `@hapi/hapi`, `http`, etc. This means that you should probably require and start the agent in your application's main file (usually `index.js`, `server.js` or `app.js`). diff --git a/docs/supported-technologies.asciidoc b/docs/supported-technologies.asciidoc index 29224652cc..5f6ff57bcd 100644 --- a/docs/supported-technologies.asciidoc +++ b/docs/supported-technologies.asciidoc @@ -65,7 +65,6 @@ These are the frameworks that we officially support: | <> | ^4.0.0 | | <> | >=1.0.0 | See also https://www.fastify.io/docs/latest/Reference/LTS/[Fastify's own LTS documentation] | <> | >=17.9.0 <22.0.0 | -| <> | >=9.0.0 <19.0.0 | Deprecated. No longer tested. | <> via koa-router or @koa/router | >=5.2.0 <13.0.0 | Koa doesn't have a built in router, so we can't support Koa directly since we rely on router information for full support. We currently support the most popular Koa router called https://github.com/koajs/koa-router[koa-router]. | <> | >=11.1.0 <13.3.0 | (Technical Preview) This instruments Next.js routing to name transactions for incoming HTTP transactions; and reports errors in user pages. It supports the Next.js production server (`next start`) and development server (`next dev`). See the <>. | <> | >=5.2.0 <12.0.0 | diff --git a/lib/instrumentation/index.js b/lib/instrumentation/index.js index 23eaf9916d..66b1b28ae1 100644 --- a/lib/instrumentation/index.js +++ b/lib/instrumentation/index.js @@ -61,7 +61,7 @@ var MODULES = [ 'generic-pool', 'graphql', 'handlebars', - ['hapi', '@hapi/hapi'], + '@hapi/hapi', 'http', 'https', 'http2', diff --git a/lib/instrumentation/modules/hapi.js b/lib/instrumentation/modules/@hapi/hapi.js similarity index 65% rename from lib/instrumentation/modules/hapi.js rename to lib/instrumentation/modules/@hapi/hapi.js index 4975c88e8f..e33b996c85 100644 --- a/lib/instrumentation/modules/hapi.js +++ b/lib/instrumentation/modules/@hapi/hapi.js @@ -8,39 +8,29 @@ var semver = require('semver'); -var shimmer = require('../shimmer'); +var shimmer = require('../../shimmer'); var onPreAuthSym = Symbol('ElasticAPMOnPreAuth'); module.exports = function (hapi, agent, { version, enabled }) { - if (!enabled) return hapi; - - agent.setFramework({ name: 'hapi', version, overwrite: false }); - - if (!semver.satisfies(version, '>=9.0.0')) { - agent.logger.debug('hapi version %s not supported - aborting...', version); + if (!enabled) { + return hapi; + } + if (!semver.satisfies(version, '>=17.9.0 <22.0.0')) { + agent.logger.debug('@hapi/hapi@%s not supported, skipping', version); return hapi; } - const isHapiGte17 = semver.satisfies(version, '>=17'); - agent.logger.debug('shimming hapi.Server.prototype.initialize'); + agent.setFramework({ name: 'hapi', version, overwrite: false }); - if (isHapiGte17) { - shimmer.massWrap(hapi, ['Server', 'server'], function (orig) { - return function (options) { - var res = orig.apply(this, arguments); - patchServer(res); - return res; - }; - }); - } else { - shimmer.wrap(hapi.Server.prototype, 'initialize', function (orig) { - return function () { - patchServer(this); - return orig.apply(this, arguments); - }; - }); - } + agent.logger.debug('shimming hapi.Server, hapi.server'); + shimmer.massWrap(hapi, ['Server', 'server'], function (orig) { + return function (options) { + var res = orig.apply(this, arguments); + patchServer(res); + return res; + }; + }); function patchServer(server) { // Hooks that are always allowed @@ -52,40 +42,14 @@ module.exports = function (hapi, agent, { version, enabled }) { agent.logger.debug('unable to enable hapi error tracking'); } - // Prior to hapi 17, when the server has no connections we can't make - // connection lifecycle hooks (in hapi 17+ the server always has - // connections, though the `server.connections` property doesn't exists, - // so this if-statement wont fire) - var conns = server.connections; - if (conns && conns.length === 0) { - agent.logger.debug( - 'unable to enable hapi instrumentation on connectionless server', - ); - return; - } - - // Hooks that are only allowed when the hapi server has connections - // (with hapi 17+ this is always the case) - if (typeof server.ext === 'function') { - server.ext('onPreAuth', onPreAuth); - server.ext('onPreResponse', onPreResponse); - if (agent._conf.captureBody !== 'off') { - server.ext('onPostAuth', onPostAuth); - } - } else { - agent.logger.debug('unable to enable automatic hapi transaction naming'); + server.ext('onPreAuth', onPreAuth); + server.ext('onPreResponse', onPreResponse); + if (agent._conf.captureBody !== 'off') { + server.ext('onPostAuth', onPostAuth); } } function attachEvents(emitter) { - if (!isHapiGte17) { - emitter.on('request-error', function (request, error) { - agent.captureError(error, { - request: request.raw && request.raw.req, - }); - }); - } - emitter.on('log', function (event, tags) { captureError('log', null, event, tags); }); @@ -150,7 +114,7 @@ module.exports = function (hapi, agent, { version, enabled }) { } } - return isHapiGte17 ? reply.continue : reply.continue(); + return reply.continue; } function onPostAuth(request, reply) { @@ -158,7 +122,7 @@ module.exports = function (hapi, agent, { version, enabled }) { // Save the parsed req body to be picked up by getContextFromRequest(). request.raw.req.payload = request.payload; } - return isHapiGte17 ? reply.continue : reply.continue(); + return reply.continue; } function onPreResponse(request, reply) { @@ -182,7 +146,7 @@ module.exports = function (hapi, agent, { version, enabled }) { agent._instrumentation.setDefaultTransactionName('CORS preflight'); } - return isHapiGte17 ? reply.continue : reply.continue(); + return reply.continue; } return hapi; diff --git a/test/_is_hapi_incompat.js b/test/_is_hapi_incompat.js index db4acad859..893dcccb34 100644 --- a/test/_is_hapi_incompat.js +++ b/test/_is_hapi_incompat.js @@ -8,11 +8,10 @@ var semver = require('semver'); -// 'hapi' and '@hapi/hapi' versions have some challenges with compat with -// various versions of node. This method tells you if the current versions -// are incompatible. -function isHapiIncompat(moduleName) { - var hapiVersion = require(`${moduleName}/package.json`).version; +// '@hapi/hapi' versions have some challenges with compat with various versions +// of node. This method tells you if the current versions are incompatible. +function isHapiIncompat() { + var hapiVersion = require(`@hapi/hapi/package.json`).version; // hapi 17+ requires Node.js 8.9.0 or higher if ( diff --git a/test/config.test.js b/test/config.test.js index e553a01916..43f1a1f0f3 100644 --- a/test/config.test.js +++ b/test/config.test.js @@ -1177,9 +1177,8 @@ test('disableInstrumentations', function (t) { [], ); var modules = new Set(flattenedModules); - modules.delete('hapi'); // Deprecated, we no longer test this instrumentation. modules.delete('jade'); // Deprecated, we no longer test this instrumentation. - if (isHapiIncompat('@hapi/hapi')) { + if (isHapiIncompat()) { modules.delete('@hapi/hapi'); } modules.delete('express-graphql'); diff --git a/test/instrumentation/modules/hapi/basic.test.js b/test/instrumentation/modules/hapi/basic.test.js deleted file mode 100644 index cb51247672..0000000000 --- a/test/instrumentation/modules/hapi/basic.test.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and other contributors where applicable. - * Licensed under the BSD 2-Clause License; you may not use this file except in - * compliance with the BSD 2-Clause License. - */ - -'use strict'; - -require('./shared')('@hapi/hapi'); diff --git a/test/instrumentation/modules/hapi/hapi.test.js b/test/instrumentation/modules/hapi/hapi.test.js new file mode 100644 index 0000000000..7cf304f8a7 --- /dev/null +++ b/test/instrumentation/modules/hapi/hapi.test.js @@ -0,0 +1,683 @@ +/* + * Copyright Elasticsearch B.V. and other contributors where applicable. + * Licensed under the BSD 2-Clause License; you may not use this file except in + * compliance with the BSD 2-Clause License. + */ + +'use strict'; + +var agent = require('../../../..').start({ + serviceName: 'test-hapi', + captureExceptions: false, + logLevel: 'off', + metricsInterval: 0, + centralConfig: false, + cloudProvider: 'none', + captureBody: 'all', +}); + +var isHapiIncompat = require('../../../_is_hapi_incompat'); +if (isHapiIncompat()) { + // Skip out of this test. + console.log( + `# SKIP this version of @hapi/hapi is incompatible with node ${process.version}`, + ); + process.exit(); +} + +var http = require('http'); + +var Hapi = require('@hapi/hapi'); +var pkg = require('@hapi/hapi/package.json'); +var semver = require('semver'); +var test = require('tape'); + +var mockClient = require('../../../_mock_http_client'); + +var originalCaptureError = agent.captureError; + +function noop() {} + +test('extract URL from request', function (t) { + resetAgent(2, function (data) { + t.strictEqual(data.transactions.length, 1); + t.strictEqual(data.errors.length, 1); + var request = data.errors[0].context.request; + t.strictEqual(request.method, 'GET'); + t.strictEqual(request.url.pathname, '/captureError'); + t.strictEqual(request.url.search, '?foo=bar'); + t.strictEqual(request.url.raw, '/captureError?foo=bar'); + t.strictEqual(request.url.hostname, 'localhost'); + t.strictEqual(request.url.port, String(server.info.port)); + server.stop(noop); + t.end(); + }); + + agent.captureError = originalCaptureError; + + var server = startServer(function (err, port) { + t.error(err, 'no error from startServer'); + http.get('http://localhost:' + port + '/captureError?foo=bar'); + }); +}); + +test('route naming', function (t) { + t.plan(8); + + resetAgent(1, function (data) { + assert(t, data); + server.stop(noop); + }); + + var server = startServer(function (err, port) { + t.error(err); + http.get('http://localhost:' + port + '/hello', function (res) { + t.strictEqual(res.statusCode, 200); + res.on('data', function (chunk) { + t.strictEqual(chunk.toString(), 'hello world'); + }); + res.on('end', function () { + agent.flush(); + }); + }); + }); +}); + +test('captureBody', function (t) { + t.plan(9); + + const postData = JSON.stringify({ foo: 'bar' }); + + resetAgent(1, function (data) { + assert(t, data, { name: 'POST /postSomeData', method: 'POST' }); + t.equal( + data.transactions[0].context.request.body, + postData, + 'body was captured to trans.context.request.body', + ); + server.stop(noop); + }); + + var server = startServer(function (err, port) { + t.error(err); + const cReq = http.request( + { + method: 'POST', + hostname: 'localhost', + port, + path: '/postSomeData', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData), + }, + }, + function (res) { + t.strictEqual(res.statusCode, 200); + res.on('data', function (chunk) { + t.strictEqual(chunk.toString(), 'your data has been posted'); + }); + res.on('end', function () { + agent.flush(); + }); + }, + ); + cReq.write(postData); + cReq.end(); + }); +}); + +test('connectionless', function (t) { + if (semver.satisfies(pkg.version, '<15.0.2')) { + t.pass('skipping'); + t.end(); + return; + } + + t.plan(1); + + resetAgent(); + + var server = makeServer(); + initServer(server, function (err) { + server.stop(noop); + t.error(err, 'start error'); + }); +}); + +test('connectionless server error logging with Error', function (t) { + if (semver.satisfies(pkg.version, '<15.0.2')) { + t.pass('skipping'); + t.end(); + return; + } + + t.plan(6); + + var customError = new Error('custom error'); + + resetAgent(); + + agent.captureError = function (err, opts) { + server.stop(noop); + + t.strictEqual(err, customError); + t.ok(opts.custom); + t.deepEqual(opts.custom.tags, ['error']); + t.false(opts.custom.internals); + t.ok(opts.custom.data instanceof Error); + }; + + var server = makeServer(); + initServer(server, function (err) { + t.error(err, 'start error'); + + server.log(['error'], customError); + }); +}); + +test('connectionless server error logging with String', function (t) { + if (semver.satisfies(pkg.version, '<15.0.2')) { + t.pass('skipping'); + t.end(); + return; + } + + t.plan(6); + + var customError = 'custom error'; + + resetAgent(); + + agent.captureError = function (err, opts) { + server.stop(noop); + + t.strictEqual(err, customError); + t.ok(opts.custom); + t.deepEqual(opts.custom.tags, ['error']); + t.false(opts.custom.internals); + t.ok(typeof opts.custom.data === 'string'); + }; + + var server = makeServer(); + initServer(server, function (err) { + t.error(err, 'start error'); + + server.log(['error'], customError); + }); +}); + +test('connectionless server error logging with Object', function (t) { + if (semver.satisfies(pkg.version, '<15.0.2')) { + t.pass('skipping'); + t.end(); + return; + } + + t.plan(6); + + var customError = { + error: 'I forgot to turn this into an actual Error', + }; + + resetAgent(); + + agent.captureError = function (err, opts) { + server.stop(noop); + + t.strictEqual(err, 'hapi server emitted a log event tagged error'); + t.ok(opts.custom); + t.deepEqual(opts.custom.tags, ['error']); + t.false(opts.custom.internals); + t.deepEqual(opts.custom.data, customError); + }; + + var server = makeServer(); + initServer(server, function (err) { + t.error(err, 'start error'); + + server.log(['error'], customError); + }); +}); + +test('server error logging with Error', function (t) { + t.plan(6); + + var customError = new Error('custom error'); + + resetAgent(); + + agent.captureError = function (err, opts) { + server.stop(noop); + + t.strictEqual(err, customError); + t.ok(opts.custom); + t.deepEqual(opts.custom.tags, ['error']); + t.false(opts.custom.internals); + t.ok(opts.custom.data instanceof Error); + }; + + var server = startServer(function (err) { + t.error(err, 'start error'); + + server.log(['error'], customError); + }); +}); + +test('server error logging with Error does not affect event tags', function (t) { + t.plan(8); + + var customError = new Error('custom error'); + + resetAgent(); + + agent.captureError = function (err, opts) { + server.stop(noop); + + t.strictEqual(err, customError); + t.ok(opts.custom); + t.deepEqual(opts.custom.tags, ['error']); + t.false(opts.custom.internals); + t.ok(opts.custom.data instanceof Error); + }; + + var server = makeServer(); + + var emitter = server.events || server; + emitter.on('log', function (event, tags) { + t.deepEqual(event.tags, ['error']); + }); + + runServer(server, function (err) { + t.error(err, 'start error'); + + emitter.on('log', function (event, tags) { + t.deepEqual(event.tags, ['error']); + }); + + server.log(['error'], customError); + }); +}); + +test('server error logging with String', function (t) { + t.plan(6); + + var customError = 'custom error'; + + resetAgent(); + + agent.captureError = function (err, opts) { + server.stop(noop); + + t.strictEqual(err, customError); + t.ok(opts.custom); + t.deepEqual(opts.custom.tags, ['error']); + t.false(opts.custom.internals); + t.ok(typeof opts.custom.data === 'string'); + }; + + var server = startServer(function (err) { + t.error(err, 'start error'); + + server.log(['error'], customError); + }); +}); + +test('server error logging with Object', function (t) { + t.plan(6); + + var customError = { + error: 'I forgot to turn this into an actual Error', + }; + + resetAgent(); + + agent.captureError = function (err, opts) { + server.stop(noop); + + t.strictEqual(err, 'hapi server emitted a log event tagged error'); + t.ok(opts.custom); + t.deepEqual(opts.custom.tags, ['error']); + t.false(opts.custom.internals); + t.deepEqual(opts.custom.data, customError); + }; + + var server = startServer(function (err) { + t.error(err, 'start error'); + + server.log(['error'], customError); + }); +}); + +test('request error logging with Error', function (t) { + t.plan(13); + + var customError = new Error('custom error'); + + resetAgent(1, function (data) { + assert(t, data, { status: 'HTTP 2xx', name: 'GET /error' }); + + server.stop(noop); + }); + + agent.captureError = function (err, opts) { + t.strictEqual(err, customError); + t.ok(opts.custom); + t.ok(opts.request); + t.deepEqual(opts.custom.tags, ['error']); + t.false(opts.custom.internals); + t.ok(opts.custom.data instanceof Error); + }; + + var server = makeServer(); + + server.route({ + method: 'GET', + path: '/error', + handler: handler(function (request) { + request.log(['error'], customError); + + return 'hello world'; + }), + }); + + runServer(server, function (err) { + t.error(err, 'start error'); + + http.get('http://localhost:' + server.info.port + '/error', function (res) { + t.strictEqual(res.statusCode, 200); + + res.resume().on('end', function () { + agent.flush(); + }); + }); + }); +}); + +test('request error logging with Error does not affect event tags', function (t) { + t.plan(15); + + var customError = new Error('custom error'); + + resetAgent(1, function (data) { + assert(t, data, { status: 'HTTP 2xx', name: 'GET /error' }); + + server.stop(noop); + }); + + agent.captureError = function (err, opts) { + t.strictEqual(err, customError); + t.ok(opts.custom); + t.ok(opts.request); + t.deepEqual(opts.custom.tags, ['elastic-apm', 'error']); + t.false(opts.custom.internals); + t.ok(opts.custom.data instanceof Error); + }; + + var server = makeServer(); + + server.route({ + method: 'GET', + path: '/error', + handler: handler(function (request) { + request.log(['elastic-apm', 'error'], customError); + + return 'hello world'; + }), + }); + + var emitter = server.events || server; + emitter.on('request', function (req, event, tags) { + if (event.channel === 'internal') return; + t.deepEqual(event.tags, ['elastic-apm', 'error']); + }); + + runServer(server, function (err) { + t.error(err, 'start error'); + + emitter.on('request', function (req, event, tags) { + if (event.channel === 'internal') return; + t.deepEqual(event.tags, ['elastic-apm', 'error']); + }); + + http.get('http://localhost:' + server.info.port + '/error', function (res) { + t.strictEqual(res.statusCode, 200); + + res.resume().on('end', function () { + agent.flush(); + }); + }); + }); +}); + +test('request error logging with String', function (t) { + t.plan(13); + + var customError = 'custom error'; + + resetAgent(1, function (data) { + assert(t, data, { status: 'HTTP 2xx', name: 'GET /error' }); + + server.stop(noop); + }); + + agent.captureError = function (err, opts) { + t.strictEqual(err, customError); + t.ok(opts.custom); + t.ok(opts.request); + t.deepEqual(opts.custom.tags, ['error']); + t.false(opts.custom.internals); + t.ok(typeof opts.custom.data === 'string'); + }; + + var server = makeServer(); + + server.route({ + method: 'GET', + path: '/error', + handler: handler(function (request) { + request.log(['error'], customError); + + return 'hello world'; + }), + }); + + runServer(server, function (err) { + t.error(err, 'start error'); + + http.get('http://localhost:' + server.info.port + '/error', function (res) { + t.strictEqual(res.statusCode, 200); + + res.resume().on('end', function () { + agent.flush(); + }); + }); + }); +}); + +test('request error logging with Object', function (t) { + t.plan(13); + + var customError = { + error: 'I forgot to turn this into an actual Error', + }; + + resetAgent(1, function (data) { + assert(t, data, { status: 'HTTP 2xx', name: 'GET /error' }); + + server.stop(noop); + }); + + agent.captureError = function (err, opts) { + t.strictEqual(err, 'hapi server emitted a request event tagged error'); + t.ok(opts.custom); + t.ok(opts.request); + t.deepEqual(opts.custom.tags, ['error']); + t.false(opts.custom.internals); + t.deepEqual(opts.custom.data, customError); + }; + + var server = makeServer(); + + server.route({ + method: 'GET', + path: '/error', + handler: handler(function (request) { + request.log(['error'], customError); + + return 'hello world'; + }), + }); + + runServer(server, function (err) { + t.error(err, 'start error'); + + http.get('http://localhost:' + server.info.port + '/error', function (res) { + t.strictEqual(res.statusCode, 200); + + res.resume().on('end', function () { + agent.flush(); + }); + }); + }); +}); + +test('error handling', function (t) { + t.plan(10); + + resetAgent(1, function (data) { + assert(t, data, { status: 'HTTP 5xx', name: 'GET /error' }); + server.stop(noop); + }); + + agent.captureError = function (err, opts) { + t.strictEqual(err.message, 'foo'); + t.ok(opts.request instanceof http.IncomingMessage); + }; + + var server = startServer(function (err, port) { + t.error(err); + http.get('http://localhost:' + port + '/error', function (res) { + t.strictEqual(res.statusCode, 500); + res.on('data', function (chunk) { + var data = JSON.parse(chunk.toString()); + t.deepEqual(data, { + statusCode: 500, + error: 'Internal Server Error', + message: 'An internal server error occurred', + }); + }); + res.on('end', function () { + agent.flush(); + }); + }); + }); +}); + +function makeServer(opts) { + // Specify 'localhost' to avoid Hapi default of '0.0.0.0' which ties to + // IPv4. We want a later HTTP client request using 'localhost' to work. + var server; + if (semver.satisfies(pkg.version, '<17')) { + server = new Hapi.Server(); + opts = opts || {}; + opts.host = opts.host || 'localhost'; + server.connection(opts); + } else { + server = new Hapi.Server({ host: 'localhost' }); + } + return server; +} + +function initServer(server, cb) { + if (semver.satisfies(pkg.version, '<17')) { + server.initialize(cb); + } else { + server.initialize().then(cb.bind(null, null), cb); + } +} + +function runServer(server, cb) { + if (semver.satisfies(pkg.version, '<17')) { + server.start(function (err) { + if (err) throw err; + cb(null, server.info.port); + }); + } else { + server.start().then(() => cb(null, server.info.port), cb); + } +} + +function startServer(cb) { + var server = buildServer(); + runServer(server, cb); + return server; +} + +function handler(fn) { + if (semver.satisfies(pkg.version, '>=17')) return fn; + return function (request, reply) { + var p = new Promise(function (resolve, reject) { + resolve(fn(request)); + }); + p.then(reply, reply); + }; +} + +function buildServer() { + var server = makeServer(); + + server.route({ + method: 'GET', + path: '/hello', + handler: handler(function (request) { + return 'hello world'; + }), + }); + server.route({ + method: 'POST', + path: '/postSomeData', + handler: handler(function (request) { + return 'your data has been posted'; + }), + }); + server.route({ + method: 'GET', + path: '/error', + handler: handler(function (request) { + throw new Error('foo'); + }), + }); + server.route({ + method: 'GET', + path: '/captureError', + handler: handler(function (request) { + agent.captureError(new Error()); + return ''; + }), + }); + return server; +} + +function assert(t, data, results) { + if (!results) results = {}; + results.status = results.status || 'HTTP 2xx'; + results.name = results.name || 'GET /hello'; + results.method = results.method || 'GET'; + + t.strictEqual(data.transactions.length, 1); + + var trans = data.transactions[0]; + + t.strictEqual(trans.name, results.name); + t.strictEqual(trans.type, 'request'); + t.strictEqual(trans.result, results.status); + t.strictEqual(trans.context.request.method, results.method); +} + +function resetAgent(expected, cb) { + agent._instrumentation.testReset(); + agent._apmClient = mockClient(expected, cb); + agent.captureError = function (err) { + throw err; + }; +} diff --git a/test/instrumentation/modules/hapi/set-framework-hapihapi.test.js b/test/instrumentation/modules/hapi/set-framework.test.js similarity index 96% rename from test/instrumentation/modules/hapi/set-framework-hapihapi.test.js rename to test/instrumentation/modules/hapi/set-framework.test.js index 55445d7a5f..fbb33a66d4 100644 --- a/test/instrumentation/modules/hapi/set-framework-hapihapi.test.js +++ b/test/instrumentation/modules/hapi/set-framework.test.js @@ -12,7 +12,7 @@ const agent = require('../../../..').start({ }); var isHapiIncompat = require('../../../_is_hapi_incompat'); -if (isHapiIncompat('@hapi/hapi')) { +if (isHapiIncompat()) { // Skip out of this test. console.log( `# SKIP this version of '@hapi/hapi' is incompatible with node ${process.version}`, diff --git a/test/instrumentation/modules/hapi/shared.js b/test/instrumentation/modules/hapi/shared.js deleted file mode 100644 index 874c441e2d..0000000000 --- a/test/instrumentation/modules/hapi/shared.js +++ /dev/null @@ -1,697 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and other contributors where applicable. - * Licensed under the BSD 2-Clause License; you may not use this file except in - * compliance with the BSD 2-Clause License. - */ - -'use strict'; - -module.exports = (moduleName) => { - var agent = require('../../../..').start({ - serviceName: 'test-hapi', - captureExceptions: false, - logLevel: 'off', - metricsInterval: 0, - centralConfig: false, - cloudProvider: 'none', - captureBody: 'all', - }); - - var isHapiIncompat = require('../../../_is_hapi_incompat'); - if (isHapiIncompat(moduleName)) { - // Skip out of this test. - console.log( - `# SKIP this version of ${moduleName} is incompatible with node ${process.version}`, - ); - process.exit(); - } - - var http = require('http'); - - var Hapi = require(moduleName); - var pkg = require(moduleName + '/package.json'); - var semver = require('semver'); - var test = require('tape'); - - var mockClient = require('../../../_mock_http_client'); - - var originalCaptureError = agent.captureError; - - function noop() {} - - test('extract URL from request', function (t) { - resetAgent(2, function (data) { - t.strictEqual(data.transactions.length, 1); - t.strictEqual(data.errors.length, 1); - var request = data.errors[0].context.request; - t.strictEqual(request.method, 'GET'); - t.strictEqual(request.url.pathname, '/captureError'); - t.strictEqual(request.url.search, '?foo=bar'); - t.strictEqual(request.url.raw, '/captureError?foo=bar'); - t.strictEqual(request.url.hostname, 'localhost'); - t.strictEqual(request.url.port, String(server.info.port)); - server.stop(noop); - t.end(); - }); - - agent.captureError = originalCaptureError; - - var server = startServer(function (err, port) { - t.error(err, 'no error from startServer'); - http.get('http://localhost:' + port + '/captureError?foo=bar'); - }); - }); - - test('route naming', function (t) { - t.plan(8); - - resetAgent(1, function (data) { - assert(t, data); - server.stop(noop); - }); - - var server = startServer(function (err, port) { - t.error(err); - http.get('http://localhost:' + port + '/hello', function (res) { - t.strictEqual(res.statusCode, 200); - res.on('data', function (chunk) { - t.strictEqual(chunk.toString(), 'hello world'); - }); - res.on('end', function () { - agent.flush(); - }); - }); - }); - }); - - test('captureBody', function (t) { - t.plan(9); - - const postData = JSON.stringify({ foo: 'bar' }); - - resetAgent(1, function (data) { - assert(t, data, { name: 'POST /postSomeData', method: 'POST' }); - t.equal( - data.transactions[0].context.request.body, - postData, - 'body was captured to trans.context.request.body', - ); - server.stop(noop); - }); - - var server = startServer(function (err, port) { - t.error(err); - const cReq = http.request( - { - method: 'POST', - hostname: 'localhost', - port, - path: '/postSomeData', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(postData), - }, - }, - function (res) { - t.strictEqual(res.statusCode, 200); - res.on('data', function (chunk) { - t.strictEqual(chunk.toString(), 'your data has been posted'); - }); - res.on('end', function () { - agent.flush(); - }); - }, - ); - cReq.write(postData); - cReq.end(); - }); - }); - - test('connectionless', function (t) { - if (semver.satisfies(pkg.version, '<15.0.2')) { - t.pass('skipping'); - t.end(); - return; - } - - t.plan(1); - - resetAgent(); - - var server = makeServer(); - initServer(server, function (err) { - server.stop(noop); - t.error(err, 'start error'); - }); - }); - - test('connectionless server error logging with Error', function (t) { - if (semver.satisfies(pkg.version, '<15.0.2')) { - t.pass('skipping'); - t.end(); - return; - } - - t.plan(6); - - var customError = new Error('custom error'); - - resetAgent(); - - agent.captureError = function (err, opts) { - server.stop(noop); - - t.strictEqual(err, customError); - t.ok(opts.custom); - t.deepEqual(opts.custom.tags, ['error']); - t.false(opts.custom.internals); - t.ok(opts.custom.data instanceof Error); - }; - - var server = makeServer(); - initServer(server, function (err) { - t.error(err, 'start error'); - - server.log(['error'], customError); - }); - }); - - test('connectionless server error logging with String', function (t) { - if (semver.satisfies(pkg.version, '<15.0.2')) { - t.pass('skipping'); - t.end(); - return; - } - - t.plan(6); - - var customError = 'custom error'; - - resetAgent(); - - agent.captureError = function (err, opts) { - server.stop(noop); - - t.strictEqual(err, customError); - t.ok(opts.custom); - t.deepEqual(opts.custom.tags, ['error']); - t.false(opts.custom.internals); - t.ok(typeof opts.custom.data === 'string'); - }; - - var server = makeServer(); - initServer(server, function (err) { - t.error(err, 'start error'); - - server.log(['error'], customError); - }); - }); - - test('connectionless server error logging with Object', function (t) { - if (semver.satisfies(pkg.version, '<15.0.2')) { - t.pass('skipping'); - t.end(); - return; - } - - t.plan(6); - - var customError = { - error: 'I forgot to turn this into an actual Error', - }; - - resetAgent(); - - agent.captureError = function (err, opts) { - server.stop(noop); - - t.strictEqual(err, 'hapi server emitted a log event tagged error'); - t.ok(opts.custom); - t.deepEqual(opts.custom.tags, ['error']); - t.false(opts.custom.internals); - t.deepEqual(opts.custom.data, customError); - }; - - var server = makeServer(); - initServer(server, function (err) { - t.error(err, 'start error'); - - server.log(['error'], customError); - }); - }); - - test('server error logging with Error', function (t) { - t.plan(6); - - var customError = new Error('custom error'); - - resetAgent(); - - agent.captureError = function (err, opts) { - server.stop(noop); - - t.strictEqual(err, customError); - t.ok(opts.custom); - t.deepEqual(opts.custom.tags, ['error']); - t.false(opts.custom.internals); - t.ok(opts.custom.data instanceof Error); - }; - - var server = startServer(function (err) { - t.error(err, 'start error'); - - server.log(['error'], customError); - }); - }); - - test('server error logging with Error does not affect event tags', function (t) { - t.plan(8); - - var customError = new Error('custom error'); - - resetAgent(); - - agent.captureError = function (err, opts) { - server.stop(noop); - - t.strictEqual(err, customError); - t.ok(opts.custom); - t.deepEqual(opts.custom.tags, ['error']); - t.false(opts.custom.internals); - t.ok(opts.custom.data instanceof Error); - }; - - var server = makeServer(); - - var emitter = server.events || server; - emitter.on('log', function (event, tags) { - t.deepEqual(event.tags, ['error']); - }); - - runServer(server, function (err) { - t.error(err, 'start error'); - - emitter.on('log', function (event, tags) { - t.deepEqual(event.tags, ['error']); - }); - - server.log(['error'], customError); - }); - }); - - test('server error logging with String', function (t) { - t.plan(6); - - var customError = 'custom error'; - - resetAgent(); - - agent.captureError = function (err, opts) { - server.stop(noop); - - t.strictEqual(err, customError); - t.ok(opts.custom); - t.deepEqual(opts.custom.tags, ['error']); - t.false(opts.custom.internals); - t.ok(typeof opts.custom.data === 'string'); - }; - - var server = startServer(function (err) { - t.error(err, 'start error'); - - server.log(['error'], customError); - }); - }); - - test('server error logging with Object', function (t) { - t.plan(6); - - var customError = { - error: 'I forgot to turn this into an actual Error', - }; - - resetAgent(); - - agent.captureError = function (err, opts) { - server.stop(noop); - - t.strictEqual(err, 'hapi server emitted a log event tagged error'); - t.ok(opts.custom); - t.deepEqual(opts.custom.tags, ['error']); - t.false(opts.custom.internals); - t.deepEqual(opts.custom.data, customError); - }; - - var server = startServer(function (err) { - t.error(err, 'start error'); - - server.log(['error'], customError); - }); - }); - - test('request error logging with Error', function (t) { - t.plan(13); - - var customError = new Error('custom error'); - - resetAgent(1, function (data) { - assert(t, data, { status: 'HTTP 2xx', name: 'GET /error' }); - - server.stop(noop); - }); - - agent.captureError = function (err, opts) { - t.strictEqual(err, customError); - t.ok(opts.custom); - t.ok(opts.request); - t.deepEqual(opts.custom.tags, ['error']); - t.false(opts.custom.internals); - t.ok(opts.custom.data instanceof Error); - }; - - var server = makeServer(); - - server.route({ - method: 'GET', - path: '/error', - handler: handler(function (request) { - request.log(['error'], customError); - - return 'hello world'; - }), - }); - - runServer(server, function (err) { - t.error(err, 'start error'); - - http.get( - 'http://localhost:' + server.info.port + '/error', - function (res) { - t.strictEqual(res.statusCode, 200); - - res.resume().on('end', function () { - agent.flush(); - }); - }, - ); - }); - }); - - test('request error logging with Error does not affect event tags', function (t) { - t.plan(15); - - var customError = new Error('custom error'); - - resetAgent(1, function (data) { - assert(t, data, { status: 'HTTP 2xx', name: 'GET /error' }); - - server.stop(noop); - }); - - agent.captureError = function (err, opts) { - t.strictEqual(err, customError); - t.ok(opts.custom); - t.ok(opts.request); - t.deepEqual(opts.custom.tags, ['elastic-apm', 'error']); - t.false(opts.custom.internals); - t.ok(opts.custom.data instanceof Error); - }; - - var server = makeServer(); - - server.route({ - method: 'GET', - path: '/error', - handler: handler(function (request) { - request.log(['elastic-apm', 'error'], customError); - - return 'hello world'; - }), - }); - - var emitter = server.events || server; - emitter.on('request', function (req, event, tags) { - if (event.channel === 'internal') return; - t.deepEqual(event.tags, ['elastic-apm', 'error']); - }); - - runServer(server, function (err) { - t.error(err, 'start error'); - - emitter.on('request', function (req, event, tags) { - if (event.channel === 'internal') return; - t.deepEqual(event.tags, ['elastic-apm', 'error']); - }); - - http.get( - 'http://localhost:' + server.info.port + '/error', - function (res) { - t.strictEqual(res.statusCode, 200); - - res.resume().on('end', function () { - agent.flush(); - }); - }, - ); - }); - }); - - test('request error logging with String', function (t) { - t.plan(13); - - var customError = 'custom error'; - - resetAgent(1, function (data) { - assert(t, data, { status: 'HTTP 2xx', name: 'GET /error' }); - - server.stop(noop); - }); - - agent.captureError = function (err, opts) { - t.strictEqual(err, customError); - t.ok(opts.custom); - t.ok(opts.request); - t.deepEqual(opts.custom.tags, ['error']); - t.false(opts.custom.internals); - t.ok(typeof opts.custom.data === 'string'); - }; - - var server = makeServer(); - - server.route({ - method: 'GET', - path: '/error', - handler: handler(function (request) { - request.log(['error'], customError); - - return 'hello world'; - }), - }); - - runServer(server, function (err) { - t.error(err, 'start error'); - - http.get( - 'http://localhost:' + server.info.port + '/error', - function (res) { - t.strictEqual(res.statusCode, 200); - - res.resume().on('end', function () { - agent.flush(); - }); - }, - ); - }); - }); - - test('request error logging with Object', function (t) { - t.plan(13); - - var customError = { - error: 'I forgot to turn this into an actual Error', - }; - - resetAgent(1, function (data) { - assert(t, data, { status: 'HTTP 2xx', name: 'GET /error' }); - - server.stop(noop); - }); - - agent.captureError = function (err, opts) { - t.strictEqual(err, 'hapi server emitted a request event tagged error'); - t.ok(opts.custom); - t.ok(opts.request); - t.deepEqual(opts.custom.tags, ['error']); - t.false(opts.custom.internals); - t.deepEqual(opts.custom.data, customError); - }; - - var server = makeServer(); - - server.route({ - method: 'GET', - path: '/error', - handler: handler(function (request) { - request.log(['error'], customError); - - return 'hello world'; - }), - }); - - runServer(server, function (err) { - t.error(err, 'start error'); - - http.get( - 'http://localhost:' + server.info.port + '/error', - function (res) { - t.strictEqual(res.statusCode, 200); - - res.resume().on('end', function () { - agent.flush(); - }); - }, - ); - }); - }); - - test('error handling', function (t) { - t.plan(10); - - resetAgent(1, function (data) { - assert(t, data, { status: 'HTTP 5xx', name: 'GET /error' }); - server.stop(noop); - }); - - agent.captureError = function (err, opts) { - t.strictEqual(err.message, 'foo'); - t.ok(opts.request instanceof http.IncomingMessage); - }; - - var server = startServer(function (err, port) { - t.error(err); - http.get('http://localhost:' + port + '/error', function (res) { - t.strictEqual(res.statusCode, 500); - res.on('data', function (chunk) { - var data = JSON.parse(chunk.toString()); - t.deepEqual(data, { - statusCode: 500, - error: 'Internal Server Error', - message: 'An internal server error occurred', - }); - }); - res.on('end', function () { - agent.flush(); - }); - }); - }); - }); - - function makeServer(opts) { - // Specify 'localhost' to avoid Hapi default of '0.0.0.0' which ties to - // IPv4. We want a later HTTP client request using 'localhost' to work. - var server; - if (semver.satisfies(pkg.version, '<17')) { - server = new Hapi.Server(); - opts = opts || {}; - opts.host = opts.host || 'localhost'; - server.connection(opts); - } else { - server = new Hapi.Server({ host: 'localhost' }); - } - return server; - } - - function initServer(server, cb) { - if (semver.satisfies(pkg.version, '<17')) { - server.initialize(cb); - } else { - server.initialize().then(cb.bind(null, null), cb); - } - } - - function runServer(server, cb) { - if (semver.satisfies(pkg.version, '<17')) { - server.start(function (err) { - if (err) throw err; - cb(null, server.info.port); - }); - } else { - server.start().then(() => cb(null, server.info.port), cb); - } - } - - function startServer(cb) { - var server = buildServer(); - runServer(server, cb); - return server; - } - - function handler(fn) { - if (semver.satisfies(pkg.version, '>=17')) return fn; - return function (request, reply) { - var p = new Promise(function (resolve, reject) { - resolve(fn(request)); - }); - p.then(reply, reply); - }; - } - - function buildServer() { - var server = makeServer(); - - server.route({ - method: 'GET', - path: '/hello', - handler: handler(function (request) { - return 'hello world'; - }), - }); - server.route({ - method: 'POST', - path: '/postSomeData', - handler: handler(function (request) { - return 'your data has been posted'; - }), - }); - server.route({ - method: 'GET', - path: '/error', - handler: handler(function (request) { - throw new Error('foo'); - }), - }); - server.route({ - method: 'GET', - path: '/captureError', - handler: handler(function (request) { - agent.captureError(new Error()); - return ''; - }), - }); - return server; - } - - function assert(t, data, results) { - if (!results) results = {}; - results.status = results.status || 'HTTP 2xx'; - results.name = results.name || 'GET /hello'; - results.method = results.method || 'GET'; - - t.strictEqual(data.transactions.length, 1); - - var trans = data.transactions[0]; - - t.strictEqual(trans.name, results.name); - t.strictEqual(trans.type, 'request'); - t.strictEqual(trans.result, results.status); - t.strictEqual(trans.context.request.method, results.method); - } - - function resetAgent(expected, cb) { - agent._instrumentation.testReset(); - agent._apmClient = mockClient(expected, cb); - agent.captureError = function (err) { - throw err; - }; - } -}; diff --git a/test/sanitize-field-names/hapi.test.js b/test/sanitize-field-names/hapi.test.js index 3a857c6b3a..937f5d4667 100644 --- a/test/sanitize-field-names/hapi.test.js +++ b/test/sanitize-field-names/hapi.test.js @@ -16,7 +16,7 @@ const { const agent = require('../..').start(createAgentConfig()); var isHapiIncompat = require('../_is_hapi_incompat'); -if (isHapiIncompat('@hapi/hapi')) { +if (isHapiIncompat()) { // Skip out of this test. process.exit(); }