diff --git a/lib/agent.js b/lib/agent.js index 0b66794c0e..afd421313f 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -71,8 +71,46 @@ Object.defineProperty(Agent.prototype, 'currentTraceIds', { } }) +// Destroy this agent. This prevents any new agent processing, communication +// with APM server, and resets changed global state *as much as is possible*. +// +// In the typical uses case -- a singleton Agent running for the full process +// lifetime -- it is *not* necessary to call `agent.destroy()`. It is used +// for some testing. +// +// Limitations: +// - Patching/wrapping of functions for instrumentation *is* undone, but +// references to the wrapped versions can remain. +// - There may be in-flight tasks (in ins.addEndedSpan() and +// agent.captureError() for example) that will complete after this destroy +// completes. They should have no impact other than CPU/resource use. Agent.prototype.destroy = function () { - if (this._transport) this._transport.destroy() + if (this._transport && this._transport.destroy) { + this._transport.destroy() + } + // So in-flight tasks in ins.addEndedSpan() and agent.captureError() do + // not use the destroyed transport. + this._transport = null + + // So in-flight tasks do not call user-added filters after the agent has + // been destroyed. + this._errorFilters = new Filters() + this._transactionFilters = new Filters() + this._spanFilters = new Filters() + + if (this._uncaughtExceptionListener) { + process.removeListener('uncaughtException', this._uncaughtExceptionListener) + } + this._metrics.stop() + this._instrumentation.stop() + + // Allow a new Agent instance to `.start()`. Typically this is only relevant + // for tests that may use multiple Agent instances in a single test process. + global[symbols.agentInitialized] = null + + if (Error.stackTraceLimit === this._conf.stackTraceLimit) { + Error.stackTraceLimit = this._origStackTraceLimit + } } // These are metrics about the agent itself -- separate from the metrics @@ -215,6 +253,7 @@ Agent.prototype.start = function (opts) { this._instrumentation.start() this._metrics.start() + this._origStackTraceLimit = Error.stackTraceLimit Error.stackTraceLimit = this._conf.stackTraceLimit if (this._conf.captureExceptions) this.handleUncaughtExceptions() diff --git a/lib/config.js b/lib/config.js index f49f2bad17..22b3f66693 100644 --- a/lib/config.js +++ b/lib/config.js @@ -22,13 +22,6 @@ if (packageName === 'elastic-apm-node') { } var userAgent = `${packageName}/${version}` -config.INTAKE_STRING_MAX_SIZE = 1024 -config.CAPTURE_ERROR_LOG_STACK_TRACES_NEVER = 'never' -config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES = 'messages' -config.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS = 'always' - -module.exports = config - let confFile = loadConfigFile() let serviceName, serviceVersion @@ -38,6 +31,11 @@ try { serviceVersion = version } catch (err) {} +const INTAKE_STRING_MAX_SIZE = 1024 +const CAPTURE_ERROR_LOG_STACK_TRACES_NEVER = 'never' +const CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES = 'messages' +const CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS = 'always' + var DEFAULTS = { abortedErrorThreshold: '25s', active: true, @@ -47,7 +45,7 @@ var DEFAULTS = { asyncHooks: true, breakdownMetrics: true, captureBody: 'off', - captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES, + captureErrorLogStackTraces: CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES, captureExceptions: true, captureHeaders: true, captureSpanStackTraces: true, @@ -562,8 +560,8 @@ function normalizeBools (opts, logger) { } function truncateOptions (opts) { - if (opts.serviceVersion) opts.serviceVersion = truncate(String(opts.serviceVersion), config.INTAKE_STRING_MAX_SIZE) - if (opts.hostname) opts.hostname = truncate(String(opts.hostname), config.INTAKE_STRING_MAX_SIZE) + if (opts.serviceVersion) opts.serviceVersion = truncate(String(opts.serviceVersion), INTAKE_STRING_MAX_SIZE) + if (opts.hostname) opts.hostname = truncate(String(opts.hostname), INTAKE_STRING_MAX_SIZE) } function bytes (input) { @@ -685,7 +683,7 @@ function getBaseClientConfig (conf, agent) { environment: conf.environment, // Sanitize conf - truncateKeywordsAt: config.INTAKE_STRING_MAX_SIZE, + truncateKeywordsAt: INTAKE_STRING_MAX_SIZE, truncateErrorMessagesAt: conf.errorMessageMaxLength, // HTTP conf @@ -720,3 +718,11 @@ function getBaseClientConfig (conf, agent) { cloudMetadataFetcher: (new CloudMetadata(cloudProvider, conf.logger, conf.serviceName)) } } + +// Exports. +module.exports = config +module.exports.INTAKE_STRING_MAX_SIZE = INTAKE_STRING_MAX_SIZE +module.exports.CAPTURE_ERROR_LOG_STACK_TRACES_NEVER = CAPTURE_ERROR_LOG_STACK_TRACES_NEVER +module.exports.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES = CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES +module.exports.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS = CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS +module.exports.DEFAULTS = DEFAULTS diff --git a/lib/instrumentation/async-hooks.js b/lib/instrumentation/async-hooks.js index 1dd168ff8d..6f0c1f4e38 100644 --- a/lib/instrumentation/async-hooks.js +++ b/lib/instrumentation/async-hooks.js @@ -3,20 +3,12 @@ const asyncHooks = require('async_hooks') const shimmer = require('./shimmer') -// FOR INTERNAL TESTING PURPOSES ONLY! -const resettable = process.env._ELASTIC_APM_ASYNC_HOOKS_RESETTABLE === 'true' -let _asyncHook - module.exports = function (ins) { const asyncHook = asyncHooks.createHook({ init, before, destroy }) - const contexts = new WeakMap() - - if (resettable) { - if (_asyncHook) _asyncHook.disable() - _asyncHook = asyncHook - } - + const activeSpans = new Map() const activeTransactions = new Map() + let contexts = new WeakMap() + Object.defineProperty(ins, 'currentTransaction', { get () { const asyncId = asyncHooks.executionAsyncId() @@ -32,7 +24,6 @@ module.exports = function (ins) { } }) - const activeSpans = new Map() Object.defineProperty(ins, 'activeSpan', { get () { const asyncId = asyncHooks.executionAsyncId() @@ -63,6 +54,18 @@ module.exports = function (ins) { } }) + shimmer.wrap(ins, 'stop', function (origStop) { + return function wrappedStop () { + asyncHook.disable() + activeTransactions.clear() + activeSpans.clear() + contexts = new WeakMap() + shimmer.unwrap(ins, 'addEndedTransaction') + shimmer.unwrap(ins, 'stop') + return origStop.call(this) + } + }) + asyncHook.enable() function init (asyncId, type, triggerAsyncId, resource) { diff --git a/lib/instrumentation/index.js b/lib/instrumentation/index.js index 127b2689ff..61c6a02cd5 100644 --- a/lib/instrumentation/index.js +++ b/lib/instrumentation/index.js @@ -86,6 +86,25 @@ function Instrumentation (agent) { } } +// Stop active instrumentation and reset global state *as much as possible*. +// +// Limitations: Removing and re-applying 'require-in-the-middle'-based patches +// has no way to update existing references to patched or unpatched exports from +// those modules. +Instrumentation.prototype.stop = function () { + // Reset context tracking. + this.currentTransaction = null + this.bindingSpan = null + this.activeSpan = null + + // Reset patching. + this._started = false + if (this._hook) { + this._hook.unhook() + this._hook = null + } +} + Object.defineProperty(Instrumentation.prototype, 'ids', { get () { const current = this.currentSpan || this.currentTransaction diff --git a/test/_agent.js b/test/_agent.js index ab23169b70..70c4e62a76 100644 --- a/test/_agent.js +++ b/test/_agent.js @@ -1,5 +1,17 @@ 'use strict' +// DEPRECATED: New tests should not use this wrapper. Instead using the +// real Agent directly, and its `agent.destroy()` method to clean up state +// and the end of tests. E.g.: +// +// const Agent = require('.../lib/agent') +// test('test name', t => { +// const agent = new Agent().start({ ... }) +// ... +// agent.destroy() +// t.end() +// }) + var Agent = require('../lib/agent') var symbols = require('../lib/symbols') diff --git a/test/_apm_server.js b/test/_apm_server.js deleted file mode 100644 index fa8baa15be..0000000000 --- a/test/_apm_server.js +++ /dev/null @@ -1,93 +0,0 @@ -'use strict' - -var assert = require('assert') -var EventEmitter = require('events') -var http = require('http') -var util = require('util') -var zlib = require('zlib') - -var getPort = require('get-port') -var ndjson = require('ndjson') - -var Agent = require('./_agent') - -var defaultAgentOpts = { - serviceName: 'some-service-name', - captureExceptions: false, - centralConfig: false, - logLevel: 'error' -} - -module.exports = APMServer - -util.inherits(APMServer, EventEmitter) - -function APMServer (agentOpts, mockOpts = { expect: [] }) { - if (!(this instanceof APMServer)) return new APMServer(agentOpts, mockOpts) - var self = this - - var requests = typeof mockOpts.expect === 'string' - ? ['metadata', mockOpts.expect] - : mockOpts.expect - - // ensure the expected types for each unique request to the APM Server is - // nested in it's own array - requests = Array.isArray(requests[0]) ? requests : [requests] - - this.agent = Agent() - - this.destroy = function () { - this.emit('close') - this.server.close() - this.agent.destroy() - } - - EventEmitter.call(this) - - getPort().then(function (port) { - self.agent.start(Object.assign( - {}, - defaultAgentOpts, - { serverUrl: 'http://localhost:' + port }, - agentOpts - )) - - var server = self.server = http.createServer(function (req, res) { - assert.strictEqual(req.method, 'POST', `Unexpected HTTP method: ${req.method}`) - assert.strictEqual(req.url, '/intake/v2/events', `Unexpected HTTP url: ${req.url}`) - - self.emit('request', req, res) - var expect = requests.shift() - var index = 0 - - var parsedStream = req.pipe(zlib.createGunzip()).pipe(ndjson.parse()) - parsedStream.on('data', function (data) { - assert.strictEqual(Object.keys(data).length, 1, `Expected number of root properties: ${Object.keys(data)}`) - - var type = Object.keys(data)[0] - - if (index === 0 && type !== 'metadata') assert.fail(`Unexpected data type at metadata index: ${type}`) - if (index !== 0 && type === 'metadata') assert.fail(`Unexpected metadata index: ${index}`) - if (expect) assert.strictEqual(type, expect.shift(), `Unexpected type '${type}' at index ${index}`) - - self.emit('data', data, index) - self.emit('data-' + type, data[type], index) - - index++ - }) - parsedStream.on('end', function () { - res.writeHead(202) - res.end('{}') - }) - }) - - self.emit('server', server) - - server.listen(port, function () { - self.emit('listening', port) - }) - }).catch(err => { - console.error(err.stack) - process.exit(1) - }) -} diff --git a/test/_mock_apm_server.js b/test/_mock_apm_server.js index 91c39dba28..0fe0b4ca8e 100644 --- a/test/_mock_apm_server.js +++ b/test/_mock_apm_server.js @@ -6,6 +6,8 @@ // // Test code using `serverUrl`... // // - Events received on the intake API will be on `server.events`. // // - Raw request data is on `server.requests`. +// // - Use `server.clear()` to clear `server.events` and `server.requests` +// // for re-use of the mock server in multiple test cases. // // - Call `server.close()` when done. // }) @@ -15,12 +17,16 @@ const zlib = require('zlib') class MockAPMServer { constructor () { - this.events = [] - this.requests = [] + this.clear() this.serverUrl = null // set in .start() this._http = http.createServer(this._onRequest.bind(this)) } + clear () { + this.events = [] + this.requests = [] + } + _onRequest (req, res) { var parsedUrl = new URL(req.url, this.serverUrl) var instream = req diff --git a/test/agent.test.js b/test/agent.test.js index f58f2bf9c9..d3d07660fc 100644 --- a/test/agent.test.js +++ b/test/agent.test.js @@ -1,43 +1,121 @@ 'use strict' +// Test the public Agent API. +// +// This test file does not rely on automatic instrumentation of modules, so +// we do not need to start the agent at the top of file. Instead, tests create +// separate instances of the Agent. + var http = require('http') var path = require('path') var os = require('os') var { sync: containerInfo } = require('container-info') -var isError = require('core-util-is').isError var test = require('tape') -var Agent = require('./_agent') -var APMServer = require('./_apm_server') +const Agent = require('../lib/agent') var config = require('../lib/config') - +const { MockAPMServer } = require('./_mock_apm_server') +const { NoopTransport } = require('../lib/noop-transport') var packageJson = require('../package.json') + var inContainer = 'containerId' in (containerInfo() || {}) -process.env.ELASTIC_APM_METRICS_INTERVAL = '0' -process.env.ELASTIC_APM_CENTRAL_CONFIG = 'false' +// Options to pass to `agent.start()` to turn off some default agent behavior +// that is unhelpful for these tests. +const agentOpts = { + serviceName: 'test-agent', + centralConfig: false, + captureExceptions: false, + metricsInterval: '0s', + cloudProvider: 'none', + spanFramesMinDuration: -1, // Never discard fast spans. + logLevel: 'warn' +} +const agentOptsNoopTransport = Object.assign( + {}, + agentOpts, + { + transport: function createNoopTransport () { + // Avoid accidentally trying to send data to an APM server. + return new NoopTransport() + } + } +) + +// ---- internal support functions + +function assertMetadata (t, payload) { + t.strictEqual(payload.service.name, 'test-agent') + t.deepEqual(payload.service.runtime, { name: 'node', version: process.versions.node }) + t.deepEqual(payload.service.agent, { name: 'nodejs', version: packageJson.version }) + + const expectedSystemKeys = ['hostname', 'architecture', 'platform'] + if (inContainer) expectedSystemKeys.push('container') + + t.deepEqual(Object.keys(payload.system), expectedSystemKeys) + t.strictEqual(payload.system.hostname, os.hostname()) + t.strictEqual(payload.system.architecture, process.arch) + t.strictEqual(payload.system.platform, process.platform) + + if (inContainer) { + t.deepEqual(Object.keys(payload.system.container), ['id']) + t.strictEqual(typeof payload.system.container.id, 'string') + t.ok(/^[\da-f]{64}$/.test(payload.system.container.id)) + } + + t.ok(payload.process) + t.strictEqual(payload.process.pid, process.pid) + t.ok(payload.process.pid > 0, 'should have a pid greater than 0') + t.ok(payload.process.title, 'should have a process title') + t.strictEqual(payload.process.title, process.title) + t.deepEqual(payload.process.argv, process.argv) + t.ok(payload.process.argv.length >= 2, 'should have at least two process arguments') +} + +function assertStackTrace (t, stacktrace) { + t.ok(stacktrace !== undefined, 'should have a stack trace') + t.ok(Array.isArray(stacktrace), 'stack trace should be an array') + t.ok(stacktrace.length > 0, 'stack trace should have at least one frame') + t.strictEqual(stacktrace[0].filename, path.join('test', 'agent.test.js')) +} + +function deep (depth, n) { + if (!n) n = 0 + if (n < depth) return deep(depth, ++n) + return new Error() +} + +// ---- tests test('#getServiceName()', function (t) { - var agent = Agent() + const agent = new Agent() - // Before agent.start() config will have already been loaded once, which + // Before agent.start(), config will have already been loaded once, which // typically means a `serviceName` determined from package.json. + t.ok(!agent.isStarted(), 'agent should not have been started yet') t.strictEqual(agent.getServiceName(), packageJson.name) t.strictEqual(agent.getServiceName(), agent._conf.serviceName) // After agent.start() config will be loaded again, this time with possible // provided config. - agent.start({ serviceName: 'myServiceName' }) + agent.start(Object.assign( + {}, + agentOptsNoopTransport, + { serviceName: 'myServiceName' } + )) t.strictEqual(agent.getServiceName(), 'myServiceName') t.strictEqual(agent.getServiceName(), agent._conf.serviceName) + agent.destroy() t.end() }) test('#setFramework()', function (t) { - var agent = Agent() - agent.start() + // Use `agentOpts` instead of `agentOptsNoopTransport` because this test is + // reaching into `agent._transport` internals. + const agent = new Agent().start(agentOpts) + t.strictEqual(agent._conf.frameworkName, undefined) t.strictEqual(agent._conf.frameworkVersion, undefined) t.strictEqual(agent._transport._conf.frameworkName, undefined) @@ -67,36 +145,36 @@ test('#setFramework()', function (t) { t.strictEqual(agent._conf.frameworkVersion, 'b') t.strictEqual(agent._transport._conf.frameworkName, 'a') t.strictEqual(agent._transport._conf.frameworkVersion, 'b') + agent.destroy() t.end() }) test('#startTransaction()', function (t) { t.test('name, type, subtype and action', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction('foo', 'type', 'subtype', 'action') t.strictEqual(trans.name, 'foo') t.strictEqual(trans.type, 'type') t.strictEqual(trans.subtype, 'subtype') t.strictEqual(trans.action, 'action') + agent.destroy() t.end() }) t.test('options.startTime', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var startTime = Date.now() - 1000 var trans = agent.startTransaction('foo', 'bar', { startTime }) trans.end() var duration = trans.duration() t.ok(duration > 990, `duration should be circa more than 1s (was: ${duration})`) // we've seen 998.752 in the wild t.ok(duration < 1100, `duration should be less than 1.1s (was: ${duration})`) + agent.destroy() t.end() }) t.test('options.childOf', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var childOf = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' var trans = agent.startTransaction('foo', 'bar', { childOf }) t.strictEqual(trans._context.traceparent.version, '00') @@ -104,144 +182,152 @@ test('#startTransaction()', function (t) { t.notEqual(trans._context.traceparent.id, '00f067aa0ba902b7') t.strictEqual(trans._context.traceparent.parentId, '00f067aa0ba902b7') t.strictEqual(trans._context.traceparent.flags, '01') + agent.destroy() t.end() }) + + t.end() }) test('#endTransaction()', function (t) { t.test('no active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) agent.endTransaction() + agent.destroy() t.end() }) t.test('with no result', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(trans.ended, false) agent.endTransaction() t.strictEqual(trans.ended, true) t.strictEqual(trans.result, 'success') + agent.destroy() t.end() }) t.test('with explicit result', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(trans.ended, false) agent.endTransaction('done') t.strictEqual(trans.ended, true) t.strictEqual(trans.result, 'done') + agent.destroy() t.end() }) t.test('with custom endTime', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var startTime = Date.now() - 1000 var endTime = startTime + 2000.123 var trans = agent.startTransaction('foo', 'bar', { startTime }) agent.endTransaction('done', endTime) t.strictEqual(trans.duration(), 2000.123) + agent.destroy() t.end() }) + + t.end() }) test('#currentTransaction', function (t) { t.test('no active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) t.notOk(agent.currentTransaction) + agent.destroy() t.end() }) t.test('with active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.currentTransaction, trans) agent.endTransaction() + agent.destroy() t.end() }) }) test('#currentSpan', function (t) { t.test('no active or binding span', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) t.notOk(agent.currentSpan) + agent.destroy() t.end() }) t.test('with binding span', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() var span = agent.startSpan() t.strictEqual(agent.currentSpan, span) span.end() trans.end() + agent.destroy() t.end() }) t.test('with active span', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() var span = agent.startSpan() process.nextTick(() => { t.strictEqual(agent.currentSpan, span) span.end() trans.end() + agent.destroy() t.end() }) }) + + t.end() }) test('#currentTraceparent', function (t) { t.test('no active transaction or span', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) t.notOk(agent.currentTraceparent) + agent.destroy() t.end() }) t.test('with active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.currentTraceparent, trans.traceparent) agent.endTransaction() + agent.destroy() t.end() }) t.test('with active span', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) agent.startTransaction() var span = agent.startSpan() t.strictEqual(agent.currentTraceparent, span.traceparent) span.end() agent.endTransaction() + agent.destroy() t.end() }) + + t.end() }) test('#currentTraceIds', function (t) { t.test('no active transaction or span', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) t.deepLooseEqual(agent.currentTraceIds, {}) t.strictEqual(agent.currentTraceIds.toString(), '') + agent.destroy() t.end() }) t.test('with active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.deepLooseEqual(agent.currentTraceIds, { 'trace.id': trans.traceId, @@ -249,12 +335,12 @@ test('#currentTraceIds', function (t) { }) t.strictEqual(agent.currentTraceIds.toString(), `trace.id=${trans.traceId} transaction.id=${trans.id}`) agent.endTransaction() + agent.destroy() t.end() }) t.test('with active span', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) agent.startTransaction() var span = agent.startSpan() t.deepLooseEqual(agent.currentTraceIds, { @@ -264,41 +350,45 @@ test('#currentTraceIds', function (t) { t.strictEqual(agent.currentTraceIds.toString(), `trace.id=${span.traceId} span.id=${span.id}`) span.end() agent.endTransaction() + agent.destroy() t.end() }) + + t.end() }) test('#setTransactionName', function (t) { t.test('no active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) t.doesNotThrow(function () { agent.setTransactionName('foo') }) + agent.destroy() t.end() }) t.test('active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() agent.setTransactionName('foo') t.strictEqual(trans.name, 'foo') + agent.destroy() t.end() }) + + t.end() }) test('#startSpan()', function (t) { t.test('no active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) t.strictEqual(agent.startSpan(), null) + agent.destroy() t.end() }) t.test('active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) agent.startTransaction() var span = agent.startSpan('span-name', 'type', 'subtype', 'action') t.ok(span, 'should return a span') @@ -306,25 +396,25 @@ test('#startSpan()', function (t) { t.strictEqual(span.type, 'type') t.strictEqual(span.subtype, 'subtype') t.strictEqual(span.action, 'action') + agent.destroy() t.end() }) t.test('options.startTime', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) agent.startTransaction() var startTime = Date.now() - 1000 - var span = agent.startSpan(null, null, { startTime }) + var span = agent.startSpan('span-with-startTime', null, { startTime }) span.end() var duration = span.duration() t.ok(duration > 990, `duration should be circa more than 1s (was: ${duration})`) // we've seen 998.752 in the wild t.ok(duration < 1100, `duration should be less than 1.1s (was: ${duration})`) + agent.destroy() t.end() }) t.test('options.childOf', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) agent.startTransaction() var childOf = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' var span = agent.startSpan(null, null, { childOf }) @@ -333,66 +423,72 @@ test('#startSpan()', function (t) { t.notEqual(span._context.traceparent.id, '00f067aa0ba902b7') t.strictEqual(span._context.traceparent.parentId, '00f067aa0ba902b7') t.strictEqual(span._context.traceparent.flags, '01') + agent.destroy() t.end() }) + + t.end() }) test('#setUserContext()', function (t) { t.test('no active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) t.strictEqual(agent.setUserContext({ foo: 1 }), false) + agent.destroy() t.end() }) t.test('active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.setUserContext({ foo: 1 }), true) t.deepEqual(trans._user, { foo: 1 }) + agent.destroy() t.end() }) + + t.end() }) test('#setCustomContext()', function (t) { t.test('no active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) t.strictEqual(agent.setCustomContext({ foo: 1 }), false) + agent.destroy() t.end() }) t.test('active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.setCustomContext({ foo: 1 }), true) t.deepEqual(trans._custom, { foo: 1 }) + agent.destroy() t.end() }) + + t.end() }) test('#setLabel()', function (t) { t.test('no active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) t.strictEqual(agent.setLabel('foo', 1), false) + agent.destroy() t.end() }) t.test('active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.setLabel('foo', 1), true) t.deepEqual(trans._labels, { foo: '1' }) + agent.destroy() t.end() }) t.test('active transaction without label stringification', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.setLabel('positive', 1, false), true) t.strictEqual(agent.setLabel('negative', -10, false), true) @@ -406,753 +502,868 @@ test('#setLabel()', function (t) { 'boolean-false': false, string: 'a custom label' }) + agent.destroy() t.end() }) + + t.end() }) test('#addLabels()', function (t) { t.test('no active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) t.strictEqual(agent.addLabels({ foo: 1 }), false) + agent.destroy() t.end() }) t.test('active transaction', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.addLabels({ foo: 1, bar: 2 }), true) t.strictEqual(agent.addLabels({ foo: 3 }), true) t.deepEqual(trans._labels, { foo: '3', bar: '2' }) + agent.destroy() t.end() }) t.test('active transaction without label stringification', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.addLabels({ foo: 1, bar: true }, false), true) t.deepEqual(trans._labels, { foo: 1, bar: true }) + agent.destroy() t.end() }) + + t.end() }) test('filters', function (t) { + let apmServer + let filterAgentOpts + + t.test('setup mock APM server', function (t) { + apmServer = new MockAPMServer() + apmServer.start(function (serverUrl) { + t.comment('mock APM serverUrl: ' + serverUrl) + filterAgentOpts = Object.assign( + {}, + agentOpts, + { serverUrl } + ) + t.end() + }) + }) + t.test('#addFilter() - error', function (t) { - t.plan(6 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'error' }) - .on('listening', function () { - this.agent.addFilter(function (obj) { - t.strictEqual(obj.exception.message, 'foo') - t.strictEqual(++obj.context.custom.order, 1) - return obj - }) - this.agent.addFilter('invalid') - this.agent.addFilter(function (obj) { - t.strictEqual(obj.exception.message, 'foo') - t.strictEqual(++obj.context.custom.order, 2) - return obj - }) + const agent = new Agent().start(filterAgentOpts) + // Test filters are run in the order specified... + agent.addFilter(function (obj) { + t.strictEqual(obj.exception.message, 'foo') + t.strictEqual(++obj.context.custom.order, 1) + return obj + }) + // ... and that an invalid filter (not a function) is handled. + agent.addFilter('invalid') + agent.addFilter(function (obj) { + t.strictEqual(obj.exception.message, 'foo') + t.strictEqual(++obj.context.custom.order, 2) + return obj + }) - this.agent.captureError(new Error('foo'), { custom: { order: 0 } }) - }) - .on('data-error', function (data) { + agent.captureError( + new Error('foo'), + { custom: { order: 0 } }, + function (err) { + t.error(err, 'captureError should not fail') + t.equal(apmServer.events.length, 2, 'got 2 events') + t.ok(apmServer.events[0].metadata, 'event 0 is metadata') + assertMetadata(t, apmServer.events[0].metadata) + const data = apmServer.events[1].error + t.ok(data, 'event 1 is an error') t.strictEqual(data.exception.message, 'foo') t.strictEqual(data.context.custom.order, 2) + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('#addFilter() - transaction', function (t) { - t.plan(6 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'transaction' }) - .on('listening', function () { - this.agent.addFilter(function (obj) { - t.strictEqual(obj.name, 'transaction-name') - t.strictEqual(++obj.context.custom.order, 1) - return obj - }) - this.agent.addFilter('invalid') - this.agent.addFilter(function (obj) { - t.strictEqual(obj.name, 'transaction-name') - t.strictEqual(++obj.context.custom.order, 2) - return obj - }) + const agent = new Agent().start(filterAgentOpts) + agent.addFilter(function (obj) { + t.strictEqual(obj.name, 'transaction-name') + t.strictEqual(++obj.context.custom.order, 1) + return obj + }) + agent.addFilter('invalid') + agent.addFilter(function (obj) { + t.strictEqual(obj.name, 'transaction-name') + t.strictEqual(++obj.context.custom.order, 2) + return obj + }) - this.agent.startTransaction('transaction-name') - this.agent.setCustomContext({ order: 0 }) - this.agent.endTransaction() - this.agent.flush() - }) - .on('data-transaction', function (data) { - t.strictEqual(data.name, 'transaction-name') - t.strictEqual(data.context.custom.order, 2) - t.end() - }) + agent.startTransaction('transaction-name') + agent.setCustomContext({ order: 0 }) + agent.endTransaction() + agent.flush(function () { + t.equal(apmServer.events.length, 2, 'got 2 events') + t.ok(apmServer.events[0].metadata, 'event 0 is metadata') + assertMetadata(t, apmServer.events[0].metadata) + const data = apmServer.events[1].transaction + t.ok(data, 'event 1 is a transaction') + t.strictEqual(data.name, 'transaction-name') + t.strictEqual(data.context.custom.order, 2) + + apmServer.clear() + agent.destroy() + t.end() + }) }) t.test('#addFilter() - span', function (t) { - t.plan(5 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'span' }) - .on('listening', function () { - this.agent.addFilter(function (obj) { - t.strictEqual(obj.name, 'span-name') - obj.order = 1 - return obj - }) - this.agent.addFilter('invalid') - this.agent.addFilter(function (obj) { - t.strictEqual(obj.name, 'span-name') - t.strictEqual(++obj.order, 2) - return obj - }) + const agent = new Agent().start(filterAgentOpts) + agent.addFilter(function (obj) { + t.strictEqual(obj.name, 'span-name') + obj.order = 1 + return obj + }) + agent.addFilter('invalid') + agent.addFilter(function (obj) { + t.strictEqual(obj.name, 'span-name') + t.strictEqual(++obj.order, 2) + return obj + }) - this.agent.startTransaction() - const span = this.agent.startSpan('span-name') - span.end() - setTimeout(() => { - this.agent.flush() - }, 50) - }) - .on('data-span', function (data) { + agent.startTransaction() + const span = agent.startSpan('span-name') + span.end() + setTimeout(() => { + agent.flush(function () { + t.equal(apmServer.events.length, 2, 'got 2 events') + t.ok(apmServer.events[0].metadata, 'event 0 is metadata') + assertMetadata(t, apmServer.events[0].metadata) + const data = apmServer.events[1].span + t.ok(data, 'event 1 is a span') t.strictEqual(data.name, 'span-name') t.strictEqual(data.order, 2) + + apmServer.clear() + agent.destroy() t.end() }) + }, 200) // Hack wait for ended span to be sent to transport. }) t.test('#addErrorFilter()', function (t) { - t.plan(6 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'error' }) - .on('listening', function () { - this.agent.addTransactionFilter(function () { - t.fail('should not call transaction filter') - }) - this.agent.addSpanFilter(function () { - t.fail('should not call span filter') - }) - this.agent.addErrorFilter(function (obj) { - t.strictEqual(obj.exception.message, 'foo') - t.strictEqual(++obj.context.custom.order, 1) - return obj - }) - this.agent.addErrorFilter('invalid') - this.agent.addErrorFilter(function (obj) { - t.strictEqual(obj.exception.message, 'foo') - t.strictEqual(++obj.context.custom.order, 2) - return obj - }) + const agent = new Agent().start(filterAgentOpts) + agent.addTransactionFilter(function () { + t.fail('should not call transaction filter') + }) + agent.addSpanFilter(function () { + t.fail('should not call span filter') + }) + agent.addErrorFilter(function (obj) { + t.strictEqual(obj.exception.message, 'foo') + t.strictEqual(++obj.context.custom.order, 1) + return obj + }) + agent.addErrorFilter('invalid') + agent.addErrorFilter(function (obj) { + t.strictEqual(obj.exception.message, 'foo') + t.strictEqual(++obj.context.custom.order, 2) + return obj + }) - this.agent.captureError(new Error('foo'), { custom: { order: 0 } }) - }) - .on('data-error', function (data) { + agent.captureError( + new Error('foo'), + { custom: { order: 0 } }, + function (err) { + t.error(err, 'captureError should not fail') + t.equal(apmServer.events.length, 2, 'got 2 events') + t.ok(apmServer.events[0].metadata, 'event 0 is metadata') + assertMetadata(t, apmServer.events[0].metadata) + const data = apmServer.events[1].error + t.ok(data, 'event 1 is an error') t.strictEqual(data.exception.message, 'foo') t.strictEqual(data.context.custom.order, 2) + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('#addTransactionFilter()', function (t) { - t.plan(6 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'transaction' }) - .on('listening', function () { - this.agent.addErrorFilter(function () { - t.fail('should not call error filter') - }) - this.agent.addSpanFilter(function () { - t.fail('should not call span filter') - }) - this.agent.addTransactionFilter(function (obj) { - t.strictEqual(obj.name, 'transaction-name') - t.strictEqual(++obj.context.custom.order, 1) - return obj - }) - this.agent.addTransactionFilter('invalid') - this.agent.addTransactionFilter(function (obj) { - t.strictEqual(obj.name, 'transaction-name') - t.strictEqual(++obj.context.custom.order, 2) - return obj - }) + const agent = new Agent().start(filterAgentOpts) + agent.addErrorFilter(function () { + t.fail('should not call error filter') + }) + agent.addSpanFilter(function () { + t.fail('should not call span filter') + }) + agent.addTransactionFilter(function (obj) { + t.strictEqual(obj.name, 'transaction-name') + t.strictEqual(++obj.context.custom.order, 1) + return obj + }) + agent.addTransactionFilter('invalid') + agent.addTransactionFilter(function (obj) { + t.strictEqual(obj.name, 'transaction-name') + t.strictEqual(++obj.context.custom.order, 2) + return obj + }) - this.agent.startTransaction('transaction-name') - this.agent.setCustomContext({ order: 0 }) - this.agent.endTransaction() - this.agent.flush() - }) - .on('data-transaction', function (data) { - t.strictEqual(data.name, 'transaction-name') - t.strictEqual(data.context.custom.order, 2) - t.end() - }) + agent.startTransaction('transaction-name') + agent.setCustomContext({ order: 0 }) + agent.endTransaction() + agent.flush(function () { + t.equal(apmServer.events.length, 2, 'got 2 events') + t.ok(apmServer.events[0].metadata, 'event 0 is metadata') + assertMetadata(t, apmServer.events[0].metadata) + const data = apmServer.events[1].transaction + t.ok(data, 'event 1 is a transaction') + t.strictEqual(data.name, 'transaction-name') + t.strictEqual(data.context.custom.order, 2) + + apmServer.clear() + agent.destroy() + t.end() + }) }) t.test('#addSpanFilter()', function (t) { - t.plan(5 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'span' }) - .on('listening', function () { - this.agent.addErrorFilter(function () { - t.fail('should not call error filter') - }) - this.agent.addTransactionFilter(function () { - t.fail('should not call transaction filter') - }) - this.agent.addSpanFilter(function (obj) { - t.strictEqual(obj.name, 'span-name') - obj.order = 1 - return obj - }) - this.agent.addSpanFilter('invalid') - this.agent.addSpanFilter(function (obj) { - t.strictEqual(obj.name, 'span-name') - t.strictEqual(++obj.order, 2) - return obj - }) + const agent = new Agent().start(filterAgentOpts) + agent.addErrorFilter(function () { + t.fail('should not call error filter') + }) + agent.addTransactionFilter(function () { + t.fail('should not call transaction filter') + }) + agent.addSpanFilter(function (obj) { + t.strictEqual(obj.name, 'span-name') + obj.order = 1 + return obj + }) + agent.addSpanFilter('invalid') + agent.addSpanFilter(function (obj) { + t.strictEqual(obj.name, 'span-name') + t.strictEqual(++obj.order, 2) + return obj + }) - this.agent.startTransaction() - const span = this.agent.startSpan('span-name') - span.end() - setTimeout(() => { - this.agent.flush() - }, 50) - }) - .on('data-span', function (data) { + agent.startTransaction() + const span = agent.startSpan('span-name') + span.end() + setTimeout(() => { + agent.flush(function () { + t.equal(apmServer.events.length, 2, 'got 2 events') + t.ok(apmServer.events[0].metadata, 'event 0 is metadata') + assertMetadata(t, apmServer.events[0].metadata) + const data = apmServer.events[1].span + t.ok(data, 'event 1 is a span') t.strictEqual(data.name, 'span-name') t.strictEqual(data.order, 2) + + apmServer.clear() + agent.destroy() t.end() }) + }, 200) // Hack wait for ended span to be sent to transport. }) t.test('#addMetadataFilter()', function (t) { - t.plan(5 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: ['metadata', 'transaction'] }) - .on('listening', function () { - this.agent.addErrorFilter(function () { - t.fail('should not call error filter') - }) - this.agent.addSpanFilter(function () { - t.fail('should not call span filter') - }) - this.agent.addMetadataFilter(function (obj) { - t.strictEqual(obj.service.agent.name, 'nodejs') - obj.order = 1 - return obj - }) - this.agent.addMetadataFilter('invalid') - this.agent.addMetadataFilter(function (obj) { - t.strictEqual(obj.service.agent.name, 'nodejs') - t.strictEqual(++obj.order, 2) - return obj - }) + const agent = new Agent().start(filterAgentOpts) + agent.addErrorFilter(function () { + t.fail('should not call error filter') + }) + agent.addSpanFilter(function () { + t.fail('should not call span filter') + }) + agent.addMetadataFilter(function (obj) { + t.strictEqual(obj.service.agent.name, 'nodejs') + obj.order = 1 + return obj + }) + agent.addMetadataFilter('invalid') + agent.addMetadataFilter(function (obj) { + t.strictEqual(obj.service.agent.name, 'nodejs') + t.strictEqual(++obj.order, 2) + return obj + }) - this.agent.startTransaction() - this.agent.endTransaction() - this.agent.flush() - }) - .on('data-metadata', function (metadata) { - t.strictEqual(metadata.service.agent.name, 'nodejs') - t.strictEqual(metadata.order, 2) - t.end() - }) + agent.startTransaction('transaction-name') + agent.endTransaction() + agent.flush(function () { + t.equal(apmServer.events.length, 2, 'got 2 events') + const data = apmServer.events[0].metadata + t.ok(data, 'event 0 is metadata') + assertMetadata(t, data) + t.strictEqual(data.service.agent.name, 'nodejs') + t.strictEqual(data.order, 2) + + apmServer.clear() + agent.destroy() + t.end() + }) }) const falsyValues = [undefined, null, false, 0, '', NaN] - falsyValues.forEach(falsy => { t.test(`#addFilter() - abort with '${String(falsy)}'`, function (t) { - t.plan(1) - - const server = http.createServer(function (req, res) { - t.fail('should not send any data') - }) - - server.listen(function () { - const agent = Agent().start({ serverUrl: 'http://localhost:' + server.address().port }) - - agent.addFilter(function (obj) { - t.strictEqual(obj.exception.message, 'foo') - return falsy - }) - agent.addFilter(function () { - t.fail('should not call 2nd filter') - }) - - agent.captureError(new Error('foo')) - - setTimeout(function () { - t.end() - server.close() - }, 200) + let calledFirstFilter = false + const agent = new Agent().start(filterAgentOpts) + agent.addFilter(function (obj) { + calledFirstFilter = true + return falsy + }) + agent.addFilter(function () { + t.fail('should not call 2nd filter') + }) + agent.captureError(new Error('foo'), function () { + t.ok(calledFirstFilter, 'called first filter') + t.equal(apmServer.requests.length, 0, 'APM server did not receive a request') + apmServer.clear() + agent.destroy() + t.end() }) }) t.test(`#addErrorFilter() - abort with '${String(falsy)}'`, function (t) { - t.plan(1) - - const server = http.createServer(function (req, res) { - t.fail('should not send any data') - }) - - server.listen(function () { - const agent = Agent().start({ - serverUrl: 'http://localhost:' + server.address().port, - captureExceptions: false - }) - - agent.addErrorFilter(function (obj) { - t.strictEqual(obj.exception.message, 'foo') - return falsy - }) - agent.addErrorFilter(function () { - t.fail('should not call 2nd filter') - }) - - agent.captureError(new Error('foo')) - - setTimeout(function () { - t.end() - server.close() - }, 50) + let calledFirstFilter = false + const agent = new Agent().start(filterAgentOpts) + agent.addErrorFilter(function (obj) { + calledFirstFilter = true + return falsy + }) + agent.addErrorFilter(function () { + t.fail('should not call 2nd filter') + }) + agent.captureError(new Error('foo'), function () { + t.ok(calledFirstFilter, 'called first filter') + t.equal(apmServer.requests.length, 0, 'APM server did not receive a request') + apmServer.clear() + agent.destroy() + t.end() }) }) t.test(`#addTransactionFilter() - abort with '${String(falsy)}'`, function (t) { - t.plan(1) - - const server = http.createServer(function (req, res) { - t.fail('should not send any data') - }) - - server.listen(function () { - const agent = Agent().start({ - serverUrl: 'http://localhost:' + server.address().port, - captureExceptions: false - }) - - agent.addTransactionFilter(function (obj) { - t.strictEqual(obj.name, 'transaction-name') - return falsy - }) - agent.addTransactionFilter(function () { - t.fail('should not call 2nd filter') - }) - - agent.startTransaction('transaction-name') - agent.endTransaction() - agent.flush() - - setTimeout(function () { - t.end() - server.close() - }, 50) + let calledFirstFilter = false + const agent = new Agent().start(filterAgentOpts) + agent.addTransactionFilter(function (obj) { + calledFirstFilter = true + return falsy + }) + agent.addTransactionFilter(function () { + t.fail('should not call 2nd filter') + }) + agent.startTransaction('transaction-name') + agent.endTransaction() + agent.flush(function () { + t.ok(calledFirstFilter, 'called first filter') + t.equal(apmServer.requests.length, 0, 'APM server did not receive a request') + apmServer.clear() + agent.destroy() + t.end() }) }) t.test(`#addSpanFilter() - abort with '${String(falsy)}'`, function (t) { - t.plan(1) - - const server = http.createServer(function (req, res) { - t.fail('should not send any data') + let calledFirstFilter = false + const agent = new Agent().start(filterAgentOpts) + agent.addSpanFilter(function (obj) { + calledFirstFilter = true + return falsy }) - - server.listen(function () { - const agent = Agent().start({ - serverUrl: 'http://localhost:' + server.address().port, - captureExceptions: false - }) - - agent.addSpanFilter(function (obj) { - t.strictEqual(obj.name, 'span-name') - return falsy - }) - agent.addSpanFilter(function () { - t.fail('should not call 2nd filter') - }) - - agent.startTransaction() - const span = agent.startSpan('span-name') - span.end() - - setTimeout(function () { - agent.flush() - setTimeout(function () { - t.end() - server.close() - }, 50) - }, 50) + agent.addSpanFilter(function () { + t.fail('should not call 2nd filter') }) + agent.startTransaction() + const span = agent.startSpan('span-name') + span.end() + setTimeout(function () { + agent.flush(function () { + t.ok(calledFirstFilter, 'called first filter') + t.equal(apmServer.requests.length, 0, 'APM server did not receive a request') + apmServer.clear() + agent.destroy() + t.end() + }) + }, 200) // Hack wait for ended span to be sent to transport. }) }) + + t.test('teardown mock APM server', function (t) { + apmServer.close() + t.end() + }) + + t.end() }) test('#flush()', function (t) { t.test('start not called', function (t) { t.plan(2) - var agent = Agent() + const agent = new Agent() agent.flush(function (err) { t.error(err, 'no error passed to agent.flush callback') t.pass('should call flush callback even if agent.start() wasn\'t called') + agent.destroy() t.end() }) }) t.test('start called, but agent inactive', function (t) { t.plan(2) - var agent = Agent() - agent.start({ active: false }) + const agent = new Agent().start({ active: false }) agent.flush(function (err) { t.error(err, 'no error passed to agent.flush callback') t.pass('should call flush callback even if agent is inactive') + agent.destroy() t.end() }) }) t.test('agent started, but no data in the queue', function (t) { t.plan(2) - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) agent.flush(function (err) { t.error(err, 'no error passed to agent.flush callback') t.pass('should call flush callback even if there\'s nothing to flush') + agent.destroy() t.end() }) }) - t.test('agent started, but no data in the queue', function (t) { - t.plan(3 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'transaction' }) - .on('listening', function () { - this.agent.startTransaction('foo') - this.agent.endTransaction() - this.agent.flush(function (err) { - // 2. ... the flush callback should be called. - t.error(err, 'no error passed to agent.flush callback') - t.pass('should call flush callback after flushing the queue') - t.end() - }) - }) - .on('data-transaction', function (data) { - // 1. The APM server should receive the transaction, and then ... - t.strictEqual(data.name, 'foo', 'APM server received the "foo" transaction') + t.test('flush with agent started, and data in the queue', function (t) { + const apmServer = new MockAPMServer() + apmServer.start(function (serverUrl) { + const agent = new Agent().start(Object.assign( + {}, + agentOpts, + { serverUrl } + )) + agent.startTransaction('foo') + agent.endTransaction() + agent.flush(function (err) { + t.error(err, 'no error passed to agent.flush callback') + t.equal(apmServer.events.length, 2, 'apmServer got 2 events') + const trans = apmServer.events[1].transaction + t.ok(trans, 'event 1 is a transaction') + t.equal(trans.name, 'foo', 'the transaction has the expected name') + + apmServer.close() + agent.destroy() + t.end() }) + }) }) + + t.end() }) test('#captureError()', function (t) { + let apmServer + let ceAgentOpts + + t.test('setup mock APM server', function (t) { + apmServer = new MockAPMServer() + apmServer.start(function (serverUrl) { + t.comment('mock APM serverUrl: ' + serverUrl) + ceAgentOpts = Object.assign( + {}, + agentOpts, + { serverUrl } + ) + t.end() + }) + }) + t.test('with callback', function (t) { - t.plan(5 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError(new Error('with callback'), function (err, id) { - // 2. ... the captureError callback should be called. - t.pass('called captureError callback') - t.error(err, 'no error from captureError callback') - t.ok(/^[a-z0-9]{32}$/i.test(id), 'has valid error.id') - t.end() - }) - }) - .on('data-error', function (data) { - // 1. The APM server should receive the error, and then ... - t.pass('APM Server received the error') - t.strictEqual(data.exception.message, 'with callback') - }) + const agent = new Agent().start(ceAgentOpts) + agent.captureError(new Error('with callback'), function (err, id) { + t.error(err, 'no error from captureError callback') + t.ok(/^[a-z0-9]{32}$/i.test(id), 'has valid error.id') + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + assertMetadata(t, apmServer.events[0].metadata) + const data = apmServer.events[1].error + t.strictEqual(data.exception.message, 'with callback') + + apmServer.clear() + agent.destroy() + t.end() + }) }) t.test('without callback', function (t) { - t.plan(1 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError(new Error('without callback')) - }) - .on('data-error', function (data) { - t.strictEqual(data.exception.message, 'without callback') - t.end() - }) + const agent = new Agent().start(ceAgentOpts) + agent.captureError(new Error('without callback')) + setTimeout(function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + assertMetadata(t, apmServer.events[0].metadata) + const data = apmServer.events[1].error + t.strictEqual(data.exception.message, 'without callback') + + apmServer.clear() + agent.destroy() + t.end() + }, 200) // Hack wait for captured error to be encoded and sent. }) t.test('generate error id', function (t) { - t.plan(1 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError(new Error('foo')) - }) - .on('data-error', function (data) { - t.ok(/^[\da-f]{32}$/.test(data.id), `should have valid id (was: ${data.id})`) - t.end() - }) + const agent = new Agent().start(ceAgentOpts) + agent.captureError(new Error('foo'), function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error + t.ok(/^[a-z0-9]{32}$/i.test(data.id), 'has valid error.id') + + apmServer.clear() + agent.destroy() + t.end() + }) }) t.test('should send a plain text message to the server', function (t) { - t.plan(1 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError('Hey!') - }) - .on('data-error', function (data) { - t.strictEqual(data.log.message, 'Hey!') - t.end() - }) + const agent = new Agent().start(ceAgentOpts) + agent.captureError('Hey!', function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error + t.strictEqual(data.log.message, 'Hey!') + + apmServer.clear() + agent.destroy() + t.end() + }) }) t.test('should use `param_message` as well as `message` if given an object as 1st argument', function (t) { - t.plan(2 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError({ message: 'Hello %s', params: ['World'] }) - }) - .on('data-error', function (data) { + const agent = new Agent().start(ceAgentOpts) + agent.captureError({ message: 'Hello %s', params: ['World'] }, + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error t.strictEqual(data.log.message, 'Hello World') t.strictEqual(data.log.param_message, 'Hello %s') + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('should not fail on a non string err.message', function (t) { - t.plan(1 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'error' }) - .on('listening', function () { - var err = new Error() - err.message = { foo: 'bar' } - this.agent.captureError(err) - }) - .on('data-error', function (data) { - t.strictEqual(data.exception.message, '[object Object]') - t.end() - }) + const agent = new Agent().start(ceAgentOpts) + var err = new Error() + err.message = { foo: 'bar' } + agent.captureError(err, function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error + t.strictEqual(data.exception.message, '[object Object]') + + apmServer.clear() + agent.destroy() + t.end() + }) }) t.test('should allow custom log message together with exception', function (t) { - t.plan(2 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError(new Error('foo'), { message: 'bar' }) - }) - .on('data-error', function (data) { + const agent = new Agent().start(ceAgentOpts) + agent.captureError(new Error('foo'), { message: 'bar' }, + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error t.strictEqual(data.exception.message, 'foo') t.strictEqual(data.log.message, 'bar') + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('should adhere to default stackTraceLimit', function (t) { - t.plan(2 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError(deep(256)) - }) - .on('data-error', function (data) { - t.strictEqual(data.exception.stacktrace.length, 50) + const agent = new Agent().start(ceAgentOpts) + agent.captureError(deep(256), + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error + t.strictEqual(data.exception.stacktrace.length, config.DEFAULTS.stackTraceLimit) t.strictEqual(data.exception.stacktrace[0].context_line.trim(), 'return new Error()') + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('should adhere to custom stackTraceLimit', function (t) { - t.plan(2 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, { stackTraceLimit: 5 }, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError(deep(42)) - }) - .on('data-error', function (data) { + const agent = new Agent().start(Object.assign( + {}, + ceAgentOpts, + { stackTraceLimit: 5 } + )) + agent.captureError(deep(42), + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error t.strictEqual(data.exception.stacktrace.length, 5) t.strictEqual(data.exception.stacktrace[0].context_line.trim(), 'return new Error()') + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('should merge context', function (t) { - t.plan(4 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'error' }) - .on('listening', function () { - var agent = this.agent - var server = http.createServer(function (req, res) { - agent.startTransaction() - t.strictEqual(agent.setUserContext({ a: 1, merge: { a: 2 } }), true) - t.strictEqual(agent.setCustomContext({ a: 3, merge: { a: 4 } }), true) - agent.captureError(new Error('foo'), { user: { b: 1, merge: { shallow: true } }, custom: { b: 2, merge: { shallow: true } } }) - res.end() - }) - - server.listen(function () { - http.request({ - port: server.address().port - }, function (res) { - res.resume() - res.on('end', function () { - server.close() - }) - }).end() - }) - }) - .on('data-error', function (data) { + const agent = new Agent().start(ceAgentOpts) + agent.startTransaction() + t.strictEqual(agent.setUserContext({ a: 1, merge: { a: 2 } }), true) + t.strictEqual(agent.setCustomContext({ a: 3, merge: { a: 4 } }), true) + agent.captureError( + new Error('foo'), + { + user: { b: 1, merge: { shallow: true } }, + custom: { b: 2, merge: { shallow: true } } + }, + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error t.deepEqual(data.context.user, { a: 1, b: 1, merge: { shallow: true } }) t.deepEqual(data.context.custom, { a: 3, b: 2, merge: { shallow: true } }) + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('capture location stack trace - off (error)', function (t) { - t.plan(2 + APMServerWithDefaultAsserts.asserts + assertStackTrace.asserts) - APMServerWithDefaultAsserts(t, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_NEVER }, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError(new Error('foo')) - }) - .on('data-error', function (data) { + const agent = new Agent().start(Object.assign( + {}, + ceAgentOpts, + { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_NEVER } + )) + agent.captureError(new Error('foo'), + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error t.strictEqual(data.exception.message, 'foo') t.notOk('log' in data, 'should not have a log') assertStackTrace(t, data.exception.stacktrace) + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('capture location stack trace - off (string)', function (t) { - t.plan(3 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_NEVER }, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError('foo') - }) - .on('data-error', function (data) { + const agent = new Agent().start(Object.assign( + {}, + ceAgentOpts, + { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_NEVER } + )) + agent.captureError('foo', + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error t.strictEqual(data.log.message, 'foo') t.notOk('stacktrace' in data.log, 'should not have a log.stacktrace') t.notOk('exception' in data, 'should not have an exception') + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('capture location stack trace - off (param msg)', function (t) { - t.plan(3 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_NEVER }, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError({ message: 'Hello %s', params: ['World'] }) - }) - .on('data-error', function (data) { + const agent = new Agent().start(Object.assign( + {}, + ceAgentOpts, + { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_NEVER } + )) + agent.captureError({ message: 'Hello %s', params: ['World'] }, + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error t.strictEqual(data.log.message, 'Hello World') t.notOk('stacktrace' in data.log, 'should not have a log.stacktrace') t.notOk('exception' in data, 'should not have an exception') + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('capture location stack trace - non-errors (error)', function (t) { - t.plan(2 + APMServerWithDefaultAsserts.asserts + assertStackTrace.asserts) - APMServerWithDefaultAsserts(t, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES }, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError(new Error('foo')) - }) - .on('data-error', function (data) { + const agent = new Agent().start(Object.assign( + {}, + ceAgentOpts, + { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES } + )) + agent.captureError(new Error('foo'), + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error t.strictEqual(data.exception.message, 'foo') t.notOk('log' in data, 'should not have a log') assertStackTrace(t, data.exception.stacktrace) + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('capture location stack trace - non-errors (string)', function (t) { - t.plan(2 + APMServerWithDefaultAsserts.asserts + assertStackTrace.asserts) - APMServerWithDefaultAsserts(t, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES }, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError('foo') - }) - .on('data-error', function (data) { + const agent = new Agent().start(Object.assign( + {}, + ceAgentOpts, + { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES } + )) + agent.captureError('foo', + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error t.strictEqual(data.log.message, 'foo') t.notOk('exception' in data, 'should not have an exception') assertStackTrace(t, data.log.stacktrace) + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('capture location stack trace - non-errors (param msg)', function (t) { - t.plan(2 + APMServerWithDefaultAsserts.asserts + assertStackTrace.asserts) - APMServerWithDefaultAsserts(t, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES }, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError({ message: 'Hello %s', params: ['World'] }) - }) - .on('data-error', function (data) { + const agent = new Agent().start(Object.assign( + {}, + ceAgentOpts, + { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES } + )) + agent.captureError({ message: 'Hello %s', params: ['World'] }, + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error t.strictEqual(data.log.message, 'Hello World') t.notOk('exception' in data, 'should not have an exception') assertStackTrace(t, data.log.stacktrace) + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('capture location stack trace - all (error)', function (t) { - t.plan(2 + APMServerWithDefaultAsserts.asserts + assertStackTrace.asserts * 2) - APMServerWithDefaultAsserts(t, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS }, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError(new Error('foo')) - }) - .on('data-error', function (data) { + const agent = new Agent().start(Object.assign( + {}, + ceAgentOpts, + { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS } + )) + agent.captureError(new Error('foo'), + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error t.strictEqual(data.log.message, 'foo') t.strictEqual(data.exception.message, 'foo') assertStackTrace(t, data.log.stacktrace) assertStackTrace(t, data.exception.stacktrace) + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('capture location stack trace - all (string)', function (t) { - t.plan(2 + APMServerWithDefaultAsserts.asserts + assertStackTrace.asserts) - APMServerWithDefaultAsserts(t, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS }, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError('foo') - }) - .on('data-error', function (data) { + const agent = new Agent().start(Object.assign( + {}, + ceAgentOpts, + { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS } + )) + agent.captureError('foo', + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error t.strictEqual(data.log.message, 'foo') t.notOk('exception' in data, 'should not have an exception') assertStackTrace(t, data.log.stacktrace) + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('capture location stack trace - all (param msg)', function (t) { - t.plan(2 + APMServerWithDefaultAsserts.asserts + assertStackTrace.asserts) - APMServerWithDefaultAsserts(t, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS }, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError({ message: 'Hello %s', params: ['World'] }) - }) - .on('data-error', function (data) { + const agent = new Agent().start(Object.assign( + {}, + ceAgentOpts, + { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS } + )) + agent.captureError({ message: 'Hello %s', params: ['World'] }, + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + const data = apmServer.events[1].error t.strictEqual(data.log.message, 'Hello World') t.notOk('exception' in data, 'should not have an exception') assertStackTrace(t, data.log.stacktrace) + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('capture error before agent is started - with callback', function (t) { - var agent = Agent() + const agent = new Agent() agent.captureError(new Error('foo'), function (err) { t.strictEqual(err.message, 'cannot capture error before agent is started') + agent.destroy() t.end() }) }) t.test('capture error before agent is started - without callback', function (t) { - var agent = Agent() + const agent = new Agent() agent.captureError(new Error('foo')) + agent.destroy() t.end() }) t.test('include valid context ids and sampled flag', function (t) { - t.plan(9 + APMServerWithDefaultAsserts.asserts) - - let trans = null - let span = null - const expect = [ - 'metadata', - 'error' - ] - - APMServerWithDefaultAsserts(t, {}, { expect }) - .on('listening', function () { - trans = this.agent.startTransaction('foo') - span = this.agent.startSpan('bar') - this.agent.captureError(new Error('with callback'), function () { - t.pass('captureError callback was called') - t.end() - }) - }) - .on('data-error', function (data) { - t.pass('APM server received the error') + const agent = new Agent().start(ceAgentOpts) + const trans = agent.startTransaction('foo') + const span = agent.startSpan('bar') + agent.captureError( + new Error('with callback'), + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + assertMetadata(t, apmServer.events[0].metadata) + const data = apmServer.events[1].error t.strictEqual(data.exception.message, 'with callback') t.strictEqual(data.id.length, 32, 'id is 32 characters') t.strictEqual(data.parent_id, span.id, 'parent_id matches span id') @@ -1160,30 +1371,35 @@ test('#captureError()', function (t) { t.strictEqual(data.transaction_id, trans.id, 'transaction_id matches transaction id') t.strictEqual(data.transaction.type, trans.type, 'transaction.type matches transaction type') t.strictEqual(data.transaction.sampled, true, 'is sampled') - }) + + apmServer.clear() + agent.destroy() + t.end() + } + ) }) t.test('custom timestamp', function (t) { - t.plan(1 + APMServerWithDefaultAsserts.asserts) - + const agent = new Agent().start(ceAgentOpts) const timestamp = Date.now() - 1000 - const expect = [ - 'metadata', - 'error' - ] - - APMServerWithDefaultAsserts(t, {}, { expect }) - .on('listening', function () { - this.agent.captureError(new Error('with callback'), { timestamp }) - }) - .on('data-error', function (data) { + agent.captureError( + new Error('with callback'), + { timestamp }, + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + assertMetadata(t, apmServer.events[0].metadata) + const data = apmServer.events[1].error t.strictEqual(data.timestamp, timestamp * 1000) + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('options.request', function (t) { - t.plan(2 + APMServerWithDefaultAsserts.asserts) + const agent = new Agent().start(ceAgentOpts) const req = new http.IncomingMessage() req.httpVersion = '1.1' @@ -1197,11 +1413,13 @@ test('#captureError()', function (t) { req.headers.password = 'this should be redacted' // testing sanitizeFieldNames req.body = 'test' - APMServerWithDefaultAsserts(t, {}, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError(new Error('with request'), { request: req }) - }) - .on('data-error', function (data) { + agent.captureError( + new Error('with request'), + { request: req }, + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + assertMetadata(t, apmServer.events[0].metadata) + const data = apmServer.events[1].error t.strictEqual(data.exception.message, 'with request') t.deepEqual(data.context.request, { http_version: '1.1', @@ -1217,14 +1435,22 @@ test('#captureError()', function (t) { }, body: '[REDACTED]' }) + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) // This tests that a urlencoded request body captured in an *error* event // is properly sanitized according to sanitizeFieldNames. t.test('options.request + captureBody=errors', function (t) { - t.plan(2 + APMServerWithDefaultAsserts.asserts) + const agent = new Agent().start(Object.assign( + {}, + ceAgentOpts, + { captureBody: 'errors' } + )) const req = new http.IncomingMessage() req.httpVersion = '1.1' @@ -1236,11 +1462,13 @@ test('#captureError()', function (t) { req.headers['content-length'] = String(bodyLen) req.headers['content-type'] = 'application/x-www-form-urlencoded' - APMServerWithDefaultAsserts(t, { captureBody: 'errors' }, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError(new Error('with request'), { request: req }) - }) - .on('data-error', function (data) { + agent.captureError( + new Error('with request'), + { request: req }, + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + assertMetadata(t, apmServer.events[0].metadata) + const data = apmServer.events[1].error t.strictEqual(data.exception.message, 'with request') t.deepEqual(data.context.request, { http_version: '1.1', @@ -1253,12 +1481,16 @@ test('#captureError()', function (t) { }, body: 'foo=bar&password=' + encodeURIComponent('[REDACTED]') }) + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) }) t.test('options.response', function (t) { - t.plan(2 + APMServerWithDefaultAsserts.asserts) + const agent = new Agent().start(ceAgentOpts) const req = new http.IncomingMessage() const res = new http.ServerResponse(req) @@ -1271,11 +1503,13 @@ test('#captureError()', function (t) { password: 'this should be redacted' // testing sanitizeFieldNames } - APMServerWithDefaultAsserts(t, {}, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError(new Error('with response'), { response: res }) - }) - .on('data-error', function (data) { + agent.captureError( + new Error('with response'), + { response: res }, + function () { + t.equal(apmServer.events.length, 2, 'APM server got 2 events') + assertMetadata(t, apmServer.events[0].metadata) + const data = apmServer.events[1].error t.strictEqual(data.exception.message, 'with response') t.deepEqual(data.context.response, { status_code: 204, @@ -1289,81 +1523,107 @@ test('#captureError()', function (t) { headers_sent: false, finished: false }) + + apmServer.clear() + agent.destroy() t.end() - }) + } + ) + }) + + t.test('teardown mock APM server', function (t) { + apmServer.close() + t.end() }) + + t.end() }) test('#handleUncaughtExceptions()', function (t) { t.test('should add itself to the uncaughtException event list', function (t) { - var agent = Agent() t.strictEqual(process._events.uncaughtException, undefined) - agent.start({ - serviceName: 'some-service-name', - captureExceptions: false, - logLevel: 'error' - }) + const agent = new Agent().start(agentOptsNoopTransport) + t.strictEqual(process._events.uncaughtException, undefined) agent.handleUncaughtExceptions() t.strictEqual(process._events.uncaughtException.length, 1) + + agent.destroy() t.end() }) t.test('should not add more than one listener for the uncaughtException event', function (t) { - var agent = Agent() - agent.start({ - serviceName: 'some-service-name', - captureExceptions: false, - logLevel: 'error' - }) + const agent = new Agent().start(agentOptsNoopTransport) agent.handleUncaughtExceptions() var before = process._events.uncaughtException.length agent.handleUncaughtExceptions() t.strictEqual(process._events.uncaughtException.length, before) + + agent.destroy() t.end() }) t.test('should send an uncaughtException to server', function (t) { - t.plan(4 + APMServerWithDefaultAsserts.asserts) - APMServerWithDefaultAsserts(t, {}, { expect: 'error' }) - .on('listening', function () { - this.agent.handleUncaughtExceptions(function (err) { - t.pass('handleUncaughtExceptions callback was called') - t.ok(isError(err)) + const apmServer = new MockAPMServer() + apmServer.start(function (serverUrl) { + const agent = new Agent().start(Object.assign( + {}, + agentOpts, + { serverUrl } + )) + + let handlerErr + agent.handleUncaughtExceptions(function (err) { + handlerErr = err + }) + + process.emit('uncaughtException', new Error('uncaught')) + + setTimeout(() => { + agent.flush(function () { + t.equal(apmServer.events.length, 2, 'apmServer got 2 events') + assertMetadata(t, apmServer.events[0].metadata) + const data = apmServer.events[1].error + t.strictEqual(data.exception.message, 'uncaught') + + t.ok(handlerErr, 'the registered uncaughtException handler was called') + t.equal(handlerErr.message, 'uncaught') + + apmServer.close() + agent.destroy() t.end() }) - process.emit('uncaughtException', new Error('uncaught')) - }) - .on('data-error', function (data) { - t.pass('APM server received the error for the uncaughtException') - t.strictEqual(data.exception.message, 'uncaught') - }) + }, 200) // Hack wait for the agent's handler to finish captureError. + }) }) + + t.end() }) test('#active: false', function (t) { t.test('should not error when started in an inactive state', function (t) { - var agent = Agent() - var client = agent.start({ active: false }) - t.ok(client.startTransaction()) - t.doesNotThrow(() => client.endTransaction()) + const agent = new Agent().start({ active: false }) + t.ok(agent.startTransaction()) + t.doesNotThrow(() => agent.endTransaction()) + agent.destroy() t.end() }) }) test('patches', function (t) { t.test('#clearPatches(name)', function (t) { - var agent = Agent() + const agent = new Agent() t.ok(agent._instrumentation._patches.has('express')) t.doesNotThrow(() => agent.clearPatches('express')) t.notOk(agent._instrumentation._patches.has('express')) t.doesNotThrow(() => agent.clearPatches('does-not-exists')) + agent.destroy() t.end() }) t.test('#addPatch(name, moduleName)', function (t) { - var agent = Agent() + const agent = new Agent() agent.clearPatches('express') - agent.start() + agent.start(agentOptsNoopTransport) agent.addPatch('express', './test/_patch.js') @@ -1373,13 +1633,14 @@ test('patches', function (t) { delete require.cache[require.resolve('express')] t.deepEqual(require('express'), patch(before)) + agent.destroy() t.end() }) t.test('#addPatch(name, function) - does not exist', function (t) { - var agent = Agent() + const agent = new Agent() agent.clearPatches('express') - agent.start() + agent.start(agentOptsNoopTransport) var replacement = { foo: 'bar' @@ -1396,12 +1657,12 @@ test('patches', function (t) { delete require.cache[require.resolve('express')] t.deepEqual(require('express'), replacement) + agent.destroy() t.end() }) t.test('#removePatch(name, handler)', function (t) { - var agent = Agent() - agent.start() + const agent = new Agent().start(agentOptsNoopTransport) t.notOk(agent._instrumentation._patches.has('does-not-exist')) @@ -1416,136 +1677,55 @@ test('patches', function (t) { agent.removePatch('does-not-exist', handler) t.notOk(agent._instrumentation._patches.has('does-not-exist')) + agent.destroy() t.end() }) +}) - t.test('#registerMetric(name, labels, callback)', function (t) { - var agent = Agent() - agent.start() - - const mockMetrics = { - calledCount: 0, - callback: null, - cbValue: 0, - labels: null, - name: null, - getOrCreateGauge: function (...args) { - this.calledCount++ - this.name = args[0] - this.callback = args[1] - this.labels = args[2] - this.cbValue = this.callback() - } +test('#registerMetric(name, labels, callback)', function (t) { + const agent = new Agent().start(agentOptsNoopTransport) + + const mockMetrics = { + calledCount: 0, + callback: null, + cbValue: 0, + labels: null, + name: null, + getOrCreateGauge (...args) { + this.calledCount++ + this.name = args[0] + this.callback = args[1] + this.labels = args[2] + this.cbValue = this.callback() + }, + stop () { } + } - agent._metrics = mockMetrics + agent._metrics = mockMetrics - const cb = () => { return 12345 } - const labels = { abc: 123 } + const cb = () => { return 12345 } + const labels = { abc: 123 } - // with labels - agent.registerMetric('custom-metrics', labels, cb) + // with labels + agent.registerMetric('custom-metrics', labels, cb) - t.strictEqual(mockMetrics.calledCount, 1) - t.strictEqual(mockMetrics.name, 'custom-metrics') - t.strictEqual(mockMetrics.callback, cb) - t.strictEqual(mockMetrics.labels, labels) - t.strictEqual(mockMetrics.cbValue, 12345) + t.strictEqual(mockMetrics.calledCount, 1) + t.strictEqual(mockMetrics.name, 'custom-metrics') + t.strictEqual(mockMetrics.callback, cb) + t.strictEqual(mockMetrics.labels, labels) + t.strictEqual(mockMetrics.cbValue, 12345) - // without labels - const cb2 = () => { return 6789 } - agent.registerMetric('custom-metrics2', cb2) + // without labels + const cb2 = () => { return 6789 } + agent.registerMetric('custom-metrics2', cb2) - t.strictEqual(mockMetrics.calledCount, 2) - t.strictEqual(mockMetrics.name, 'custom-metrics2') - t.strictEqual(mockMetrics.callback, cb2) - t.strictEqual(mockMetrics.labels, undefined) - t.strictEqual(mockMetrics.cbValue, 6789) + t.strictEqual(mockMetrics.calledCount, 2) + t.strictEqual(mockMetrics.name, 'custom-metrics2') + t.strictEqual(mockMetrics.callback, cb2) + t.strictEqual(mockMetrics.labels, undefined) + t.strictEqual(mockMetrics.cbValue, 6789) - t.end() - }) + agent.destroy() + t.end() }) - -function assertMetadata (t, payload) { - t.strictEqual(payload.service.name, 'some-service-name') - t.deepEqual(payload.service.runtime, { name: 'node', version: process.versions.node }) - t.deepEqual(payload.service.agent, { name: 'nodejs', version: packageJson.version }) - - const expectedSystemKeys = ['hostname', 'architecture', 'platform'] - if (inContainer) expectedSystemKeys.push('container') - - t.deepEqual(Object.keys(payload.system), expectedSystemKeys) - t.strictEqual(payload.system.hostname, os.hostname()) - t.strictEqual(payload.system.architecture, process.arch) - t.strictEqual(payload.system.platform, process.platform) - - if (inContainer) { - t.deepEqual(Object.keys(payload.system.container), ['id']) - t.strictEqual(typeof payload.system.container.id, 'string') - t.ok(/^[\da-f]{64}$/.test(payload.system.container.id)) - } - - t.ok(payload.process) - t.strictEqual(payload.process.pid, process.pid) - t.ok(payload.process.pid > 0, 'should have a pid greater than 0') - t.ok(payload.process.title, 'should have a process title') - t.strictEqual(payload.process.title, process.title) - t.deepEqual(payload.process.argv, process.argv) - t.ok(payload.process.argv.length >= 2, 'should have at least two process arguments') -} -assertMetadata.asserts = inContainer ? 17 : 14 - -function assertTransaction (t, trans, name, input, output) { - t.strictEqual(trans.name, name) - t.strictEqual(trans.type, 'lambda') - t.strictEqual(trans.result, 'success') - t.ok(trans.context) - var custom = trans.context.custom - t.ok(custom) - var lambda = custom.lambda - t.ok(lambda) - t.deepEqual(lambda.input, input) - t.strictEqual(lambda.output, output) -} -assertTransaction.asserts = 8 - -function assertStackTrace (t, stacktrace) { - t.ok(stacktrace !== undefined, 'should have a stack trace') - t.ok(Array.isArray(stacktrace), 'stack trace should be an array') - t.ok(stacktrace.length > 0, 'stack trace should have at least one frame') - t.strictEqual(stacktrace[0].filename, path.join('test', 'agent.test.js')) -} -assertStackTrace.asserts = 4 - -function validateRequest (t) { - return function (req) { - t.strictEqual(req.method, 'POST', 'should be a POST request') - t.strictEqual(req.url, '/intake/v2/events', 'should be sent to the intake endpoint') - } -} -validateRequest.asserts = 2 - -function validateMetadata (t) { - return function (data, index) { - t.strictEqual(index, 0, 'metadata should always be sent first') - assertMetadata(t, data) - } -} -validateMetadata.asserts = 1 + assertMetadata.asserts - -function APMServerWithDefaultAsserts (t, agentOpts, mockOpts) { - var server = APMServer(agentOpts, mockOpts) - .on('request', validateRequest(t)) - .on('data-metadata', validateMetadata(t)) - t.on('end', function () { - server.destroy() - }) - return server -} -APMServerWithDefaultAsserts.asserts = validateRequest.asserts + validateMetadata.asserts - -function deep (depth, n) { - if (!n) n = 0 - if (n < depth) return deep(depth, ++n) - return new Error() -} diff --git a/test/config.test.js b/test/config.test.js index 04fc1bb187..cc4af97d49 100644 --- a/test/config.test.js +++ b/test/config.test.js @@ -14,17 +14,35 @@ var rimraf = require('rimraf') var semver = require('semver') var test = require('tape') -var Agent = require('./_agent') -var APMServer = require('./_apm_server') +const Agent = require('../lib/agent') +const { MockAPMServer } = require('./_mock_apm_server') +const { NoopTransport } = require('../lib/noop-transport') var config = require('../lib/config') var Instrumentation = require('../lib/instrumentation') var apmVersion = require('../package').version var apmName = require('../package').name var isHapiIncompat = require('./_is_hapi_incompat') -process.env.ELASTIC_APM_METRICS_INTERVAL = '0' -process.env.ELASTIC_APM_CENTRAL_CONFIG = 'false' -process.env._ELASTIC_APM_ASYNC_HOOKS_RESETTABLE = 'true' +// Options to pass to `agent.start()` to turn off some default agent behavior +// that is unhelpful for these tests. +const agentOpts = { + centralConfig: false, + captureExceptions: false, + metricsInterval: '0s', + cloudProvider: 'none', + spanFramesMinDuration: -1, // Never discard fast spans. + logLevel: 'warn' +} +const agentOptsNoopTransport = Object.assign( + {}, + agentOpts, + { + transport: function createNoopTransport () { + // Avoid accidentally trying to send data to an APM server. + return new NoopTransport() + } + } +) var optionFixtures = [ ['abortedErrorThreshold', 'ABORTED_ERROR_THRESHOLD', 25], @@ -91,7 +109,7 @@ optionFixtures.forEach(function (fixture) { var existingValue = process.env[envName] test(`should be configurable by environment variable ${envName}`, function (t) { - var agent = Agent() + var agent = new Agent() var value if (bool) value = !fixture[2] @@ -108,7 +126,7 @@ optionFixtures.forEach(function (fixture) { process.env[envName] = value.toString() - agent.start() + agent.start(agentOptsNoopTransport) if (array) { t.deepEqual(agent._conf[fixture[0]], [value]) @@ -122,11 +140,12 @@ optionFixtures.forEach(function (fixture) { delete process.env[envName] } + agent.destroy() t.end() }) test(`should overwrite option property ${fixture[0]} by ${envName}`, function (t) { - var agent = Agent() + var agent = new Agent() var opts = {} var value1, value2 @@ -155,7 +174,7 @@ optionFixtures.forEach(function (fixture) { opts[fixture[0]] = value1 process.env[envName] = value2.toString() - agent.start(opts) + agent.start(Object.assign({}, agentOptsNoopTransport, opts)) if (array) { t.deepEqual(agent._conf[fixture[0]], [value2]) @@ -169,6 +188,7 @@ optionFixtures.forEach(function (fixture) { delete process.env[envName] } + agent.destroy() t.end() }) } @@ -177,9 +197,12 @@ optionFixtures.forEach(function (fixture) { if (existingValue) { delete process.env[envName] } + var opts = Object.assign({}, agentOptsNoopTransport) + if (fixture[0] in opts) { + delete opts[fixture[0]] + } - var agent = Agent() - agent.start() + var agent = new Agent().start(opts) if (array) { t.deepEqual(agent._conf[fixture[0]], fixture[2]) } else { @@ -190,42 +213,49 @@ optionFixtures.forEach(function (fixture) { process.env[envName] = existingValue } + agent.destroy() t.end() }) }) falsyValues.forEach(function (val) { test('should be disabled by environment variable ELASTIC_APM_ACTIVE set to: ' + util.inspect(val), function (t) { - var agent = Agent() + var agent = new Agent() process.env.ELASTIC_APM_ACTIVE = val - agent.start({ serviceName: 'foo', secretToken: 'baz' }) + agent.start(Object.assign({}, agentOptsNoopTransport, { serviceName: 'foo', secretToken: 'baz' })) t.strictEqual(agent._conf.active, false) delete process.env.ELASTIC_APM_ACTIVE + agent.destroy() t.end() }) }) truthyValues.forEach(function (val) { test('should be enabled by environment variable ELASTIC_APM_ACTIVE set to: ' + util.inspect(val), function (t) { - var agent = Agent() + var agent = new Agent() process.env.ELASTIC_APM_ACTIVE = val - agent.start({ serviceName: 'foo', secretToken: 'baz' }) + agent.start(Object.assign({}, agentOptsNoopTransport, { serviceName: 'foo', secretToken: 'baz' })) t.strictEqual(agent._conf.active, true) delete process.env.ELASTIC_APM_ACTIVE + agent.destroy() t.end() }) }) test('should log invalid booleans', function (t) { - var agent = Agent() + var agent = new Agent() var logger = new CaptureLogger() - agent.start({ - serviceName: 'foo', - secretToken: 'baz', - active: 'nope', - logger - }) + agent.start(Object.assign( + {}, + agentOptsNoopTransport, + { + serviceName: 'foo', + secretToken: 'baz', + active: 'nope', + logger + } + )) t.strictEqual(logger.calls.length, 2) @@ -235,6 +265,7 @@ test('should log invalid booleans', function (t) { var debug = logger.calls.shift() t.strictEqual(debug.message, 'Elastic APM agent disabled (`active` is false)') + agent.destroy() t.end() }) @@ -244,11 +275,12 @@ var MINUS_ONE_EQUAL_INFINITY = [ MINUS_ONE_EQUAL_INFINITY.forEach(function (key) { test(key + ' should be Infinity if set to -1', function (t) { - var agent = Agent() + var agent = new Agent() var opts = {} opts[key] = -1 - agent.start(opts) + agent.start(Object.assign({}, agentOptsNoopTransport, opts)) t.strictEqual(agent._conf[key], Infinity) + agent.destroy() t.end() }) }) @@ -260,11 +292,12 @@ var bytesValues = [ bytesValues.forEach(function (key) { test(key + ' should be converted to a number', function (t) { - var agent = Agent() + var agent = new Agent() var opts = {} opts[key] = '1mb' - agent.start(opts) + agent.start(Object.assign({}, agentOptsNoopTransport, opts)) t.strictEqual(agent._conf[key], 1024 * 1024) + agent.destroy() t.end() }) }) @@ -278,66 +311,42 @@ var timeValues = [ timeValues.forEach(function (key) { test(key + ' should convert minutes to seconds', function (t) { - if (key === 'metricsInterval') { - delete process.env.ELASTIC_APM_METRICS_INTERVAL - t.on('end', function () { - process.env.ELASTIC_APM_METRICS_INTERVAL = '0' - }) - } - - var agent = Agent() + var agent = new Agent() var opts = {} opts[key] = '1m' - agent.start(opts) + agent.start(Object.assign({}, agentOptsNoopTransport, opts)) t.strictEqual(agent._conf[key], 60) + agent.destroy() t.end() }) test(key + ' should convert milliseconds to seconds', function (t) { - if (key === 'metricsInterval') { - delete process.env.ELASTIC_APM_METRICS_INTERVAL - t.on('end', function () { - process.env.ELASTIC_APM_METRICS_INTERVAL = '0' - }) - } - - var agent = Agent() + var agent = new Agent() var opts = {} opts[key] = '2000ms' - agent.start(opts) + agent.start(Object.assign({}, agentOptsNoopTransport, opts)) t.strictEqual(agent._conf[key], 2) + agent.destroy() t.end() }) test(key + ' should parse seconds', function (t) { - if (key === 'metricsInterval') { - delete process.env.ELASTIC_APM_METRICS_INTERVAL - t.on('end', function () { - process.env.ELASTIC_APM_METRICS_INTERVAL = '0' - }) - } - - var agent = Agent() + var agent = new Agent() var opts = {} opts[key] = '5s' - agent.start(opts) + agent.start(Object.assign({}, agentOptsNoopTransport, opts)) t.strictEqual(agent._conf[key], 5) + agent.destroy() t.end() }) test(key + ' should support bare numbers', function (t) { - if (key === 'metricsInterval') { - delete process.env.ELASTIC_APM_METRICS_INTERVAL - t.on('end', function () { - process.env.ELASTIC_APM_METRICS_INTERVAL = '0' - }) - } - - var agent = Agent() + var agent = new Agent() var opts = {} opts[key] = 10 - agent.start(opts) + agent.start(Object.assign({}, agentOptsNoopTransport, opts)) t.strictEqual(agent._conf[key], 10) + agent.destroy() t.end() }) }) @@ -362,29 +371,32 @@ keyValuePairValues.forEach(function (key) { ] test(key + ' should support string form', function (t) { - var agent = Agent() + var agent = new Agent() var opts = {} opts[key] = string - agent._config(opts) + agent.start(Object.assign({}, agentOptsNoopTransport, opts)) t.deepEqual(agent._conf[key], pairs) + agent.destroy() t.end() }) test(key + ' should support object form', function (t) { - var agent = Agent() + var agent = new Agent() var opts = {} opts[key] = object - agent._config(opts) + agent.start(Object.assign({}, agentOptsNoopTransport, opts)) t.deepEqual(agent._conf[key], pairs) + agent.destroy() t.end() }) test(key + ' should support pair form', function (t) { - var agent = Agent() + var agent = new Agent() var opts = {} opts[key] = pairs - agent._config(opts) + agent.start(Object.assign({}, agentOptsNoopTransport, opts)) t.deepEqual(agent._conf[key], pairs) + agent.destroy() t.end() }) }) @@ -399,49 +411,57 @@ var noPrefixValues = [ noPrefixValues.forEach(function (pair) { const [key, envVar] = pair test(`maps ${envVar} to ${key}`, (t) => { - var agent = Agent() + var agent = new Agent() process.env[envVar] = 'test' - agent.start() + agent.start(agentOptsNoopTransport) delete process.env[envVar] t.strictEqual(agent._conf[key], 'test') + agent.destroy() t.end() }) }) test('should overwrite option property active by ELASTIC_APM_ACTIVE', function (t) { - var agent = Agent() + var agent = new Agent() var opts = { serviceName: 'foo', secretToken: 'baz', active: true } process.env.ELASTIC_APM_ACTIVE = 'false' - agent.start(opts) + agent.start(Object.assign({}, agentOptsNoopTransport, opts)) t.strictEqual(agent._conf.active, false) delete process.env.ELASTIC_APM_ACTIVE + agent.destroy() t.end() }) test('should default serviceName to package name', function (t) { - var agent = Agent() + var agent = new Agent() agent.start() t.strictEqual(agent._conf.serviceName, 'elastic-apm-node') + agent.destroy() t.end() }) test('should default to empty request blacklist arrays', function (t) { - var agent = Agent() - agent.start() + var agent = new Agent() + agent.start(agentOptsNoopTransport) t.strictEqual(agent._conf.ignoreUrlStr.length, 0) t.strictEqual(agent._conf.ignoreUrlRegExp.length, 0) t.strictEqual(agent._conf.ignoreUserAgentStr.length, 0) t.strictEqual(agent._conf.ignoreUserAgentRegExp.length, 0) t.strictEqual(agent._conf.transactionIgnoreUrlRegExp.length, 0) + agent.destroy() t.end() }) test('should separate strings and regexes into their own blacklist arrays', function (t) { - var agent = Agent() - agent.start({ - ignoreUrls: ['str1', /regex1/], - ignoreUserAgents: ['str2', /regex2/] - }) + var agent = new Agent() + agent.start(Object.assign( + {}, + agentOptsNoopTransport, + { + ignoreUrls: ['str1', /regex1/], + ignoreUserAgents: ['str2', /regex2/] + } + )) t.deepEqual(agent._conf.ignoreUrlStr, ['str1']) t.deepEqual(agent._conf.ignoreUserAgentStr, ['str2']) @@ -454,14 +474,19 @@ test('should separate strings and regexes into their own blacklist arrays', func t.ok(isRegExp(agent._conf.ignoreUserAgentRegExp[0])) t.strictEqual(agent._conf.ignoreUserAgentRegExp[0].toString(), '/regex2/') + agent.destroy() t.end() }) test('should compile wildcards from string', function (t) { - var agent = Agent() - agent.start({ - transactionIgnoreUrls: ['foo', '/str1', '/wil*card'] - }) + var agent = new Agent() + agent.start(Object.assign( + {}, + agentOptsNoopTransport, + { + transactionIgnoreUrls: ['foo', '/str1', '/wil*card'] + } + )) t.strictEqual( agent._conf.transactionIgnoreUrlRegExp.length, @@ -469,20 +494,23 @@ test('should compile wildcards from string', function (t) { 'was everything added?' ) + agent.destroy() t.end() }) test('invalid serviceName => inactive', function (t) { - var agent = Agent() - agent.start({ serviceName: 'foo&bar' }) + var agent = new Agent() + agent.start(Object.assign({}, agentOptsNoopTransport, { serviceName: 'foo&bar' })) t.strictEqual(agent._conf.active, false) + agent.destroy() t.end() }) test('valid serviceName => active', function (t) { - var agent = Agent() - agent.start({ serviceName: 'fooBAR0123456789_- ' }) + var agent = new Agent() + agent.start(Object.assign({}, agentOptsNoopTransport, { serviceName: 'fooBAR0123456789_- ' })) t.strictEqual(agent._conf.active, true) + agent.destroy() t.end() }) @@ -509,7 +537,11 @@ test('serviceName defaults to package name', function (t) { action: 'create', path: path.join(tmp, 'index.js'), contents: ` - var apm = require('elastic-apm-node').start({logLevel: 'off'}) + var apm = require('elastic-apm-node').start({ + centralConfig: false, + metricsInterval: '0s', + logLevel: 'off' + }) console.log(JSON.stringify(apm._conf)) ` }, @@ -632,45 +664,45 @@ var captureBodyTests = [ captureBodyTests.forEach(function (captureBodyTest) { test('captureBody => ' + captureBodyTest.value, function (t) { - t.plan(4) - - var agent = Agent() - agent.start({ - serviceName: 'test', - captureExceptions: false, - captureBody: captureBodyTest.value - }) - - var sendError = agent._transport.sendError - var sendTransaction = agent._transport.sendTransaction - agent._transport.sendError = function (error, cb) { - var request = error.context.request - t.ok(request) - t.strictEqual(request.body, captureBodyTest.errors) - if (cb) process.nextTick(cb) - } - agent._transport.sendTransaction = function (trans, cb) { - var request = trans.context.request - t.ok(request) - t.strictEqual(request.body, captureBodyTest.transactions) - if (cb) process.nextTick(cb) - } - t.on('end', function () { - agent._transport.sendError = sendError - agent._transport.sendTransaction = sendTransaction - }) + const apmServer = new MockAPMServer() + apmServer.start(function (serverUrl) { + const agent = new Agent().start(Object.assign( + {}, + agentOpts, + { + serverUrl, + captureBody: captureBodyTest.value + } + )) - var req = new IncomingMessage() - req.socket = { remoteAddress: '127.0.0.1' } - req.headers['transfer-encoding'] = 'chunked' - req.headers['content-length'] = 4 - req.body = 'test' + var req = new IncomingMessage() + req.socket = { remoteAddress: '127.0.0.1' } + req.headers['transfer-encoding'] = 'chunked' + req.headers['content-length'] = 4 + req.body = 'test' - agent.captureError(new Error('wat'), { request: req }) + var trans = agent.startTransaction() + trans.req = req + trans.end() - var trans = agent.startTransaction() - trans.req = req - trans.end() + agent.captureError(new Error('wat'), { request: req }, + function () { + t.equal(apmServer.events.length, 3, 'apmServer got 3 events') + let data = apmServer.events[1].transaction + t.ok(data, 'event 1 is a transaction') + t.strictEqual(data.context.request.body, captureBodyTest.transactions, + 'transaction.context.request.body is ' + captureBodyTest.transactions) + data = apmServer.events[2].error + t.ok(data, 'event 2 is an error') + t.strictEqual(data.context.request.body, captureBodyTest.errors, + 'error.context.request.body is ' + captureBodyTest.errors) + + agent.destroy() + apmServer.close() + t.end() + } + ) + }) }) }) @@ -681,24 +713,26 @@ var usePathAsTransactionNameTests = [ usePathAsTransactionNameTests.forEach(function (usePathAsTransactionNameTest) { test('usePathAsTransactionName => ' + usePathAsTransactionNameTest.value, function (t) { - t.plan(2) - - var agent = Agent() - agent.start({ - serviceName: 'test', - captureExceptions: false, - usePathAsTransactionName: usePathAsTransactionNameTest.value - }) - - var sendTransaction = agent._transport.sendTransaction - agent._transport.sendTransaction = function (trans, cb) { - t.ok(trans) - t.strictEqual(trans.name, usePathAsTransactionNameTest.transactionName) - if (cb) process.nextTick(cb) - } - t.on('end', function () { - agent._transport.sendTransaction = sendTransaction - }) + var sentTrans + var agent = new Agent() + agent.start(Object.assign( + {}, + agentOptsNoopTransport, + { + usePathAsTransactionName: usePathAsTransactionNameTest.value, + transport () { + return { + sendTransaction (trans, cb) { + sentTrans = trans + if (cb) process.nextTick(cb) + }, + flush (cb) { + if (cb) process.nextTick(cb) + } + } + } + } + )) var req = new IncomingMessage() req.socket = { remoteAddress: '127.0.0.1' } @@ -708,6 +742,15 @@ usePathAsTransactionNameTests.forEach(function (usePathAsTransactionNameTest) { var trans = agent.startTransaction() trans.req = req trans.end() + + agent.flush(function () { + t.ok(sentTrans, 'sent a transaction') + t.strictEqual(sentTrans.name, usePathAsTransactionNameTest.transactionName, + 'transaction.name is ' + usePathAsTransactionNameTest.transactionName) + + agent.destroy() + t.end() + }) }) }) @@ -747,12 +790,12 @@ test('disableInstrumentations', function (t) { var selectionSet = new Set(typeof selection === 'string' ? selection.split(',') : selection) t.test(name + ' -> ' + Array.from(selectionSet).join(','), function (t) { - var agent = Agent() - agent.start({ - serviceName: 'service', - disableInstrumentations: selection, - captureExceptions: false - }) + var agent = new Agent() + agent.start(Object.assign( + {}, + agentOptsNoopTransport, + { disableInstrumentations: selection } + )) var found = new Set() @@ -767,6 +810,7 @@ test('disableInstrumentations', function (t) { t.deepEqual(selectionSet, found, 'disabled all selected modules') + agent.destroy() t.end() }) } @@ -787,54 +831,38 @@ test('disableInstrumentations', function (t) { }) test('custom transport', function (t) { - var agent = Agent() - agent.start({ - captureExceptions: false, - cloudProvider: 'none', - serviceName: 'fooBAR0123456789_- ', - transport () { - var transactions = [] - var spans = [] - var errors = [] - function makeSenderFor (list) { - return (item, callback) => { - list.push(item) - if (callback) { - setImmediate(callback) - } - } - } - var first = true - return { - sendTransaction: makeSenderFor(transactions), - sendSpan: makeSenderFor(spans), - sendError: makeSenderFor(errors), - config: () => {}, - flush (cb) { - if (cb) setImmediate(cb) - if (first) { - // first flush is from calling `agent.flush()` below, second flush - // is done by the internals of `captureError()`. This logic will - // change once the following issue is implemented: - // https://github.com/elastic/apm-agent-nodejs/issues/686 - first = false - return - } + class MyTransport { + constructor () { + this.transactions = [] + this.spans = [] + this.errors = [] + } - // add slight delay to give the span time to be fully encoded and sent - setTimeout(function () { - t.strictEqual(transactions.length, 1, 'received correct number of transactions') - assertEncodedTransaction(t, trans, transactions[0]) - t.strictEqual(spans.length, 1, 'received correct number of spans') - assertEncodedSpan(t, span, spans[0]) - t.strictEqual(errors.length, 1, 'received correct number of errors') - assertEncodedError(t, error, errors[0], trans, span) - t.end() - }, 200) - } - } + sendTransaction (data, cb) { + this.transactions.push(data) + if (cb) setImmediate(cb) } - }) + + sendSpan (data, cb) { + this.spans.push(data) + if (cb) setImmediate(cb) + } + + sendError (data, cb) { + this.errors.push(data) + if (cb) setImmediate(cb) + } + + config () {} + + flush (cb) { + if (cb) setImmediate(cb) + } + } + const myTransport = new MyTransport() + + var agent = new Agent() + agent.start(Object.assign({}, agentOpts, { transport: () => myTransport })) var error = new Error('error') var trans = agent.startTransaction('transaction') @@ -842,7 +870,17 @@ test('custom transport', function (t) { agent.captureError(error) span.end() trans.end() - agent.flush() + + setTimeout(function () { + t.equal(myTransport.transactions.length, 1, 'received correct number of transactions') + assertEncodedTransaction(t, trans, myTransport.transactions[0]) + t.equal(myTransport.spans.length, 1, 'received correct number of spans') + assertEncodedSpan(t, span, myTransport.spans[0]) + t.equal(myTransport.errors.length, 1, 'received correct number of errors') + assertEncodedError(t, error, myTransport.errors[0], trans, span) + agent.destroy() + t.end() + }, 200) // Hack wait for ended span and captured error to be sent to transport. }) test('addPatch', function (t) { @@ -851,14 +889,18 @@ test('addPatch', function (t) { delete require.cache[require.resolve('express')] - const agent = Agent() - agent.start({ - addPatch: 'express=./test/_patch.js', - captureExceptions: false - }) + const agent = new Agent() + agent.start(Object.assign( + {}, + agentOptsNoopTransport, + { + addPatch: 'express=./test/_patch.js' + } + )) t.deepEqual(require('express'), patch(before)) + agent.destroy() t.end() }) @@ -866,42 +908,53 @@ test('globalLabels should be received by transport', function (t) { var globalLabels = { foo: 'bar' } - var opts = { globalLabels } - - var server = APMServer(opts, { expect: 'error' }) - .on('listening', function () { - this.agent.captureError(new Error('trigger metadata')) - }) - .on('data-metadata', (data) => { - t.deepEqual(data.labels, globalLabels) - t.end() - }) - t.on('end', function () { - server.destroy() + const apmServer = new MockAPMServer() + apmServer.start(function (serverUrl) { + const agent = new Agent().start(Object.assign( + {}, + agentOpts, + { + serverUrl, + globalLabels + } + )) + agent.captureError(new Error('trigger metadata'), + function () { + t.equal(apmServer.events.length, 2, 'apmServer got 2 events') + const data = apmServer.events[0].metadata + t.ok(data, 'first event is metadata') + t.deepEqual(data.labels, globalLabels, 'metadata.labels has globalLabels') + agent.destroy() + apmServer.close() + t.end() + } + ) }) }) test('instrument: false allows manual instrumentation', function (t) { - var trans - var opts = { - metricsInterval: 0, - instrument: false - } - - var server = APMServer(opts, { expect: 'transaction' }) - .on('listening', function () { - trans = this.agent.startTransaction('trans') - trans.end() - this.agent.flush() - }) - .on('data-transaction', (data) => { + const apmServer = new MockAPMServer() + apmServer.start(function (serverUrl) { + const agent = new Agent().start(Object.assign( + {}, + agentOpts, + { + serverUrl, + instrument: false + } + )) + const trans = agent.startTransaction('trans') + trans.end() + agent.flush(function () { + t.equal(apmServer.events.length, 2, 'apmServer got 2 events') + const data = apmServer.events[1].transaction + t.ok(data, 'second event is a transaction') assertEncodedTransaction(t, trans, data) + agent.destroy() + apmServer.close() t.end() }) - - t.on('end', function () { - server.destroy() }) }) @@ -1035,52 +1088,70 @@ test('transactionSampleRate precision', function (t) { }) test('should accept and normalize cloudProvider', function (t) { - const agentDefault = Agent() - agentDefault.start() + const agentDefault = new Agent() + agentDefault.start({ + disableSend: true + }) t.equals(agentDefault._conf.cloudProvider, 'auto', 'cloudProvider config defaults to auto') + agentDefault.destroy() - const agentGcp = Agent() + const agentGcp = new Agent() agentGcp.start({ + disableSend: true, cloudProvider: 'gcp' }) + agentGcp.destroy() t.equals(agentGcp._conf.cloudProvider, 'gcp', 'cloudProvider can be set to gcp') - const agentAzure = Agent() + const agentAzure = new Agent() agentAzure.start({ + disableSend: true, cloudProvider: 'azure' }) + agentAzure.destroy() t.equals(agentAzure._conf.cloudProvider, 'azure', 'cloudProvider can be set to azure') - const agentAws = Agent() + const agentAws = new Agent() agentAws.start({ + disableSend: true, cloudProvider: 'aws' }) + agentAws.destroy() t.equals(agentAws._conf.cloudProvider, 'aws', 'cloudProvider can be set to aws') - const agentNone = Agent() + const agentNone = new Agent() agentNone.start({ + disableSend: true, cloudProvider: 'none' }) + agentNone.destroy() t.equals(agentNone._conf.cloudProvider, 'none', 'cloudProvider can be set to none') - const agentUnknown = Agent() + const agentUnknown = new Agent() agentUnknown.start({ + disableSend: true, + logLevel: 'off', // Silence the log.warn for the invalid cloudProvider value. cloudProvider: 'this-is-not-a-thing' }) + agentUnknown.destroy() t.equals(agentUnknown._conf.cloudProvider, 'auto', 'unknown cloudProvider defaults to auto') - const agentGcpFromEnv = Agent() + const agentGcpFromEnv = new Agent() process.env.ELASTIC_APM_CLOUD_PROVIDER = 'gcp' - agentGcpFromEnv.start() + agentGcpFromEnv.start({ + disableSend: true + }) t.equals(agentGcpFromEnv._conf.cloudProvider, 'gcp', 'cloudProvider can be set via env') delete process.env.ELASTIC_APM_CLOUD_PROVIDER + agentGcpFromEnv.destroy() + t.end() }) test('should accept and normalize ignoreMessageQueues', function (suite) { suite.test('ignoreMessageQueues defaults', function (t) { - const agent = Agent() - agent.start() + const agent = new Agent() + agent.start(agentOptsNoopTransport) t.equals( agent._conf.ignoreMessageQueues.length, 0, @@ -1092,12 +1163,17 @@ test('should accept and normalize ignoreMessageQueues', function (suite) { 0, 'ignore message queue regex defaults empty' ) + agent.destroy() t.end() }) suite.test('ignoreMessageQueues via configuration', function (t) { - const agent = Agent() - agent.start({ ignoreMessageQueues: ['f*o', 'bar'] }) + const agent = new Agent() + agent.start(Object.assign( + {}, + agentOptsNoopTransport, + { ignoreMessageQueues: ['f*o', 'bar'] } + )) t.equals( agent._conf.ignoreMessageQueues.length, 2, @@ -1114,13 +1190,14 @@ test('should accept and normalize ignoreMessageQueues', function (suite) { agent._conf.ignoreMessageQueuesRegExp[0].test('faooooo'), 'wildcard converted to regular expression' ) + agent.destroy() t.end() }) suite.test('ignoreMessageQueues via env', function (t) { - const agent = Agent() + const agent = new Agent() process.env.ELASTIC_IGNORE_MESSAGE_QUEUES = 'f*o,bar,baz' - agent.start() + agent.start(agentOptsNoopTransport) t.equals( agent._conf.ignoreMessageQueues.length, 3, @@ -1137,6 +1214,7 @@ test('should accept and normalize ignoreMessageQueues', function (suite) { agent._conf.ignoreMessageQueuesRegExp[0].test('faooooo'), 'wildcard converted to regular expression' ) + agent.destroy() t.end() })