From 9dbded4af1ea7a694643c09d6825503b82b23cbd Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Wed, 25 Aug 2021 18:05:12 -0700 Subject: [PATCH 1/8] test: refactor to move knowledge for in-process Agent re-use into Agent methods Move away from the test/_apm_server.js and test/_agent.js pattern that encapsulates internal details of Agent and Instrumentation to allow re-use of an Agent instance in-process in the same test file. The pattern from these test support files also makes leftover state from a test case an issue for *subsequent* test cases to deal with. Instead we add agent._testReset (and ins.testReset) to encapsulate these internal details. This also fixes a bug in agent.test.js where a new async-hook was being created and enabled for each Agent re-use. Cleanup was never disabling those async-hooks. --- lib/agent.js | 37 +- lib/instrumentation/async-hooks.js | 27 +- lib/instrumentation/index.js | 34 +- test/_mock_apm_server.js | 10 +- test/agent.test.js | 1704 +++++++++++++++------------- test/config.test.js | 1 - 6 files changed, 1029 insertions(+), 784 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 0b66794c0e..8360fe7046 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -46,6 +46,31 @@ function Agent () { this.lambda = elasticApmAwsLambda(this) } +// Reset the running state of this agent to a (relatively) clean state, to +// allow re-`.start()` and re-use of this Agent within the same process for +// testing. This is not supported outside of the test suite. +// +// Limitations: There are untracked async tasks (a) between span.end() and +// and transport.sendSpan(); and (b) between agent.captureError() and +// transport.sendError(). Currently there is no way to wait for completion of +// those tasks in this method. There is code in each of ins.addEndedSpan and +// agent.captureError to mitigate this. +Agent.prototype._testReset = function () { + this._errorFilters = new Filters() + this._transactionFilters = new Filters() + this._spanFilters = new Filters() + if (this._transport && this._transport.destroy) { + this._transport.destroy() + } + this._transport = null + global[symbols.agentInitialized] = null // Mark as not yet started. + if (this._uncaughtExceptionListener) { + process.removeListener('uncaughtException', this._uncaughtExceptionListener) + } + this._metrics.stop() + this._instrumentation.testReset() +} + Object.defineProperty(Agent.prototype, 'currentTransaction', { get () { return this._instrumentation.currentTransaction @@ -394,6 +419,12 @@ Agent.prototype.captureError = function (err, opts, cb) { span._setOutcomeFromErrorCapture(constants.OUTCOME_FAILURE) } + // Ensure an inflight `agent.captureError()` across a call to + // `agent._testReset()` does not impact testing of the agent after that call + // -- by using *current* values of _errorFilters and _transport. + const errorFilters = agent._errorFilters + const transport = agent._transport + // Move the remaining captureError processing to a later tick because: // 1. This allows the calling code to continue processing. For example, for // Express instrumentation this can significantly improve latency in @@ -454,7 +485,7 @@ Agent.prototype.captureError = function (err, opts, cb) { // _err is always null from createAPMError. const id = apmError.id - apmError = agent._errorFilters.process(apmError) + apmError = errorFilters.process(apmError) if (!apmError) { agent.logger.debug('error ignored by filter %o', { id }) if (cb) { @@ -463,9 +494,9 @@ Agent.prototype.captureError = function (err, opts, cb) { return } - if (agent._transport) { + if (transport) { agent.logger.info('Sending error to Elastic APM: %o', { id }) - agent._transport.sendError(apmError, function () { + transport.sendError(apmError, function () { agent.flush(function (flushErr) { if (cb) { cb(flushErr, id) diff --git a/lib/instrumentation/async-hooks.js b/lib/instrumentation/async-hooks.js index 1dd168ff8d..ded9cf8d14 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 - } + let activeSpans = new Map() + let activeTransactions = new Map() + let contexts = new WeakMap() - const activeTransactions = new Map() 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, 'testReset', function (origTestReset) { + return function wrappedTestReset () { + asyncHook.disable() + activeTransactions = new Map() + activeSpans = new Map() + contexts = new WeakMap() + shimmer.unwrap(ins, 'addEndedTransaction') + shimmer.unwrap(ins, 'testReset') + return origTestReset.call(this) + } + }) + asyncHook.enable() function init (asyncId, type, triggerAsyncId, resource) { diff --git a/lib/instrumentation/index.js b/lib/instrumentation/index.js index 127b2689ff..37755e5a4c 100644 --- a/lib/instrumentation/index.js +++ b/lib/instrumentation/index.js @@ -86,6 +86,27 @@ function Instrumentation (agent) { } } +// Reset internal context tracking state for (relatively) clean re-use of this +// Instrumentation in the same process. Used for testing. +// +// Limitations: Removing and re-applying 'require-in-the-middle'-based patches +// has no way to update user or test code that already has references to +// patched or unpatched exports from those modules. That may mean some +// automatic instrumentation may not work. +Instrumentation.prototype.testReset = 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 @@ -232,13 +253,20 @@ Instrumentation.prototype.addEndedSpan = function (span) { // Save effort and logging if disableSend=true. } else if (this._started) { agent.logger.debug('encoding span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type }) + + // Ensure an inflight `span._encode()` across a call to `agent._testReset()` + // does not impact testing of the agent after that call -- by using + // *current* values of _spanFilters and _transport. + const spanFilters = agent._spanFilters + const transport = agent._transport + span._encode(function (err, payload) { if (err) { agent.logger.error('error encoding span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type, error: err.message }) return } - payload = agent._spanFilters.process(payload) + payload = spanFilters.process(payload) if (!payload) { agent.logger.debug('span ignored by filter %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type }) @@ -246,7 +274,9 @@ Instrumentation.prototype.addEndedSpan = function (span) { } agent.logger.debug('sending span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type }) - if (agent._transport) agent._transport.sendSpan(payload) + if (transport) { + transport.sendSpan(payload) + } }) } else { agent.logger.debug('ignoring span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type }) 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..19381fe889 100644 --- a/test/agent.test.js +++ b/test/agent.test.js @@ -1,43 +1,119 @@ '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()` it at the top of file. Also, the first test +// relies on testing state before the first start. +const agent = require('..') + 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') 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() + } + } +) -test('#getServiceName()', function (t) { - var agent = Agent() +// ---- 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') - // Before agent.start() config will have already been loaded once, which + 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) { + // 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._testReset() 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. + agent.start(agentOpts) + t.strictEqual(agent._conf.frameworkName, undefined) t.strictEqual(agent._conf.frameworkVersion, undefined) t.strictEqual(agent._transport._conf.frameworkName, undefined) @@ -67,36 +143,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._testReset() t.end() }) test('#startTransaction()', function (t) { t.test('name, type, subtype and action', function (t) { - var agent = Agent() - agent.start() + 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._testReset() t.end() }) t.test('options.startTime', function (t) { - var agent = Agent() - agent.start() + 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._testReset() t.end() }) t.test('options.childOf', function (t) { - var agent = Agent() - agent.start() + 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 +180,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._testReset() t.end() }) + + t.end() }) test('#endTransaction()', function (t) { t.test('no active transaction', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) agent.endTransaction() + agent._testReset() t.end() }) t.test('with no result', function (t) { - var agent = Agent() - agent.start() + 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._testReset() t.end() }) t.test('with explicit result', function (t) { - var agent = Agent() - agent.start() + 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._testReset() t.end() }) t.test('with custom endTime', function (t) { - var agent = Agent() - agent.start() + 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._testReset() t.end() }) + + t.end() }) test('#currentTransaction', function (t) { t.test('no active transaction', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) t.notOk(agent.currentTransaction) + agent._testReset() t.end() }) t.test('with active transaction', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.currentTransaction, trans) agent.endTransaction() + agent._testReset() t.end() }) }) test('#currentSpan', function (t) { t.test('no active or binding span', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) t.notOk(agent.currentSpan) + agent._testReset() t.end() }) t.test('with binding span', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) var trans = agent.startTransaction() var span = agent.startSpan() t.strictEqual(agent.currentSpan, span) span.end() trans.end() + agent._testReset() t.end() }) t.test('with active span', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) var trans = agent.startTransaction() var span = agent.startSpan() process.nextTick(() => { t.strictEqual(agent.currentSpan, span) span.end() trans.end() + agent._testReset() t.end() }) }) + + t.end() }) test('#currentTraceparent', function (t) { t.test('no active transaction or span', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) t.notOk(agent.currentTraceparent) + agent._testReset() t.end() }) t.test('with active transaction', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.currentTraceparent, trans.traceparent) agent.endTransaction() + agent._testReset() t.end() }) t.test('with active span', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) agent.startTransaction() var span = agent.startSpan() t.strictEqual(agent.currentTraceparent, span.traceparent) span.end() agent.endTransaction() + agent._testReset() t.end() }) + + t.end() }) test('#currentTraceIds', function (t) { t.test('no active transaction or span', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) t.deepLooseEqual(agent.currentTraceIds, {}) t.strictEqual(agent.currentTraceIds.toString(), '') + agent._testReset() t.end() }) t.test('with active transaction', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) var trans = agent.startTransaction() t.deepLooseEqual(agent.currentTraceIds, { 'trace.id': trans.traceId, @@ -249,12 +333,12 @@ test('#currentTraceIds', function (t) { }) t.strictEqual(agent.currentTraceIds.toString(), `trace.id=${trans.traceId} transaction.id=${trans.id}`) agent.endTransaction() + agent._testReset() t.end() }) t.test('with active span', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) agent.startTransaction() var span = agent.startSpan() t.deepLooseEqual(agent.currentTraceIds, { @@ -264,41 +348,45 @@ test('#currentTraceIds', function (t) { t.strictEqual(agent.currentTraceIds.toString(), `trace.id=${span.traceId} span.id=${span.id}`) span.end() agent.endTransaction() + agent._testReset() t.end() }) + + t.end() }) test('#setTransactionName', function (t) { t.test('no active transaction', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) t.doesNotThrow(function () { agent.setTransactionName('foo') }) + agent._testReset() t.end() }) t.test('active transaction', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) var trans = agent.startTransaction() agent.setTransactionName('foo') t.strictEqual(trans.name, 'foo') + agent._testReset() t.end() }) + + t.end() }) test('#startSpan()', function (t) { t.test('no active transaction', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) t.strictEqual(agent.startSpan(), null) + agent._testReset() t.end() }) t.test('active transaction', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) agent.startTransaction() var span = agent.startSpan('span-name', 'type', 'subtype', 'action') t.ok(span, 'should return a span') @@ -306,25 +394,25 @@ test('#startSpan()', function (t) { t.strictEqual(span.type, 'type') t.strictEqual(span.subtype, 'subtype') t.strictEqual(span.action, 'action') + agent._testReset() t.end() }) t.test('options.startTime', function (t) { - var agent = Agent() - agent.start() + 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._testReset() t.end() }) t.test('options.childOf', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) agent.startTransaction() var childOf = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' var span = agent.startSpan(null, null, { childOf }) @@ -333,66 +421,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._testReset() t.end() }) + + t.end() }) test('#setUserContext()', function (t) { t.test('no active transaction', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) t.strictEqual(agent.setUserContext({ foo: 1 }), false) + agent._testReset() t.end() }) t.test('active transaction', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.setUserContext({ foo: 1 }), true) t.deepEqual(trans._user, { foo: 1 }) + agent._testReset() t.end() }) + + t.end() }) test('#setCustomContext()', function (t) { t.test('no active transaction', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) t.strictEqual(agent.setCustomContext({ foo: 1 }), false) + agent._testReset() t.end() }) t.test('active transaction', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.setCustomContext({ foo: 1 }), true) t.deepEqual(trans._custom, { foo: 1 }) + agent._testReset() t.end() }) + + t.end() }) test('#setLabel()', function (t) { t.test('no active transaction', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) t.strictEqual(agent.setLabel('foo', 1), false) + agent._testReset() t.end() }) t.test('active transaction', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.setLabel('foo', 1), true) t.deepEqual(trans._labels, { foo: '1' }) + agent._testReset() t.end() }) t.test('active transaction without label stringification', function (t) { - var agent = Agent() - agent.start() + 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 +500,867 @@ test('#setLabel()', function (t) { 'boolean-false': false, string: 'a custom label' }) + agent._testReset() t.end() }) + + t.end() }) test('#addLabels()', function (t) { t.test('no active transaction', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) t.strictEqual(agent.addLabels({ foo: 1 }), false) + agent._testReset() t.end() }) t.test('active transaction', function (t) { - var agent = Agent() - agent.start() + 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._testReset() t.end() }) t.test('active transaction without label stringification', function (t) { - var agent = Agent() - agent.start() + 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._testReset() 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 - }) + 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._testReset() 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 - }) + 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._testReset() + 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 - }) + 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._testReset() t.end() }) + }, 50) // 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 - }) + 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._testReset() 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 - }) + 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._testReset() + 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 - }) + 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._testReset() t.end() }) + }, 50) // 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 - }) + 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._testReset() + 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 + 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._testReset() + 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 + 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._testReset() + 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 + 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._testReset() + 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 + 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._testReset() + t.end() + }) + }, 50) // 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() 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') + t.pass('should call flush callback even if agent.start(agentOptsNoopTransport) wasn\'t called') + agent._testReset() t.end() }) }) t.test('start called, but agent inactive', function (t) { t.plan(2) - var agent = Agent() 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._testReset() t.end() }) }) t.test('agent started, but no data in the queue', function (t) { t.plan(2) - var agent = Agent() - agent.start() + 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._testReset() 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) { + 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._testReset() + 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') - }) + 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._testReset() + 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() - }) + 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._testReset() + t.end() + }, 50) // 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() - }) + 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._testReset() + 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() - }) + 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._testReset() + 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) { + 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._testReset() 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() - }) + 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._testReset() + 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) { + 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._testReset() 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) { + 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, 50) t.strictEqual(data.exception.stacktrace[0].context_line.trim(), 'return new Error()') + + apmServer.clear() + agent._testReset() 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) { + 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._testReset() 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) { + 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._testReset() 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) { + 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._testReset() 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) { + 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._testReset() 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) { + 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._testReset() 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) { + 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._testReset() 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) { + 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._testReset() 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) { + 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._testReset() 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) { + 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._testReset() 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) { + 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._testReset() 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) { + 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._testReset() t.end() - }) + } + ) }) t.test('capture error before agent is started - with callback', function (t) { - var agent = Agent() agent.captureError(new Error('foo'), function (err) { t.strictEqual(err.message, 'cannot capture error before agent is started') + agent._testReset() t.end() }) }) t.test('capture error before agent is started - without callback', function (t) { - var agent = Agent() agent.captureError(new Error('foo')) + agent._testReset() t.end() }) + // XXX This one is relying on the agent.captureError change to stash `this._transport` + // so delayed-processing error from the previous one or two test cases don't bleed into this one. 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') + 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 +1368,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._testReset() + t.end() + } + ) }) t.test('custom timestamp', function (t) { - t.plan(1 + APMServerWithDefaultAsserts.asserts) - + 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._testReset() t.end() - }) + } + ) }) t.test('options.request', function (t) { - t.plan(2 + APMServerWithDefaultAsserts.asserts) + agent.start(ceAgentOpts) const req = new http.IncomingMessage() req.httpVersion = '1.1' @@ -1197,11 +1410,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 +1432,22 @@ test('#captureError()', function (t) { }, body: '[REDACTED]' }) + + apmServer.clear() + agent._testReset() 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) + agent.start(Object.assign( + {}, + ceAgentOpts, + { captureBody: 'errors' } + )) const req = new http.IncomingMessage() req.httpVersion = '1.1' @@ -1236,11 +1459,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 +1478,16 @@ test('#captureError()', function (t) { }, body: 'foo=bar&password=' + encodeURIComponent('[REDACTED]') }) + + apmServer.clear() + agent._testReset() t.end() - }) + } + ) }) t.test('options.response', function (t) { - t.plan(2 + APMServerWithDefaultAsserts.asserts) + agent.start(ceAgentOpts) const req = new http.IncomingMessage() const res = new http.ServerResponse(req) @@ -1271,11 +1500,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 +1520,107 @@ test('#captureError()', function (t) { headers_sent: false, finished: false }) + + apmServer.clear() + agent._testReset() 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' - }) + agent.start(agentOptsNoopTransport) + t.strictEqual(process._events.uncaughtException, undefined) agent.handleUncaughtExceptions() t.strictEqual(process._events.uncaughtException.length, 1) + + agent._testReset() 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' - }) + agent.start(agentOptsNoopTransport) agent.handleUncaughtExceptions() var before = process._events.uncaughtException.length agent.handleUncaughtExceptions() t.strictEqual(process._events.uncaughtException.length, before) + + agent._testReset() 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) { + 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._testReset() 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') - }) + }, 50) // 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()) + + agent._testReset() t.end() }) }) test('patches', function (t) { t.test('#clearPatches(name)', function (t) { - var agent = 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._testReset() t.end() }) t.test('#addPatch(name, moduleName)', function (t) { - var agent = Agent() agent.clearPatches('express') - agent.start() + agent.start(agentOptsNoopTransport) agent.addPatch('express', './test/_patch.js') @@ -1373,13 +1630,13 @@ test('patches', function (t) { delete require.cache[require.resolve('express')] t.deepEqual(require('express'), patch(before)) + agent._testReset() t.end() }) t.test('#addPatch(name, function) - does not exist', function (t) { - var agent = Agent() agent.clearPatches('express') - agent.start() + agent.start(agentOptsNoopTransport) var replacement = { foo: 'bar' @@ -1396,12 +1653,12 @@ test('patches', function (t) { delete require.cache[require.resolve('express')] t.deepEqual(require('express'), replacement) + agent._testReset() t.end() }) t.test('#removePatch(name, handler)', function (t) { - var agent = Agent() - agent.start() + agent.start(agentOptsNoopTransport) t.notOk(agent._instrumentation._patches.has('does-not-exist')) @@ -1416,136 +1673,55 @@ test('patches', function (t) { agent.removePatch('does-not-exist', handler) t.notOk(agent._instrumentation._patches.has('does-not-exist')) + agent._testReset() 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) { + 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._testReset() + 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..287b8b456c 100644 --- a/test/config.test.js +++ b/test/config.test.js @@ -24,7 +24,6 @@ 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' var optionFixtures = [ ['abortedErrorThreshold', 'ABORTED_ERROR_THRESHOLD', 25], From 43511d9f56940fc74863569fcb02ec46794174bb Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Thu, 26 Aug 2021 13:05:58 -0700 Subject: [PATCH 2/8] drop some hacks; move smarts to Agent#destroy and Instrumentation#stop, much nicer --- lib/agent.js | 78 ++++--- lib/config.js | 15 +- lib/instrumentation/async-hooks.js | 8 +- lib/instrumentation/index.js | 22 +- test/agent.test.js | 358 +++++++++++++++-------------- 5 files changed, 244 insertions(+), 237 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 8360fe7046..afd421313f 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -46,31 +46,6 @@ function Agent () { this.lambda = elasticApmAwsLambda(this) } -// Reset the running state of this agent to a (relatively) clean state, to -// allow re-`.start()` and re-use of this Agent within the same process for -// testing. This is not supported outside of the test suite. -// -// Limitations: There are untracked async tasks (a) between span.end() and -// and transport.sendSpan(); and (b) between agent.captureError() and -// transport.sendError(). Currently there is no way to wait for completion of -// those tasks in this method. There is code in each of ins.addEndedSpan and -// agent.captureError to mitigate this. -Agent.prototype._testReset = function () { - this._errorFilters = new Filters() - this._transactionFilters = new Filters() - this._spanFilters = new Filters() - if (this._transport && this._transport.destroy) { - this._transport.destroy() - } - this._transport = null - global[symbols.agentInitialized] = null // Mark as not yet started. - if (this._uncaughtExceptionListener) { - process.removeListener('uncaughtException', this._uncaughtExceptionListener) - } - this._metrics.stop() - this._instrumentation.testReset() -} - Object.defineProperty(Agent.prototype, 'currentTransaction', { get () { return this._instrumentation.currentTransaction @@ -96,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 @@ -240,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() @@ -419,12 +433,6 @@ Agent.prototype.captureError = function (err, opts, cb) { span._setOutcomeFromErrorCapture(constants.OUTCOME_FAILURE) } - // Ensure an inflight `agent.captureError()` across a call to - // `agent._testReset()` does not impact testing of the agent after that call - // -- by using *current* values of _errorFilters and _transport. - const errorFilters = agent._errorFilters - const transport = agent._transport - // Move the remaining captureError processing to a later tick because: // 1. This allows the calling code to continue processing. For example, for // Express instrumentation this can significantly improve latency in @@ -485,7 +493,7 @@ Agent.prototype.captureError = function (err, opts, cb) { // _err is always null from createAPMError. const id = apmError.id - apmError = errorFilters.process(apmError) + apmError = agent._errorFilters.process(apmError) if (!apmError) { agent.logger.debug('error ignored by filter %o', { id }) if (cb) { @@ -494,9 +502,9 @@ Agent.prototype.captureError = function (err, opts, cb) { return } - if (transport) { + if (agent._transport) { agent.logger.info('Sending error to Elastic APM: %o', { id }) - transport.sendError(apmError, function () { + agent._transport.sendError(apmError, function () { agent.flush(function (flushErr) { if (cb) { cb(flushErr, id) diff --git a/lib/config.js b/lib/config.js index f49f2bad17..7b69576e3a 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 @@ -720,3 +713,11 @@ function getBaseClientConfig (conf, agent) { cloudMetadataFetcher: (new CloudMetadata(cloudProvider, conf.logger, conf.serviceName)) } } + +module.exports = config + +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' +config.DEFAULTS = DEFAULTS diff --git a/lib/instrumentation/async-hooks.js b/lib/instrumentation/async-hooks.js index ded9cf8d14..873c0c84a3 100644 --- a/lib/instrumentation/async-hooks.js +++ b/lib/instrumentation/async-hooks.js @@ -54,15 +54,15 @@ module.exports = function (ins) { } }) - shimmer.wrap(ins, 'testReset', function (origTestReset) { - return function wrappedTestReset () { + shimmer.wrap(ins, 'stop', function (origStop) { + return function wrappedStop () { asyncHook.disable() activeTransactions = new Map() activeSpans = new Map() contexts = new WeakMap() shimmer.unwrap(ins, 'addEndedTransaction') - shimmer.unwrap(ins, 'testReset') - return origTestReset.call(this) + shimmer.unwrap(ins, 'stop') + return origStop.call(this) } }) diff --git a/lib/instrumentation/index.js b/lib/instrumentation/index.js index 37755e5a4c..5a88319263 100644 --- a/lib/instrumentation/index.js +++ b/lib/instrumentation/index.js @@ -86,14 +86,12 @@ function Instrumentation (agent) { } } -// Reset internal context tracking state for (relatively) clean re-use of this -// Instrumentation in the same process. Used for testing. +// 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 user or test code that already has references to -// patched or unpatched exports from those modules. That may mean some -// automatic instrumentation may not work. -Instrumentation.prototype.testReset = function () { +// 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 @@ -254,19 +252,13 @@ Instrumentation.prototype.addEndedSpan = function (span) { } else if (this._started) { agent.logger.debug('encoding span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type }) - // Ensure an inflight `span._encode()` across a call to `agent._testReset()` - // does not impact testing of the agent after that call -- by using - // *current* values of _spanFilters and _transport. - const spanFilters = agent._spanFilters - const transport = agent._transport - span._encode(function (err, payload) { if (err) { agent.logger.error('error encoding span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type, error: err.message }) return } - payload = spanFilters.process(payload) + payload = agent._spanFilters.process(payload) if (!payload) { agent.logger.debug('span ignored by filter %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type }) @@ -274,8 +266,8 @@ Instrumentation.prototype.addEndedSpan = function (span) { } agent.logger.debug('sending span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type }) - if (transport) { - transport.sendSpan(payload) + if (agent._transport) { + agent._transport.sendSpan(payload) } }) } else { diff --git a/test/agent.test.js b/test/agent.test.js index 19381fe889..4df4f95d0c 100644 --- a/test/agent.test.js +++ b/test/agent.test.js @@ -1,11 +1,10 @@ '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()` it at the top of file. Also, the first test -// relies on testing state before the first start. -const agent = require('..') +// 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') @@ -14,11 +13,12 @@ var os = require('os') var { sync: containerInfo } = require('container-info') var test = require('tape') +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() || {}) // Options to pass to `agent.start()` to turn off some default agent behavior @@ -89,6 +89,8 @@ function deep (depth, n) { // ---- tests test('#getServiceName()', function (t) { + const agent = new Agent() + // 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') @@ -105,14 +107,14 @@ test('#getServiceName()', function (t) { t.strictEqual(agent.getServiceName(), 'myServiceName') t.strictEqual(agent.getServiceName(), agent._conf.serviceName) - agent._testReset() + agent.destroy() t.end() }) test('#setFramework()', function (t) { // Use `agentOpts` instead of `agentOptsNoopTransport` because this test is // reaching into `agent._transport` internals. - agent.start(agentOpts) + const agent = new Agent().start(agentOpts) t.strictEqual(agent._conf.frameworkName, undefined) t.strictEqual(agent._conf.frameworkVersion, undefined) @@ -143,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._testReset() + agent.destroy() t.end() }) test('#startTransaction()', function (t) { t.test('name, type, subtype and action', function (t) { - agent.start(agentOptsNoopTransport) + 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._testReset() + agent.destroy() t.end() }) t.test('options.startTime', function (t) { - agent.start(agentOptsNoopTransport) + 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._testReset() + agent.destroy() t.end() }) t.test('options.childOf', function (t) { - agent.start(agentOptsNoopTransport) + 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') @@ -180,7 +182,7 @@ 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._testReset() + agent.destroy() t.end() }) @@ -189,42 +191,42 @@ test('#startTransaction()', function (t) { test('#endTransaction()', function (t) { t.test('no active transaction', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) agent.endTransaction() - agent._testReset() + agent.destroy() t.end() }) t.test('with no result', function (t) { - agent.start(agentOptsNoopTransport) + 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._testReset() + agent.destroy() t.end() }) t.test('with explicit result', function (t) { - agent.start(agentOptsNoopTransport) + 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._testReset() + agent.destroy() t.end() }) t.test('with custom endTime', function (t) { - agent.start(agentOptsNoopTransport) + 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._testReset() + agent.destroy() t.end() }) @@ -233,50 +235,50 @@ test('#endTransaction()', function (t) { test('#currentTransaction', function (t) { t.test('no active transaction', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) t.notOk(agent.currentTransaction) - agent._testReset() + agent.destroy() t.end() }) t.test('with active transaction', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.currentTransaction, trans) agent.endTransaction() - agent._testReset() + agent.destroy() t.end() }) }) test('#currentSpan', function (t) { t.test('no active or binding span', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) t.notOk(agent.currentSpan) - agent._testReset() + agent.destroy() t.end() }) t.test('with binding span', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() var span = agent.startSpan() t.strictEqual(agent.currentSpan, span) span.end() trans.end() - agent._testReset() + agent.destroy() t.end() }) t.test('with active span', function (t) { - agent.start(agentOptsNoopTransport) + 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._testReset() + agent.destroy() t.end() }) }) @@ -286,29 +288,29 @@ test('#currentSpan', function (t) { test('#currentTraceparent', function (t) { t.test('no active transaction or span', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) t.notOk(agent.currentTraceparent) - agent._testReset() + agent.destroy() t.end() }) t.test('with active transaction', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.currentTraceparent, trans.traceparent) agent.endTransaction() - agent._testReset() + agent.destroy() t.end() }) t.test('with active span', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) agent.startTransaction() var span = agent.startSpan() t.strictEqual(agent.currentTraceparent, span.traceparent) span.end() agent.endTransaction() - agent._testReset() + agent.destroy() t.end() }) @@ -317,15 +319,15 @@ test('#currentTraceparent', function (t) { test('#currentTraceIds', function (t) { t.test('no active transaction or span', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) t.deepLooseEqual(agent.currentTraceIds, {}) t.strictEqual(agent.currentTraceIds.toString(), '') - agent._testReset() + agent.destroy() t.end() }) t.test('with active transaction', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.deepLooseEqual(agent.currentTraceIds, { 'trace.id': trans.traceId, @@ -333,12 +335,12 @@ test('#currentTraceIds', function (t) { }) t.strictEqual(agent.currentTraceIds.toString(), `trace.id=${trans.traceId} transaction.id=${trans.id}`) agent.endTransaction() - agent._testReset() + agent.destroy() t.end() }) t.test('with active span', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) agent.startTransaction() var span = agent.startSpan() t.deepLooseEqual(agent.currentTraceIds, { @@ -348,7 +350,7 @@ test('#currentTraceIds', function (t) { t.strictEqual(agent.currentTraceIds.toString(), `trace.id=${span.traceId} span.id=${span.id}`) span.end() agent.endTransaction() - agent._testReset() + agent.destroy() t.end() }) @@ -357,20 +359,20 @@ test('#currentTraceIds', function (t) { test('#setTransactionName', function (t) { t.test('no active transaction', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) t.doesNotThrow(function () { agent.setTransactionName('foo') }) - agent._testReset() + agent.destroy() t.end() }) t.test('active transaction', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() agent.setTransactionName('foo') t.strictEqual(trans.name, 'foo') - agent._testReset() + agent.destroy() t.end() }) @@ -379,14 +381,14 @@ test('#setTransactionName', function (t) { test('#startSpan()', function (t) { t.test('no active transaction', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) t.strictEqual(agent.startSpan(), null) - agent._testReset() + agent.destroy() t.end() }) t.test('active transaction', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) agent.startTransaction() var span = agent.startSpan('span-name', 'type', 'subtype', 'action') t.ok(span, 'should return a span') @@ -394,12 +396,12 @@ test('#startSpan()', function (t) { t.strictEqual(span.type, 'type') t.strictEqual(span.subtype, 'subtype') t.strictEqual(span.action, 'action') - agent._testReset() + agent.destroy() t.end() }) t.test('options.startTime', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) agent.startTransaction() var startTime = Date.now() - 1000 var span = agent.startSpan('span-with-startTime', null, { startTime }) @@ -407,12 +409,12 @@ test('#startSpan()', function (t) { 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._testReset() + agent.destroy() t.end() }) t.test('options.childOf', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) agent.startTransaction() var childOf = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' var span = agent.startSpan(null, null, { childOf }) @@ -421,7 +423,7 @@ 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._testReset() + agent.destroy() t.end() }) @@ -430,18 +432,18 @@ test('#startSpan()', function (t) { test('#setUserContext()', function (t) { t.test('no active transaction', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) t.strictEqual(agent.setUserContext({ foo: 1 }), false) - agent._testReset() + agent.destroy() t.end() }) t.test('active transaction', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.setUserContext({ foo: 1 }), true) t.deepEqual(trans._user, { foo: 1 }) - agent._testReset() + agent.destroy() t.end() }) @@ -450,18 +452,18 @@ test('#setUserContext()', function (t) { test('#setCustomContext()', function (t) { t.test('no active transaction', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) t.strictEqual(agent.setCustomContext({ foo: 1 }), false) - agent._testReset() + agent.destroy() t.end() }) t.test('active transaction', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.setCustomContext({ foo: 1 }), true) t.deepEqual(trans._custom, { foo: 1 }) - agent._testReset() + agent.destroy() t.end() }) @@ -470,23 +472,23 @@ test('#setCustomContext()', function (t) { test('#setLabel()', function (t) { t.test('no active transaction', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) t.strictEqual(agent.setLabel('foo', 1), false) - agent._testReset() + agent.destroy() t.end() }) t.test('active transaction', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) var trans = agent.startTransaction() t.strictEqual(agent.setLabel('foo', 1), true) t.deepEqual(trans._labels, { foo: '1' }) - agent._testReset() + agent.destroy() t.end() }) t.test('active transaction without label stringification', function (t) { - agent.start(agentOptsNoopTransport) + 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) @@ -500,7 +502,7 @@ test('#setLabel()', function (t) { 'boolean-false': false, string: 'a custom label' }) - agent._testReset() + agent.destroy() t.end() }) @@ -509,28 +511,28 @@ test('#setLabel()', function (t) { test('#addLabels()', function (t) { t.test('no active transaction', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) t.strictEqual(agent.addLabels({ foo: 1 }), false) - agent._testReset() + agent.destroy() t.end() }) t.test('active transaction', function (t) { - agent.start(agentOptsNoopTransport) + 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._testReset() + agent.destroy() t.end() }) t.test('active transaction without label stringification', function (t) { - agent.start(agentOptsNoopTransport) + 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._testReset() + agent.destroy() t.end() }) @@ -555,7 +557,7 @@ test('filters', function (t) { }) t.test('#addFilter() - error', function (t) { - agent.start(filterAgentOpts) + const agent = new Agent().start(filterAgentOpts) // Test filters are run in the order specified... agent.addFilter(function (obj) { t.strictEqual(obj.exception.message, 'foo') @@ -584,14 +586,14 @@ test('filters', function (t) { t.strictEqual(data.context.custom.order, 2) apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('#addFilter() - transaction', function (t) { - agent.start(filterAgentOpts) + const agent = new Agent().start(filterAgentOpts) agent.addFilter(function (obj) { t.strictEqual(obj.name, 'transaction-name') t.strictEqual(++obj.context.custom.order, 1) @@ -617,13 +619,13 @@ test('filters', function (t) { t.strictEqual(data.context.custom.order, 2) apmServer.clear() - agent._testReset() + agent.destroy() t.end() }) }) t.test('#addFilter() - span', function (t) { - agent.start(filterAgentOpts) + const agent = new Agent().start(filterAgentOpts) agent.addFilter(function (obj) { t.strictEqual(obj.name, 'span-name') obj.order = 1 @@ -650,14 +652,14 @@ test('filters', function (t) { t.strictEqual(data.order, 2) apmServer.clear() - agent._testReset() + agent.destroy() t.end() }) }, 50) // Hack wait for ended span to be sent to transport. }) t.test('#addErrorFilter()', function (t) { - agent.start(filterAgentOpts) + const agent = new Agent().start(filterAgentOpts) agent.addTransactionFilter(function () { t.fail('should not call transaction filter') }) @@ -690,14 +692,14 @@ test('filters', function (t) { t.strictEqual(data.context.custom.order, 2) apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('#addTransactionFilter()', function (t) { - agent.start(filterAgentOpts) + const agent = new Agent().start(filterAgentOpts) agent.addErrorFilter(function () { t.fail('should not call error filter') }) @@ -729,13 +731,13 @@ test('filters', function (t) { t.strictEqual(data.context.custom.order, 2) apmServer.clear() - agent._testReset() + agent.destroy() t.end() }) }) t.test('#addSpanFilter()', function (t) { - agent.start(filterAgentOpts) + const agent = new Agent().start(filterAgentOpts) agent.addErrorFilter(function () { t.fail('should not call error filter') }) @@ -768,14 +770,14 @@ test('filters', function (t) { t.strictEqual(data.order, 2) apmServer.clear() - agent._testReset() + agent.destroy() t.end() }) }, 50) // Hack wait for ended span to be sent to transport. }) t.test('#addMetadataFilter()', function (t) { - agent.start(filterAgentOpts) + const agent = new Agent().start(filterAgentOpts) agent.addErrorFilter(function () { t.fail('should not call error filter') }) @@ -805,7 +807,7 @@ test('filters', function (t) { t.strictEqual(data.order, 2) apmServer.clear() - agent._testReset() + agent.destroy() t.end() }) }) @@ -814,7 +816,7 @@ test('filters', function (t) { falsyValues.forEach(falsy => { t.test(`#addFilter() - abort with '${String(falsy)}'`, function (t) { let calledFirstFilter = false - agent.start(filterAgentOpts) + const agent = new Agent().start(filterAgentOpts) agent.addFilter(function (obj) { calledFirstFilter = true return falsy @@ -826,14 +828,14 @@ test('filters', function (t) { t.ok(calledFirstFilter, 'called first filter') t.equal(apmServer.requests.length, 0, 'APM server did not receive a request') apmServer.clear() - agent._testReset() + agent.destroy() t.end() }) }) t.test(`#addErrorFilter() - abort with '${String(falsy)}'`, function (t) { let calledFirstFilter = false - agent.start(filterAgentOpts) + const agent = new Agent().start(filterAgentOpts) agent.addErrorFilter(function (obj) { calledFirstFilter = true return falsy @@ -845,14 +847,14 @@ test('filters', function (t) { t.ok(calledFirstFilter, 'called first filter') t.equal(apmServer.requests.length, 0, 'APM server did not receive a request') apmServer.clear() - agent._testReset() + agent.destroy() t.end() }) }) t.test(`#addTransactionFilter() - abort with '${String(falsy)}'`, function (t) { let calledFirstFilter = false - agent.start(filterAgentOpts) + const agent = new Agent().start(filterAgentOpts) agent.addTransactionFilter(function (obj) { calledFirstFilter = true return falsy @@ -866,14 +868,14 @@ test('filters', function (t) { t.ok(calledFirstFilter, 'called first filter') t.equal(apmServer.requests.length, 0, 'APM server did not receive a request') apmServer.clear() - agent._testReset() + agent.destroy() t.end() }) }) t.test(`#addSpanFilter() - abort with '${String(falsy)}'`, function (t) { let calledFirstFilter = false - agent.start(filterAgentOpts) + const agent = new Agent().start(filterAgentOpts) agent.addSpanFilter(function (obj) { calledFirstFilter = true return falsy @@ -889,7 +891,7 @@ test('filters', function (t) { t.ok(calledFirstFilter, 'called first filter') t.equal(apmServer.requests.length, 0, 'APM server did not receive a request') apmServer.clear() - agent._testReset() + agent.destroy() t.end() }) }, 50) // Hack wait for ended span to be sent to transport. @@ -907,32 +909,33 @@ test('filters', function (t) { test('#flush()', function (t) { t.test('start not called', function (t) { t.plan(2) + 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(agentOptsNoopTransport) wasn\'t called') - agent._testReset() + t.pass('should call flush callback even if const agent = new Agent().start(agentOptsNoopTransport) wasn\'t called') + agent.destroy() t.end() }) }) t.test('start called, but agent inactive', function (t) { t.plan(2) - 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._testReset() + agent.destroy() t.end() }) }) t.test('agent started, but no data in the queue', function (t) { t.plan(2) - agent.start(agentOptsNoopTransport) + 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._testReset() + agent.destroy() t.end() }) }) @@ -940,7 +943,7 @@ test('#flush()', function (t) { t.test('flush with agent started, and data in the queue', function (t) { const apmServer = new MockAPMServer() apmServer.start(function (serverUrl) { - agent.start(Object.assign( + const agent = new Agent().start(Object.assign( {}, agentOpts, { serverUrl } @@ -955,7 +958,7 @@ test('#flush()', function (t) { t.equal(trans.name, 'foo', 'the transaction has the expected name') apmServer.close() - agent._testReset() + agent.destroy() t.end() }) }) @@ -982,7 +985,7 @@ test('#captureError()', function (t) { }) t.test('with callback', function (t) { - agent.start(ceAgentOpts) + 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') @@ -992,13 +995,13 @@ test('#captureError()', function (t) { t.strictEqual(data.exception.message, 'with callback') apmServer.clear() - agent._testReset() + agent.destroy() t.end() }) }) t.test('without callback', function (t) { - agent.start(ceAgentOpts) + 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') @@ -1007,39 +1010,39 @@ test('#captureError()', function (t) { t.strictEqual(data.exception.message, 'without callback') apmServer.clear() - agent._testReset() + agent.destroy() t.end() }, 50) // Hack wait for captured error to be encoded and sent. }) t.test('generate error id', function (t) { - agent.start(ceAgentOpts) + 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._testReset() + agent.destroy() t.end() }) }) t.test('should send a plain text message to the server', function (t) { - agent.start(ceAgentOpts) + 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._testReset() + agent.destroy() t.end() }) }) t.test('should use `param_message` as well as `message` if given an object as 1st argument', function (t) { - agent.start(ceAgentOpts) + 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') @@ -1048,14 +1051,14 @@ test('#captureError()', function (t) { t.strictEqual(data.log.param_message, 'Hello %s') apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('should not fail on a non string err.message', function (t) { - agent.start(ceAgentOpts) + const agent = new Agent().start(ceAgentOpts) var err = new Error() err.message = { foo: 'bar' } agent.captureError(err, function () { @@ -1064,13 +1067,13 @@ test('#captureError()', function (t) { t.strictEqual(data.exception.message, '[object Object]') apmServer.clear() - agent._testReset() + agent.destroy() t.end() }) }) t.test('should allow custom log message together with exception', function (t) { - agent.start(ceAgentOpts) + 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') @@ -1079,30 +1082,30 @@ test('#captureError()', function (t) { t.strictEqual(data.log.message, 'bar') apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('should adhere to default stackTraceLimit', function (t) { - agent.start(ceAgentOpts) + 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, 50) + t.strictEqual(data.exception.stacktrace.length, config.DEFAULTS.stackTraceLimit) t.strictEqual(data.exception.stacktrace[0].context_line.trim(), 'return new Error()') apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('should adhere to custom stackTraceLimit', function (t) { - agent.start(Object.assign( + const agent = new Agent().start(Object.assign( {}, ceAgentOpts, { stackTraceLimit: 5 } @@ -1115,14 +1118,14 @@ test('#captureError()', function (t) { t.strictEqual(data.exception.stacktrace[0].context_line.trim(), 'return new Error()') apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('should merge context', function (t) { - agent.start(ceAgentOpts) + 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) @@ -1139,14 +1142,14 @@ test('#captureError()', function (t) { t.deepEqual(data.context.custom, { a: 3, b: 2, merge: { shallow: true } }) apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('capture location stack trace - off (error)', function (t) { - agent.start(Object.assign( + const agent = new Agent().start(Object.assign( {}, ceAgentOpts, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_NEVER } @@ -1160,14 +1163,14 @@ test('#captureError()', function (t) { assertStackTrace(t, data.exception.stacktrace) apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('capture location stack trace - off (string)', function (t) { - agent.start(Object.assign( + const agent = new Agent().start(Object.assign( {}, ceAgentOpts, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_NEVER } @@ -1181,14 +1184,14 @@ test('#captureError()', function (t) { t.notOk('exception' in data, 'should not have an exception') apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('capture location stack trace - off (param msg)', function (t) { - agent.start(Object.assign( + const agent = new Agent().start(Object.assign( {}, ceAgentOpts, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_NEVER } @@ -1202,14 +1205,14 @@ test('#captureError()', function (t) { t.notOk('exception' in data, 'should not have an exception') apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('capture location stack trace - non-errors (error)', function (t) { - agent.start(Object.assign( + const agent = new Agent().start(Object.assign( {}, ceAgentOpts, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES } @@ -1223,14 +1226,14 @@ test('#captureError()', function (t) { assertStackTrace(t, data.exception.stacktrace) apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('capture location stack trace - non-errors (string)', function (t) { - agent.start(Object.assign( + const agent = new Agent().start(Object.assign( {}, ceAgentOpts, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES } @@ -1244,14 +1247,14 @@ test('#captureError()', function (t) { assertStackTrace(t, data.log.stacktrace) apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('capture location stack trace - non-errors (param msg)', function (t) { - agent.start(Object.assign( + const agent = new Agent().start(Object.assign( {}, ceAgentOpts, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES } @@ -1265,14 +1268,14 @@ test('#captureError()', function (t) { assertStackTrace(t, data.log.stacktrace) apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('capture location stack trace - all (error)', function (t) { - agent.start(Object.assign( + const agent = new Agent().start(Object.assign( {}, ceAgentOpts, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS } @@ -1287,14 +1290,14 @@ test('#captureError()', function (t) { assertStackTrace(t, data.exception.stacktrace) apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('capture location stack trace - all (string)', function (t) { - agent.start(Object.assign( + const agent = new Agent().start(Object.assign( {}, ceAgentOpts, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS } @@ -1308,14 +1311,14 @@ test('#captureError()', function (t) { assertStackTrace(t, data.log.stacktrace) apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('capture location stack trace - all (param msg)', function (t) { - agent.start(Object.assign( + const agent = new Agent().start(Object.assign( {}, ceAgentOpts, { captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS } @@ -1329,30 +1332,32 @@ test('#captureError()', function (t) { assertStackTrace(t, data.log.stacktrace) apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('capture error before agent is started - with callback', function (t) { + const agent = new Agent() agent.captureError(new Error('foo'), function (err) { t.strictEqual(err.message, 'cannot capture error before agent is started') - agent._testReset() + agent.destroy() t.end() }) }) t.test('capture error before agent is started - without callback', function (t) { + const agent = new Agent() agent.captureError(new Error('foo')) - agent._testReset() + agent.destroy() t.end() }) // XXX This one is relying on the agent.captureError change to stash `this._transport` // so delayed-processing error from the previous one or two test cases don't bleed into this one. t.test('include valid context ids and sampled flag', function (t) { - agent.start(ceAgentOpts) + const agent = new Agent().start(ceAgentOpts) const trans = agent.startTransaction('foo') const span = agent.startSpan('bar') agent.captureError( @@ -1370,14 +1375,14 @@ test('#captureError()', function (t) { t.strictEqual(data.transaction.sampled, true, 'is sampled') apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('custom timestamp', function (t) { - agent.start(ceAgentOpts) + const agent = new Agent().start(ceAgentOpts) const timestamp = Date.now() - 1000 agent.captureError( new Error('with callback'), @@ -1389,14 +1394,14 @@ test('#captureError()', function (t) { t.strictEqual(data.timestamp, timestamp * 1000) apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('options.request', function (t) { - agent.start(ceAgentOpts) + const agent = new Agent().start(ceAgentOpts) const req = new http.IncomingMessage() req.httpVersion = '1.1' @@ -1434,7 +1439,7 @@ test('#captureError()', function (t) { }) apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) @@ -1443,7 +1448,7 @@ test('#captureError()', function (t) { // 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) { - agent.start(Object.assign( + const agent = new Agent().start(Object.assign( {}, ceAgentOpts, { captureBody: 'errors' } @@ -1480,14 +1485,14 @@ test('#captureError()', function (t) { }) apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) }) t.test('options.response', function (t) { - agent.start(ceAgentOpts) + const agent = new Agent().start(ceAgentOpts) const req = new http.IncomingMessage() const res = new http.ServerResponse(req) @@ -1522,7 +1527,7 @@ test('#captureError()', function (t) { }) apmServer.clear() - agent._testReset() + agent.destroy() t.end() } ) @@ -1539,30 +1544,30 @@ test('#captureError()', function (t) { test('#handleUncaughtExceptions()', function (t) { t.test('should add itself to the uncaughtException event list', function (t) { t.strictEqual(process._events.uncaughtException, undefined) - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) t.strictEqual(process._events.uncaughtException, undefined) agent.handleUncaughtExceptions() t.strictEqual(process._events.uncaughtException.length, 1) - agent._testReset() + agent.destroy() t.end() }) t.test('should not add more than one listener for the uncaughtException event', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) agent.handleUncaughtExceptions() var before = process._events.uncaughtException.length agent.handleUncaughtExceptions() t.strictEqual(process._events.uncaughtException.length, before) - agent._testReset() + agent.destroy() t.end() }) t.test('should send an uncaughtException to server', function (t) { const apmServer = new MockAPMServer() apmServer.start(function (serverUrl) { - agent.start(Object.assign( + const agent = new Agent().start(Object.assign( {}, agentOpts, { serverUrl } @@ -1586,7 +1591,7 @@ test('#handleUncaughtExceptions()', function (t) { t.equal(handlerErr.message, 'uncaught') apmServer.close() - agent._testReset() + agent.destroy() t.end() }) }, 50) // Hack wait for the agent's handler to finish captureError. @@ -1598,27 +1603,27 @@ test('#handleUncaughtExceptions()', function (t) { test('#active: false', function (t) { t.test('should not error when started in an inactive state', function (t) { - var client = agent.start({ active: false }) - t.ok(client.startTransaction()) - t.doesNotThrow(() => client.endTransaction()) - - agent._testReset() + 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) { + 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._testReset() + agent.destroy() t.end() }) t.test('#addPatch(name, moduleName)', function (t) { + const agent = new Agent() agent.clearPatches('express') agent.start(agentOptsNoopTransport) @@ -1630,11 +1635,12 @@ test('patches', function (t) { delete require.cache[require.resolve('express')] t.deepEqual(require('express'), patch(before)) - agent._testReset() + agent.destroy() t.end() }) t.test('#addPatch(name, function) - does not exist', function (t) { + const agent = new Agent() agent.clearPatches('express') agent.start(agentOptsNoopTransport) @@ -1653,12 +1659,12 @@ test('patches', function (t) { delete require.cache[require.resolve('express')] t.deepEqual(require('express'), replacement) - agent._testReset() + agent.destroy() t.end() }) t.test('#removePatch(name, handler)', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) t.notOk(agent._instrumentation._patches.has('does-not-exist')) @@ -1673,13 +1679,13 @@ test('patches', function (t) { agent.removePatch('does-not-exist', handler) t.notOk(agent._instrumentation._patches.has('does-not-exist')) - agent._testReset() + agent.destroy() t.end() }) }) test('#registerMetric(name, labels, callback)', function (t) { - agent.start(agentOptsNoopTransport) + const agent = new Agent().start(agentOptsNoopTransport) const mockMetrics = { calledCount: 0, @@ -1722,6 +1728,6 @@ test('#registerMetric(name, labels, callback)', function (t) { t.strictEqual(mockMetrics.labels, undefined) t.strictEqual(mockMetrics.cbValue, 6789) - agent._testReset() + agent.destroy() t.end() }) From 7b96ccc83882d7af124025f4688eb86e0be06498 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Thu, 26 Aug 2021 14:59:18 -0700 Subject: [PATCH 3/8] minor improvements, and reduce the diff --- lib/instrumentation/async-hooks.js | 8 ++++---- lib/instrumentation/index.js | 5 +---- test/agent.test.js | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/instrumentation/async-hooks.js b/lib/instrumentation/async-hooks.js index 873c0c84a3..6f0c1f4e38 100644 --- a/lib/instrumentation/async-hooks.js +++ b/lib/instrumentation/async-hooks.js @@ -5,8 +5,8 @@ const shimmer = require('./shimmer') module.exports = function (ins) { const asyncHook = asyncHooks.createHook({ init, before, destroy }) - let activeSpans = new Map() - let activeTransactions = new Map() + const activeSpans = new Map() + const activeTransactions = new Map() let contexts = new WeakMap() Object.defineProperty(ins, 'currentTransaction', { @@ -57,8 +57,8 @@ module.exports = function (ins) { shimmer.wrap(ins, 'stop', function (origStop) { return function wrappedStop () { asyncHook.disable() - activeTransactions = new Map() - activeSpans = new Map() + activeTransactions.clear() + activeSpans.clear() contexts = new WeakMap() shimmer.unwrap(ins, 'addEndedTransaction') shimmer.unwrap(ins, 'stop') diff --git a/lib/instrumentation/index.js b/lib/instrumentation/index.js index 5a88319263..61c6a02cd5 100644 --- a/lib/instrumentation/index.js +++ b/lib/instrumentation/index.js @@ -251,7 +251,6 @@ Instrumentation.prototype.addEndedSpan = function (span) { // Save effort and logging if disableSend=true. } else if (this._started) { agent.logger.debug('encoding span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type }) - span._encode(function (err, payload) { if (err) { agent.logger.error('error encoding span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type, error: err.message }) @@ -266,9 +265,7 @@ Instrumentation.prototype.addEndedSpan = function (span) { } agent.logger.debug('sending span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type }) - if (agent._transport) { - agent._transport.sendSpan(payload) - } + if (agent._transport) agent._transport.sendSpan(payload) }) } else { agent.logger.debug('ignoring span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type }) diff --git a/test/agent.test.js b/test/agent.test.js index 4df4f95d0c..45f3626da0 100644 --- a/test/agent.test.js +++ b/test/agent.test.js @@ -912,7 +912,7 @@ test('#flush()', function (t) { 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 const agent = new Agent().start(agentOptsNoopTransport) wasn\'t called') + t.pass('should call flush callback even if agent.start() wasn\'t called') agent.destroy() t.end() }) From a9c20bce0fe67a08ebf8796d639e14c89fa97ac3 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Thu, 26 Aug 2021 15:18:40 -0700 Subject: [PATCH 4/8] fix breakage in config.js exports from earlier change --- lib/config.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/config.js b/lib/config.js index 7b69576e3a..22b3f66693 100644 --- a/lib/config.js +++ b/lib/config.js @@ -31,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, @@ -40,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, @@ -555,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) { @@ -678,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 @@ -714,10 +719,10 @@ function getBaseClientConfig (conf, agent) { } } +// Exports. module.exports = config - -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' -config.DEFAULTS = DEFAULTS +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 From 6beb6acbfb64eeab14b7ee3dc93918e613276dda Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Thu, 26 Aug 2021 16:45:33 -0700 Subject: [PATCH 5/8] remove _apm_server.js and _agent.js usage from test/config.test.js --- test/config.test.js | 566 +++++++++++++++++++++++++------------------- 1 file changed, 323 insertions(+), 243 deletions(-) diff --git a/test/config.test.js b/test/config.test.js index 287b8b456c..8671182fa8 100644 --- a/test/config.test.js +++ b/test/config.test.js @@ -14,16 +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' +// 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], @@ -90,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] @@ -107,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]) @@ -121,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 @@ -154,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]) @@ -168,6 +188,7 @@ optionFixtures.forEach(function (fixture) { delete process.env[envName] } + agent.destroy() t.end() }) } @@ -176,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 { @@ -189,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) @@ -234,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() }) @@ -243,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() }) }) @@ -259,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() }) }) @@ -277,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() }) }) @@ -361,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() }) }) @@ -398,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']) @@ -453,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, @@ -468,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() }) @@ -508,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)) ` }, @@ -631,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() + } + ) + }) }) }) @@ -680,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' } @@ -707,9 +742,19 @@ 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() + }) }) }) +if (false) // XXX skip slow test('disableInstrumentations', function (t) { var expressGraphqlVersion = require('express-graphql/package.json').version var esVersion = require('@elastic/elasticsearch/package.json').version @@ -746,12 +791,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() @@ -766,6 +811,7 @@ test('disableInstrumentations', function (t) { t.deepEqual(selectionSet, found, 'disabled all selected modules') + agent.destroy() t.end() }) } @@ -786,54 +832,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') @@ -841,7 +871,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() + }, 50) // Hack wait for ended span and captured error to be sent to transport. }) test('addPatch', function (t) { @@ -850,14 +890,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() }) @@ -865,42 +909,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() }) }) @@ -1034,52 +1089,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, @@ -1091,12 +1164,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, @@ -1113,13 +1191,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, @@ -1136,6 +1215,7 @@ test('should accept and normalize ignoreMessageQueues', function (suite) { agent._conf.ignoreMessageQueuesRegExp[0].test('faooooo'), 'wildcard converted to regular expression' ) + agent.destroy() t.end() }) From b5d21813ff89a3071a1b880e90c71fa541e4c992 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Thu, 26 Aug 2021 16:49:31 -0700 Subject: [PATCH 6/8] fix make check; remove debugging XXXs --- test/agent.test.js | 2 -- test/config.test.js | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/test/agent.test.js b/test/agent.test.js index 45f3626da0..802bf53939 100644 --- a/test/agent.test.js +++ b/test/agent.test.js @@ -1354,8 +1354,6 @@ test('#captureError()', function (t) { t.end() }) - // XXX This one is relying on the agent.captureError change to stash `this._transport` - // so delayed-processing error from the previous one or two test cases don't bleed into this one. t.test('include valid context ids and sampled flag', function (t) { const agent = new Agent().start(ceAgentOpts) const trans = agent.startTransaction('foo') diff --git a/test/config.test.js b/test/config.test.js index 8671182fa8..1379213500 100644 --- a/test/config.test.js +++ b/test/config.test.js @@ -754,7 +754,6 @@ usePathAsTransactionNameTests.forEach(function (usePathAsTransactionNameTest) { }) }) -if (false) // XXX skip slow test('disableInstrumentations', function (t) { var expressGraphqlVersion = require('express-graphql/package.json').version var esVersion = require('@elastic/elasticsearch/package.json').version @@ -895,7 +894,7 @@ test('addPatch', function (t) { {}, agentOptsNoopTransport, { - addPatch: 'express=./test/_patch.js', + addPatch: 'express=./test/_patch.js' } )) From cdc05b05e8a12b625939dc7a079b426f7700f5c5 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Thu, 26 Aug 2021 16:51:26 -0700 Subject: [PATCH 7/8] drop test/_apm_server.js, no longer used --- test/_apm_server.js | 93 --------------------------------------------- 1 file changed, 93 deletions(-) delete mode 100644 test/_apm_server.js 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) - }) -} From 6810173956cfc2b34d6eca5b3876ba746e73ed3b Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Thu, 26 Aug 2021 17:25:53 -0700 Subject: [PATCH 8/8] deprecation note; bump timeout because CI is sometimes too slow (saw one failure with 50ms) --- test/_agent.js | 12 ++++++++++++ test/agent.test.js | 10 +++++----- test/config.test.js | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) 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/agent.test.js b/test/agent.test.js index 802bf53939..d3d07660fc 100644 --- a/test/agent.test.js +++ b/test/agent.test.js @@ -655,7 +655,7 @@ test('filters', function (t) { agent.destroy() t.end() }) - }, 50) // Hack wait for ended span to be sent to transport. + }, 200) // Hack wait for ended span to be sent to transport. }) t.test('#addErrorFilter()', function (t) { @@ -773,7 +773,7 @@ test('filters', function (t) { agent.destroy() t.end() }) - }, 50) // Hack wait for ended span to be sent to transport. + }, 200) // Hack wait for ended span to be sent to transport. }) t.test('#addMetadataFilter()', function (t) { @@ -894,7 +894,7 @@ test('filters', function (t) { agent.destroy() t.end() }) - }, 50) // Hack wait for ended span to be sent to transport. + }, 200) // Hack wait for ended span to be sent to transport. }) }) @@ -1012,7 +1012,7 @@ test('#captureError()', function (t) { apmServer.clear() agent.destroy() t.end() - }, 50) // Hack wait for captured error to be encoded and sent. + }, 200) // Hack wait for captured error to be encoded and sent. }) t.test('generate error id', function (t) { @@ -1592,7 +1592,7 @@ test('#handleUncaughtExceptions()', function (t) { agent.destroy() t.end() }) - }, 50) // Hack wait for the agent's handler to finish captureError. + }, 200) // Hack wait for the agent's handler to finish captureError. }) }) diff --git a/test/config.test.js b/test/config.test.js index 1379213500..cc4af97d49 100644 --- a/test/config.test.js +++ b/test/config.test.js @@ -880,7 +880,7 @@ test('custom transport', function (t) { assertEncodedError(t, error, myTransport.errors[0], trans, span) agent.destroy() t.end() - }, 50) // Hack wait for ended span and captured error to be sent to transport. + }, 200) // Hack wait for ended span and captured error to be sent to transport. }) test('addPatch', function (t) {