diff --git a/lib/instrumentation/core/dns.js b/lib/instrumentation/core/dns.js index f0b525e907..8fed703b3b 100644 --- a/lib/instrumentation/core/dns.js +++ b/lib/instrumentation/core/dns.js @@ -4,28 +4,50 @@ */ 'use strict' - -const { RecorderSpec } = require('../../../lib/shim/specs') +// eslint-disable-next-line n/no-unsupported-features/node-builtins +const { tracingChannel } = require('node:diagnostics_channel') +const { INSTRUMENTED_METHODS, RESOLVE_METHODS } = require('#agentlib/subscribers/core/dns/constants.js') +const shimmer = require('#agentlib/shimmer.js') module.exports = initialize -function initialize(agent, dns, moduleName, shim) { - const methods = [ - 'lookup', - 'resolve', - 'resolve4', - 'resolve6', - 'resolveCname', - 'resolveMx', - 'resolveNaptr', - 'resolveNs', - 'resolvePtr', - 'resolveSrv', - 'resolveTxt', - 'reverse' - ] +function initialize(_agent, dns) { + shimmer.wrapMethod(dns, 'dns', INSTRUMENTED_METHODS, function wrapMethod(original, method) { + const channel = tracingChannel(`nr:dns:${method}`) + return function wrappedMethod(...args) { + const callback = args.at(-1) + const callbackName = callback?.name || '' + const data = { name: `dns.${method}`, callbackName } + return channel.traceCallback(original, -1, data, this, ...args) + } + }) - shim.record(dns, methods, function recordDnsMethod(shim, fn, name) { - return new RecorderSpec({ name: 'dns.' + name, callback: shim.LAST }) + // All code below can be added separately. it adds the following: + // * instrumentation on resolve methods from `dns.Resolver.prototype` + // * instrumentation on `dns.promises.` + // * instrumentation on resolve methods from `dns.promises.Resolver.prototype` + shimmer.wrapMethod(dns.Resolver.prototype, 'dns', RESOLVE_METHODS, function wrapMethod(original, method) { + const channel = tracingChannel(`nr:dns:${method}`) + return function wrappedMethod(...args) { + const callback = args.at(-1) + const callbackName = callback?.name || '' + const data = { name: `dns.${method}`, callbackName } + return channel.traceCallback(original, -1, data, this, ...args) + } + }) + + shimmer.wrapMethod(dns.promises, 'dns', INSTRUMENTED_METHODS, function wrapMethod(original, method) { + const channel = tracingChannel(`nr:promises:dns:${method}`) + return function wrappedMethod(...args) { + const data = { name: `dns.${method}` } + return channel.tracePromise(original, data, this, ...args) + } + }) + shimmer.wrapMethod(dns.promises.Resolver.prototype, 'dns', RESOLVE_METHODS, function wrapMethod(original, method) { + const channel = tracingChannel(`nr:promises:dns:${method}`) + return function wrappedMethod(...args) { + const data = { name: `dns.${method}` } + return channel.tracePromise(original, data, this, ...args) + } }) } diff --git a/lib/subscriber-configs.js b/lib/subscriber-configs.js index b52599868f..032c61a9fd 100644 --- a/lib/subscriber-configs.js +++ b/lib/subscriber-configs.js @@ -17,6 +17,7 @@ const subscribers = { ...require('./subscribers/bunyan/config'), ...require('./subscribers/cassandra-driver/config'), ...require('./subscribers/connect/config'), + ...require('./subscribers/core/dns/config'), ...require('./subscribers/elasticsearch/config'), ...require('./subscribers/express/config'), ...require('./subscribers/fastify/config'), diff --git a/lib/subscribers/base.js b/lib/subscribers/base.js index bf469a7779..d4d33d3659 100644 --- a/lib/subscribers/base.js +++ b/lib/subscribers/base.js @@ -282,7 +282,7 @@ class Subscriber { } const ctx = this.agent.tracer.getContext() if (this.requireActiveTx && !ctx?.transaction?.isActive()) { - this.logger.trace('Not recording event for %s, transaction is not active', this.package) + this.logger.trace('Not recording event for %s, transaction is not active', this.packageName) return ctx } diff --git a/lib/subscribers/core/README.md b/lib/subscribers/core/README.md new file mode 100644 index 0000000000..a037e9006e --- /dev/null +++ b/lib/subscribers/core/README.md @@ -0,0 +1 @@ +Houses core instrumentation. It differs from third-party because we are still monkey patching code but emitting it over tracing channel. More to come on this documentation. diff --git a/lib/subscribers/core/base.js b/lib/subscribers/core/base.js new file mode 100644 index 0000000000..97971487e7 --- /dev/null +++ b/lib/subscribers/core/base.js @@ -0,0 +1,122 @@ +/* + * Copyright 2026 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +// eslint-disable-next-line n/no-unsupported-features/node-builtins +const { tracingChannel } = require('node:diagnostics_channel') + +class BaseCoreSubscriber { + constructor({ agent, logger, packageName, instrumentedMethods = [], hasCallback = false, prefix }) { + this.agent = agent + this.config = agent.config + this.logger = logger.child({ component: `${packageName}-subscriber` }) + this.instrumentedMethods = instrumentedMethods + this.packageName = packageName + this.hasCallback = hasCallback + this.prefix = prefix ? `nr:${prefix}` : 'nr' + this.channels = this.buildChannels() + this.store = agent.tracer._contextManager._asyncLocalStorage + this.handlers = { + asyncEnd: this.asyncEnd.bind(this) + } + + if (hasCallback) { + this.handlers.end = this.end.bind(this) + } + } + + buildChannels() { + return this.instrumentedMethods.map((method) => tracingChannel(`${this.prefix}:${this.packageName}:${method}`)) + } + + get enabled() { + return this.config.instrumentation[this.packageName].enabled === true + } + + createSegment({ name, recorder, ctx }) { + const parent = ctx.segment + + const segment = this.agent.tracer.createSegment({ + name, + parent, + recorder, + transaction: ctx.transaction + }) + + if (segment) { + segment.start() + return ctx.enterSegment({ segment }) + } + return ctx + } + + handler(data, ctx) { + return this.createSegment({ name: data.name, ctx }) + } + + end(data) { + const ctx = this.agent.tracer.getContext() + ctx?.segment?.touch() + } + + asyncEnd(data) { + const ctx = this.agent.tracer.getContext() + ctx?.segment?.touch() + } + + enable() { + for (const channel of this.channels) { + channel.start.bindStore(this.store, (data) => { + const ctx = this.agent.tracer.getContext() + if (!ctx?.transaction?.isActive()) { + this.logger.trace('Not recording event for %s, transaction is not active', this.packageName) + return ctx + } + + // TODO: do we have to register subscriber tracking metrics like 3rd party? + return this.handler(data, ctx) + }) + + if (this.hasCallback === true) { + this.handleCallback(channel) + } + } + } + + subscribe() { + for (const channel of this.channels) { + channel.subscribe(this.handlers) + } + } + + unsubscribe() { + for (const channel of this.channels) { + channel.unsubscribe(this.handlers) + } + } + + disable() { + for (const channel of this.channels) { + channel.start.unbindStore(this.store) + channel.asyncStart.unbindStore(this.store) + } + } + + handleCallback(channel) { + channel.asyncStart.bindStore(this.store, (data) => { + const name = `Callback: ${data.callbackName}` + const ctx = this.agent.tracer.getContext() + if (!ctx?.transaction?.isActive()) { + this.logger.trace('Not recording callback segment %s for %s, transaction is not active', name, this.packageName) + return ctx + } + + const newCtx = this.createSegment({ name, ctx }) + return newCtx + }) + } +} + +module.exports = BaseCoreSubscriber diff --git a/lib/subscribers/core/dns/config.js b/lib/subscribers/core/dns/config.js new file mode 100644 index 0000000000..7b4d321fe6 --- /dev/null +++ b/lib/subscribers/core/dns/config.js @@ -0,0 +1,16 @@ +/* + * Copyright 2026 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +module.exports = { + dns: [ + { + path: './core/dns/index', instrumentations: [] + }, + { + path: './core/dns/promises', instrumentations: [] + } + ] +} diff --git a/lib/subscribers/core/dns/constants.js b/lib/subscribers/core/dns/constants.js new file mode 100644 index 0000000000..9c3cb2044c --- /dev/null +++ b/lib/subscribers/core/dns/constants.js @@ -0,0 +1,31 @@ +/* + * Copyright 2026 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const RESOLVE_METHODS = [ + 'resolve', + 'resolve4', + 'resolve6', + 'resolveCname', + 'resolveMx', + 'resolveNaptr', + 'resolveNs', + 'resolvePtr', + 'resolveSrv', + 'resolveTxt', + // missing + // resolveAny + // resolveCaa + // resolveNaptr + // resolveSoa + // resolveTlsa +] +const INSTRUMENTED_METHODS = [ + 'lookup', + 'reverse', + ...RESOLVE_METHODS +] + +module.exports = { INSTRUMENTED_METHODS, RESOLVE_METHODS } diff --git a/lib/subscribers/core/dns/index.js b/lib/subscribers/core/dns/index.js new file mode 100644 index 0000000000..cd7ccabb12 --- /dev/null +++ b/lib/subscribers/core/dns/index.js @@ -0,0 +1,16 @@ +/* + * Copyright 2026 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const { INSTRUMENTED_METHODS } = require('./constants') +const BaseCoreSubscriber = require('../base') + +class DnsSubscriber extends BaseCoreSubscriber { + constructor({ agent, logger }) { + super({ agent, logger, packageName: 'dns', instrumentedMethods: INSTRUMENTED_METHODS, hasCallback: true }) + } +} + +module.exports = DnsSubscriber diff --git a/lib/subscribers/core/dns/promises.js b/lib/subscribers/core/dns/promises.js new file mode 100644 index 0000000000..90ee904da2 --- /dev/null +++ b/lib/subscribers/core/dns/promises.js @@ -0,0 +1,16 @@ +/* + * Copyright 2026 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const { INSTRUMENTED_METHODS } = require('./constants') +const BaseCoreSubscriber = require('../base') + +class DnsPromisesSubscriber extends BaseCoreSubscriber { + constructor({ agent, logger }) { + super({ agent, logger, packageName: 'dns', prefix: 'promises', instrumentedMethods: INSTRUMENTED_METHODS }) + } +} + +module.exports = DnsPromisesSubscriber diff --git a/test/integration/core/dns-utils.js b/test/integration/core/dns-utils.js new file mode 100644 index 0000000000..33580d15fd --- /dev/null +++ b/test/integration/core/dns-utils.js @@ -0,0 +1,80 @@ +/* + * Copyright 2026 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +/** + * Mock most methods so we can control the results. + * Wrap calling the callback in a `setImmediate` so it will + * properly emit both `end` and `asyncEnd` within the tracing channel + * @param {object} params to function + * @param {object} params.dns dns package + * @param {object} params.sandbox sinon sandbox + */ +module.exports = function mockDns({ dns, sandbox }) { + sandbox.stub(dns, 'reverse').callsFake((_addr, cb) => { + setImmediate(() => { + cb(null, ['localhost']) + }) + }) + sandbox.stub(dns.promises, 'reverse').callsFake(async() => Promise.resolve(['localhost'])) + + sandbox.stub(dns, 'resolve').callsFake((_, cb) => { + setImmediate(() => { + cb(null, ['127.0.0.1']) + }) + }) + sandbox.stub(dns.Resolver.prototype, 'resolve').callsFake((_, cb) => { + setImmediate(() => { + cb(null, ['127.0.0.1']) + }) + }) + sandbox.stub(dns.promises, 'resolve').callsFake(async () => Promise.resolve(['127.0.0.1'])) + sandbox.stub(dns.promises.Resolver.prototype, 'resolve').callsFake(async () => Promise.resolve(['127.0.0.1'])) + sandbox.stub(dns, 'resolve4').callsFake((_, cb) => { + setImmediate(() => { + cb(null, ['127.0.0.1']) + }) + }) + sandbox.stub(dns.promises, 'resolve4').callsFake(async () => Promise.resolve(['127.0.0.1'])) + sandbox.stub(dns, 'resolve6').callsFake((_, cb) => { + setImmediate(() => { + cb(null, ['::1']) + }) + }) + sandbox.stub(dns.promises, 'resolve6').callsFake(async () => Promise.resolve(['::1'])) + const error = Error('boom') + error.code = 'ENODATA' + sandbox.stub(dns, 'resolveCname').callsFake((_, cb) => { + setImmediate(() => { + cb(error) + }) + }) + sandbox.stub(dns.promises, 'resolveCname').callsFake(async () => Promise.reject(error)) + sandbox.stub(dns, 'resolveMx').callsFake((_, cb) => { + setImmediate(() => { + cb(null, ['127.0.0.1']) + }) + }) + sandbox.stub(dns.promises, 'resolveMx').callsFake(async () => Promise.resolve(['127.0.0.1'])) + sandbox.stub(dns, 'resolveNs').callsFake((_, cb) => { + setImmediate(() => { + cb(null, ['a.iana-servers.net', 'b.iana-servers.net']) + }) + }) + sandbox.stub(dns.promises, 'resolveNs').callsFake(async () => Promise.resolve(['a.iana-servers.net', 'b.iana-servers.net'])) + sandbox.stub(dns, 'resolveTxt').callsFake((_, cb) => { + setImmediate(() => { + cb(null, ['one', 'two', 'three']) + }) + }) + sandbox.stub(dns.promises, 'resolveTxt').callsFake(async () => Promise.resolve(['one', 'two', 'three'])) + sandbox.stub(dns, 'resolveSrv').callsFake((_, cb) => { + setImmediate(() => { + cb(error) + }) + }) + sandbox.stub(dns.promises, 'resolveSrv').callsFake(async () => Promise.reject(error)) +} diff --git a/test/integration/core/dns.test.js b/test/integration/core/dns.test.js index 1319d902ca..8da010d6fb 100644 --- a/test/integration/core/dns.test.js +++ b/test/integration/core/dns.test.js @@ -10,63 +10,21 @@ const assert = require('node:assert') const dns = require('dns') const helper = require('../../lib/agent_helper') const verifySegments = require('./verify.js') - -const resolveMethods = [ - 'resolve', - 'resolve4', - 'resolve6', - 'resolveAny', - 'resolveCaa', - 'resolveCname', - 'resolveMx', - 'resolveNaptr', - 'resolveNs', - 'resolvePtr', - 'resolveSoa', - 'resolveSrv', - 'resolveTxt' -] +const sinon = require('sinon') +const mockDns = require('./dns-utils') test.beforeEach((ctx) => { + const sandbox = sinon.createSandbox() ctx.nr = {} - ctx.nr.reverse = dns.reverse - ctx.nr.origResolves = {} - - // wrap dns.reverse to not try to actually execute this function - dns.reverse = (addr, cb) => { - cb(undefined, ['localhost']) - } - - for (const fn of resolveMethods) { - ctx.nr.origResolves[fn] = dns[fn] - } - dns.resolve = (_, cb) => cb(null, ['127.0.0.1']) - dns.resolve4 = (_, cb) => cb(null, ['127.0.0.1']) - dns.resolve6 = (_, cb) => cb(null, ['::1']) - dns.resolveCname = (_, cb) => { - const error = Error('boom') - error.code = 'ENODATA' - cb(error) - } - dns.resolveMx = (_, cb) => cb(null, ['127.0.0.1']) - dns.resolveNs = (_, cb) => cb(null, ['a.iana-servers.net', 'b.iana-servers.net']) - dns.resolveTxt = (_, cb) => cb(null, ['one', 'two', 'three']) - dns.resolveSrv = (_, cb) => { - const error = Error('boom') - error.code = 'ENODATA' - cb(error) - } + ctx.nr.sandbox = sandbox + mockDns({ dns, sandbox }) ctx.nr.agent = helper.instrumentMockedAgent() }) test.afterEach((ctx) => { helper.unloadAgent(ctx.nr.agent) - dns.reverse = ctx.nr.reverse - - for (const fn of resolveMethods) { - dns[fn] = ctx.nr.origResolves[fn] - } + ctx.nr.sandbox.restore() }) test('lookup - IPv4', function (t, end) { @@ -81,6 +39,16 @@ test('lookup - IPv4', function (t, end) { }) }) +test('(promise)lookup - IPv4', async function (t) { + const { agent } = t.nr + await helper.runInTransaction(agent, async function () { + const { address, family } = await dns.promises.lookup('localhost', { verbatim: false }) + assert.equal(address, '127.0.0.1') + assert.equal(family, 4) + verifySegments({ agent, name: 'dns.lookup', assertCallbacks: false }) + }) +}) + test('lookup - IPv6', function (t, end) { const { agent } = t.nr helper.runInTransaction(agent, function () { @@ -100,26 +68,72 @@ test('resolve', function (t, end) { dns.resolve('example.com', function (err, ips) { assert.ok(!err, 'should not error') assert.equal(ips.length, 1) - assert.ok(ips[0].match(/^(?:\d{1,3}\.){3}\d{1,3}$/)) + assert.equal(ips[0], '127.0.0.1') + + verifySegments({ agent, end, name: 'dns.resolve' }) + }) + }) +}) + +test('Resolver.resolve', function (t, end) { + const { agent } = t.nr + const resolver = new dns.Resolver() + helper.runInTransaction(agent, function () { + resolver.resolve('example.com', function (err, ips) { + assert.ok(!err, 'should not error') + assert.equal(ips.length, 1) + assert.equal(ips[0], '127.0.0.1') - const children = [] - verifySegments({ agent, end, name: 'dns.resolve', children }) + verifySegments({ agent, end, name: 'dns.resolve' }) }) }) }) +test('(promise)resolve', async function (t) { + const { agent } = t.nr + await helper.runInTransaction(agent, async function () { + const ips = await dns.promises.resolve('example.com') + assert.equal(ips.length, 1) + assert.equal(ips[0], '127.0.0.1') + + verifySegments({ agent, name: 'dns.resolve', assertCallbacks: false }) + }) +}) + +test('(promise) Resolver.resolve', async function (t) { + const { agent } = t.nr + const resolver = new dns.promises.Resolver() + await helper.runInTransaction(agent, async function () { + const ips = await resolver.resolve('example.com') + assert.equal(ips.length, 1) + assert.equal(ips[0], '127.0.0.1') + + verifySegments({ agent, name: 'dns.resolve', assertCallbacks: false }) + }) +}) + test('resolve4', function (t, end) { const { agent } = t.nr helper.runInTransaction(agent, function () { dns.resolve4('example.com', function (err, ips) { assert.ok(!err, 'should not error') assert.equal(ips.length, 1) - assert.ok(ips[0].match(/^(?:\d{1,3}\.){3}\d{1,3}$/)) + assert.equal(ips[0], '127.0.0.1') verifySegments({ agent, end, name: 'dns.resolve4' }) }) }) }) +test('(promise)resolve4', async function (t) { + const { agent } = t.nr + await helper.runInTransaction(agent, async function () { + const ips = await dns.promises.resolve4('example.com') + assert.equal(ips.length, 1) + assert.equal(ips[0], '127.0.0.1') + verifySegments({ agent, name: 'dns.resolve4', assertCallbacks: false }) + }) +}) + test('resolve6', function (t, end) { const { agent } = t.nr helper.runInTransaction(agent, function () { @@ -132,6 +146,16 @@ test('resolve6', function (t, end) { }) }) +test('(promise)resolve6', async function (t) { + const { agent } = t.nr + await helper.runInTransaction(agent, async function () { + const ips = await dns.promises.resolve6('example.com') + assert.equal(ips.length, 1) + assert.equal(ips[0], '::1') + verifySegments({ agent, name: 'dns.resolve6', assertCallbacks: false }) + }) +}) + test('resolveCname', function (t, end) { const { agent } = t.nr helper.runInTransaction(agent, function () { @@ -142,18 +166,37 @@ test('resolveCname', function (t, end) { }) }) +test('(promise)resolveCname', async function (t) { + const { agent } = t.nr + await helper.runInTransaction(agent, async function () { + await assert.rejects(() => dns.promises.resolveCname('example.com')) + verifySegments({ agent, name: 'dns.resolveCname', assertCallbacks: false }) + }) +}) + test('resolveMx', function (t, end) { const { agent } = t.nr helper.runInTransaction(agent, function () { dns.resolveMx('example.com', function (err, ips) { assert.ok(!err, 'should not error') assert.equal(ips.length, 1) + assert.equal(ips[0], '127.0.0.1') verifySegments({ agent, end, name: 'dns.resolveMx' }) }) }) }) +test('(promise)resolveMx', async function (t) { + const { agent } = t.nr + await helper.runInTransaction(agent, async function () { + const ips = await dns.promises.resolveMx('example.com') + assert.equal(ips.length, 1) + assert.equal(ips[0], '127.0.0.1') + verifySegments({ agent, name: 'dns.resolveMx', assertCallbacks: false }) + }) +}) + test('resolveNs', function (t, end) { const { agent } = t.nr helper.runInTransaction(agent, function () { @@ -165,17 +208,36 @@ test('resolveNs', function (t, end) { }) }) +test('(promise)resolveNs', async function (t) { + const { agent } = t.nr + await helper.runInTransaction(agent, async function () { + const names = await dns.promises.resolveNs('example.com') + assert.deepEqual(names.sort(), ['a.iana-servers.net', 'b.iana-servers.net']) + verifySegments({ agent, name: 'dns.resolveNs', assertCallbacks: false }) + }) +}) + test('resolveTxt', function (t, end) { const { agent } = t.nr helper.runInTransaction(agent, function () { dns.resolveTxt('example.com', function (err, data) { assert.ok(!err, 'should not error') + assert.deepEqual(data, ['one', 'two', 'three']) assert.ok(Array.isArray(data)) verifySegments({ agent, end, name: 'dns.resolveTxt' }) }) }) }) +test('(promise)resolveTxt', async function (t) { + const { agent } = t.nr + await helper.runInTransaction(agent, async function () { + const data = await dns.promises.resolveTxt('example.com') + assert.deepEqual(data, ['one', 'two', 'three']) + verifySegments({ agent, name: 'dns.resolveTxt', assertCallbacks: false }) + }) +}) + test('resolveSrv', function (t, end) { const { agent } = t.nr helper.runInTransaction(agent, function () { @@ -186,13 +248,32 @@ test('resolveSrv', function (t, end) { }) }) +test('(promise)resolveSrv', async function (t) { + const { agent } = t.nr + await helper.runInTransaction(agent, async function () { + await assert.rejects(() => dns.promises.resolveSrv('example.com')) + verifySegments({ agent, name: 'dns.resolveSrv', assertCallbacks: false }) + }) +}) + test('reverse', function (t, end) { const { agent } = t.nr helper.runInTransaction(agent, function () { dns.reverse('127.0.0.1', function (err, names) { assert.ok(!err, 'should not error') - assert.ok(names.indexOf('localhost') !== -1, 'should have expected name') + assert.equal(names.length, 1) + assert.equal(names[0], 'localhost') verifySegments({ agent, end, name: 'dns.reverse' }) }) }) }) + +test('(promise)reverse', async function (t) { + const { agent } = t.nr + await helper.runInTransaction(agent, async function () { + const names = await dns.promises.reverse('127.0.0.1') + assert.equal(names.length, 1) + assert.equal(names[0], 'localhost') + verifySegments({ agent, name: 'dns.reverse', assertCallbacks: false }) + }) +}) diff --git a/test/integration/core/verify.js b/test/integration/core/verify.js index bfcb5c1856..619651bb2b 100644 --- a/test/integration/core/verify.js +++ b/test/integration/core/verify.js @@ -7,29 +7,36 @@ module.exports = verifySegments -function verifySegments({ agent, name, children = [], end, assert = require('node:assert') }) { +function verifySegments({ agent, name, children = [], end, assert = require('node:assert'), assertCallbacks = true }) { const { trace } = agent.getTransaction() const traceChildren = trace.getChildren(trace.root.id) assert.equal(traceChildren.length, 1, 'should have a single child') const child = traceChildren[0] - const childChildren = trace.getChildren(child.id) assert.equal(child.name, name, 'child segment should have correct name') assert.ok(child.timer.touched, 'child should started and ended') - assert.equal( - childChildren.length, - 1 + children.length, - 'child should have a single callback segment' - ) + if (assertCallbacks) { + verifyCallbacks({ trace, child, assert, end, children }) + } else { + end?.() + } +} + +function verifyCallbacks({ trace, child, assert, end, children }) { + const childChildren = trace.getChildren(child.id) for (let i = 0; i < children.length; ++i) { assert.equal(childChildren[i].name, children[i]) } - const callback = childChildren[childChildren.length - 1] assert.ok( callback.name === 'Callback: anonymous' || callback.name === 'Callback: ', 'callback segment should have correct name' ) + assert.equal( + childChildren.length, + 1 + children.length, + 'child should have a single callback segment' + ) assert.ok(callback.timer.start, 'callback should have started') assert.ok(!callback.timer.touched, 'callback should not have ended')