From cb11075cbb5c8f2c2281d1b7f85d8aa35d03e9bf Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Wed, 15 Jul 2026 02:10:54 +0200 Subject: [PATCH 1/6] test: stop peer service checks racing operation spans The shared helper started the operation before registering its trace expectation and only inspected the first span of each payload. MongoDB connection traffic could therefore consume the payload containing the expected operation span, leaving the test to time out. --- packages/dd-trace/test/setup/mocha.js | 31 ++++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/dd-trace/test/setup/mocha.js b/packages/dd-trace/test/setup/mocha.js index 2b7af5263f..bd748704c1 100644 --- a/packages/dd-trace/test/setup/mocha.js +++ b/packages/dd-trace/test/setup/mocha.js @@ -182,6 +182,14 @@ function withNamingSchema ( }) } +/** + * @param {() => import('../../src/proxy')} tracer + * @param {string} pluginName + * @param {((callback: (error?: Error) => void) => unknown) | (() => Promise)} spanGenerationFn + * @param {string | (() => string)} service + * @param {string} serviceSource + * @param {{ desc?: string }} [opts] + */ function withPeerService (tracer, pluginName, spanGenerationFn, service, serviceSource, opts = {}) { describe('peer service computation' + (opts.desc ? ` ${opts.desc}` : ''), function () { this.timeout(10000) @@ -199,6 +207,13 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service }) it('should compute peer service', async () => { + const { expectSomeSpan } = require('../plugins/helpers') + const traceAssertion = expectSomeSpan(getAgent(), { + meta: { + 'peer.service': typeof service === 'function' ? service() : service, + '_dd.peer.service.source': serviceSource, + }, + }) const useCallback = spanGenerationFn.length === 1 const spanGenerationPromise = useCallback ? new Promise(/** @type {() => void} */ (resolve, reject) => { @@ -217,14 +232,14 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service util.inspect(spanGenerationPromise, { depth: 1 }), ) - await Promise.all([ - getAgent().assertSomeTraces(traces => { - const span = traces[0][0] - assert.strictEqual(span.meta['peer.service'], typeof service === 'function' ? service() : service) - assert.strictEqual(span.meta['_dd.peer.service.source'], serviceSource) - }), - spanGenerationPromise, - ]) + try { + await Promise.all([ + traceAssertion, + spanGenerationPromise, + ]) + } finally { + traceAssertion.cancel() + } }) }) } From 8161e81abd6a52a41674fa6806ee3a27b61540a5 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Thu, 16 Jul 2026 02:56:54 +0200 Subject: [PATCH 2/6] test: bind peer service checks to their operation spans Delayed setup traces can carry the same peer-service tags as the operation under test, allowing the shared matcher to pass without observing that operation. Run each generator under a unique parent and match its child span instead. Generator throws and invalid return values can bypass assertion cleanup. Keep the whole operation inside the cleanup boundary so the original failure is not replaced by a leaked-expectation teardown error. --- packages/dd-trace/test/setup/mocha.js | 59 ++++++++++++++++----------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/packages/dd-trace/test/setup/mocha.js b/packages/dd-trace/test/setup/mocha.js index bd748704c1..cd2f43d7a7 100644 --- a/packages/dd-trace/test/setup/mocha.js +++ b/packages/dd-trace/test/setup/mocha.js @@ -208,37 +208,48 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service it('should compute peer service', async () => { const { expectSomeSpan } = require('../plugins/helpers') - const traceAssertion = expectSomeSpan(getAgent(), { - meta: { - 'peer.service': typeof service === 'function' ? service() : service, - '_dd.peer.service.source': serviceSource, - }, - }) - const useCallback = spanGenerationFn.length === 1 - const spanGenerationPromise = useCallback - ? new Promise(/** @type {() => void} */ (resolve, reject) => { - const result = spanGenerationFn((err) => err ? reject(err) : resolve()) - // Some callback based methods are a mixture of callback and promise, - // depending on the module version. Await the promises as well. - if (util.types.isPromise(result)) { - result.then?.(resolve, reject) - } + const currentTracer = global._ddtrace + const parentSpan = currentTracer.startSpan('peer-service.test') + let traceAssertion + + try { + traceAssertion = expectSomeSpan(getAgent(), { + parent_id: BigInt(parentSpan.context().toSpanId()), + meta: { + 'peer.service': typeof service === 'function' ? service() : service, + '_dd.peer.service.source': serviceSource, + }, + }) + const useCallback = spanGenerationFn.length === 1 + const spanGenerationPromise = currentTracer.scope().activate(parentSpan, () => { + return useCallback + ? new Promise(/** @type {() => void} */ (resolve, reject) => { + const result = spanGenerationFn((error) => error ? reject(error) : resolve()) + // Some callback based methods are a mixture of callback and promise, + // depending on the module version. Await the promises as well. + if (util.types.isPromise(result)) { + result.then?.(resolve, reject) + } + }) + : spanGenerationFn() }) - : spanGenerationFn() - assert.strictEqual( - typeof spanGenerationPromise?.then, 'function', - 'spanGenerationFn should return a promise in case no callback is defined. Received: ' + - util.inspect(spanGenerationPromise, { depth: 1 }), - ) + assert.strictEqual( + typeof spanGenerationPromise?.then, 'function', + 'spanGenerationFn should return a promise in case no callback is defined. Received: ' + + util.inspect(spanGenerationPromise, { depth: 1 }), + ) - try { await Promise.all([ traceAssertion, - spanGenerationPromise, + (async () => { + await spanGenerationPromise + parentSpan.finish() + })(), ]) } finally { - traceAssertion.cancel() + parentSpan.finish() + traceAssertion?.cancel() } }) }) From ab7913bf1e0d202f0a11655e32280c64b388839f Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Mon, 20 Jul 2026 21:00:45 +0200 Subject: [PATCH 3/6] test: match peer service spans by trace Peer-service spans can be nested below integration spans, so requiring a direct parent excluded valid operations. Match the generated trace to keep setup traffic isolated without constraining span depth. --- packages/dd-trace/test/setup/mocha.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dd-trace/test/setup/mocha.js b/packages/dd-trace/test/setup/mocha.js index cd2f43d7a7..1977bb0cb7 100644 --- a/packages/dd-trace/test/setup/mocha.js +++ b/packages/dd-trace/test/setup/mocha.js @@ -214,7 +214,7 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service try { traceAssertion = expectSomeSpan(getAgent(), { - parent_id: BigInt(parentSpan.context().toSpanId()), + trace_id: BigInt(parentSpan.context().toTraceId()), meta: { 'peer.service': typeof service === 'function' ? service() : service, '_dd.peer.service.source': serviceSource, From bacd2c5ae01a8d0d8481606a256112005c2a4476 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Mon, 20 Jul 2026 22:34:00 +0200 Subject: [PATCH 4/6] test: stop correlation spans delaying trace export Keeping the correlation parent open until the operation settled delayed trace export. Integrations that publish duplicate finish notifications could then recompute `_dd.peer.service.source` before the trace was sent. --- packages/dd-trace/test/setup/mocha.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/dd-trace/test/setup/mocha.js b/packages/dd-trace/test/setup/mocha.js index 1977bb0cb7..8e888ed55a 100644 --- a/packages/dd-trace/test/setup/mocha.js +++ b/packages/dd-trace/test/setup/mocha.js @@ -233,6 +233,7 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service }) : spanGenerationFn() }) + parentSpan.finish() assert.strictEqual( typeof spanGenerationPromise?.then, 'function', @@ -242,10 +243,7 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service await Promise.all([ traceAssertion, - (async () => { - await spanGenerationPromise - parentSpan.finish() - })(), + spanGenerationPromise, ]) } finally { parentSpan.finish() From 571fe98f9fe5c6a82420939386824e4770c7d68f Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Mon, 20 Jul 2026 23:58:53 +0200 Subject: [PATCH 5/6] fix(tracing): make peer service finalization idempotent Repeated finish notifications can reach an outbound span before an ancestor lets the trace flush. The second pass treats the computed peer service as preconfigured and rewrites its source to `peer.service`; an existing source now marks finalization. The peer-service assertion keeps its correlation parent open through completion and matches the integration component, so delayed setup traces and spans from another integration cannot satisfy it. --- .../test/mongodb.spec.js | 5 ++- .../test/index.spec.js | 3 +- packages/dd-trace/src/plugins/outbound.js | 5 ++- .../dd-trace/test/plugins/outbound.spec.js | 38 ++++++++++++++++ packages/dd-trace/test/setup/mocha.js | 44 ++++++++++++++----- 5 files changed, 80 insertions(+), 15 deletions(-) diff --git a/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js b/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js index 306dcc06af..12e331b86c 100644 --- a/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js +++ b/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js @@ -107,7 +107,8 @@ describe('Plugin', () => { 'mongodb-core', (done) => collection.insertOne({ a: 1 }, {}, done), 'test', - 'peer.service' + 'peer.service', + { component: 'mongodb' } ) // The bulkWrite span is opened with `db.name` set, so it flows through the same @@ -118,7 +119,7 @@ describe('Plugin', () => { () => collection.bulkWrite([{ insertOne: { document: { a: 1 } } }]), 'test', 'peer.service', - { desc: 'with bulkWrite' } + { component: 'mongodb', desc: 'with bulkWrite' } ) it('should do automatic instrumentation', done => { diff --git a/packages/datadog-plugin-mongoose/test/index.spec.js b/packages/datadog-plugin-mongoose/test/index.spec.js index 2d63853655..0753039d68 100644 --- a/packages/datadog-plugin-mongoose/test/index.spec.js +++ b/packages/datadog-plugin-mongoose/test/index.spec.js @@ -73,7 +73,8 @@ describe('Plugin', () => { return new PeerCat({ name: 'PeerCat' }).save() }, () => dbName, - 'peer.service') + 'peer.service', + { component: 'mongodb' }) it('should propagate context with write operations', () => { const Cat = mongoose.model('Cat1', { name: String }) diff --git a/packages/dd-trace/src/plugins/outbound.js b/packages/dd-trace/src/plugins/outbound.js index 05ae4bcae3..635a07cd51 100644 --- a/packages/dd-trace/src/plugins/outbound.js +++ b/packages/dd-trace/src/plugins/outbound.js @@ -125,7 +125,10 @@ class OutboundPlugin extends TracingPlugin { */ tagPeerService (span) { if (this._tracerConfig.spanComputePeerService) { - const peerData = this.getPeerService(span.context().getTags()) + const tags = span.context().getTags() + if (tags[PEER_SERVICE_SOURCE_KEY] !== undefined) return + + const peerData = this.getPeerService(tags) if (peerData !== undefined) { span.addTags(this.getPeerServiceRemap(peerData)) } diff --git a/packages/dd-trace/test/plugins/outbound.spec.js b/packages/dd-trace/test/plugins/outbound.spec.js index 2edd27f354..1ef24c107b 100644 --- a/packages/dd-trace/test/plugins/outbound.spec.js +++ b/packages/dd-trace/test/plugins/outbound.spec.js @@ -50,6 +50,44 @@ describe('OuboundPlugin', () => { sinon.assert.notCalled(getRemapStub) }) + it('should not recompute peer service after its source was set', () => { + computePeerServiceStub.value({ spanComputePeerService: true }) + const tags = { + 'peer.service': 'mypeerservice', + '_dd.peer.service.source': 'out.host', + } + instance.tagPeerService({ context: () => ({ getTags: () => tags }) }) + + sinon.assert.notCalled(getPeerServiceStub) + sinon.assert.notCalled(getRemapStub) + }) + + it('should not recompute peer service when only its source was set', () => { + computePeerServiceStub.value({ spanComputePeerService: true }) + const tags = { + '_dd.peer.service.source': 'out.host', + } + instance.tagPeerService({ context: () => ({ getTags: () => tags }) }) + + sinon.assert.notCalled(getPeerServiceStub) + sinon.assert.notCalled(getRemapStub) + }) + + it('should compute peer service when only its value was set', () => { + computePeerServiceStub.value({ spanComputePeerService: true }) + getPeerServiceStub.returns({ + 'peer.service': 'mypeerservice', + '_dd.peer.service.source': 'peer.service', + }) + const tags = { + 'peer.service': 'mypeerservice', + } + instance.tagPeerService({ context: () => ({ getTags: () => tags }), addTags: () => {} }) + + sinon.assert.called(getPeerServiceStub) + sinon.assert.called(getRemapStub) + }) + it('should do nothing when disabled', () => { computePeerServiceStub.value({ spanComputePeerService: false }) instance.tagPeerService({ context: () => ({ _tags: {}, getTags () { return this._tags } }), addTags: () => {} }) diff --git a/packages/dd-trace/test/setup/mocha.js b/packages/dd-trace/test/setup/mocha.js index 8e888ed55a..0c0070c47a 100644 --- a/packages/dd-trace/test/setup/mocha.js +++ b/packages/dd-trace/test/setup/mocha.js @@ -188,7 +188,7 @@ function withNamingSchema ( * @param {((callback: (error?: Error) => void) => unknown) | (() => Promise)} spanGenerationFn * @param {string | (() => string)} service * @param {string} serviceSource - * @param {{ desc?: string }} [opts] + * @param {{ component?: string, desc?: string }} [opts] */ function withPeerService (tracer, pluginName, spanGenerationFn, service, serviceSource, opts = {}) { describe('peer service computation' + (opts.desc ? ` ${opts.desc}` : ''), function () { @@ -207,19 +207,39 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service }) it('should compute peer service', async () => { - const { expectSomeSpan } = require('../plugins/helpers') const currentTracer = global._ddtrace const parentSpan = currentTracer.startSpan('peer-service.test') + const traceId = BigInt(parentSpan.context().toTraceId()) + const component = opts.component ?? pluginName let traceAssertion + /** + * @param {Array }>>} traces + */ + function assertPeerServiceSpan (traces) { + const expectedService = typeof service === 'function' ? service() : service + + for (const trace of traces) { + for (const span of trace) { + if ( + span.trace_id === traceId && + span.meta.component === component && + span.meta['peer.service'] === expectedService && + span.meta['_dd.peer.service.source'] === serviceSource + ) { + return + } + } + } + + assert.fail( + `No ${component} span in trace ${traceId} had peer.service=${expectedService} and ` + + `_dd.peer.service.source=${serviceSource}.\n\nCandidate Traces:\n${util.inspect(traces, { depth: null })}` + ) + } + try { - traceAssertion = expectSomeSpan(getAgent(), { - trace_id: BigInt(parentSpan.context().toTraceId()), - meta: { - 'peer.service': typeof service === 'function' ? service() : service, - '_dd.peer.service.source': serviceSource, - }, - }) + traceAssertion = getAgent().assertSomeTraces(assertPeerServiceSpan, { timeoutMs: 9000 }) const useCallback = spanGenerationFn.length === 1 const spanGenerationPromise = currentTracer.scope().activate(parentSpan, () => { return useCallback @@ -233,7 +253,6 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service }) : spanGenerationFn() }) - parentSpan.finish() assert.strictEqual( typeof spanGenerationPromise?.then, 'function', @@ -243,7 +262,10 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service await Promise.all([ traceAssertion, - spanGenerationPromise, + (async () => { + await spanGenerationPromise + parentSpan.finish() + })(), ]) } finally { parentSpan.finish() From ea19d7d23c21e0286dfa2eab38817bc15bb41568 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Tue, 21 Jul 2026 12:22:57 +0200 Subject: [PATCH 6/6] test(mongodb): target bulkWrite peer service parent --- packages/datadog-plugin-mongodb-core/test/mongodb.spec.js | 6 +++++- packages/dd-trace/test/setup/mocha.js | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js b/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js index 12e331b86c..ec886934ee 100644 --- a/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js +++ b/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js @@ -119,7 +119,11 @@ describe('Plugin', () => { () => collection.bulkWrite([{ insertOne: { document: { a: 1 } } }]), 'test', 'peer.service', - { component: 'mongodb', desc: 'with bulkWrite' } + { + component: 'mongodb', + desc: 'with bulkWrite', + resource: () => `bulkWrite test.${collectionName}`, + } ) it('should do automatic instrumentation', done => { diff --git a/packages/dd-trace/test/setup/mocha.js b/packages/dd-trace/test/setup/mocha.js index 0c0070c47a..f7888d5f95 100644 --- a/packages/dd-trace/test/setup/mocha.js +++ b/packages/dd-trace/test/setup/mocha.js @@ -188,7 +188,7 @@ function withNamingSchema ( * @param {((callback: (error?: Error) => void) => unknown) | (() => Promise)} spanGenerationFn * @param {string | (() => string)} service * @param {string} serviceSource - * @param {{ component?: string, desc?: string }} [opts] + * @param {{ component?: string, desc?: string, resource?: string | (() => string) }} [opts] */ function withPeerService (tracer, pluginName, spanGenerationFn, service, serviceSource, opts = {}) { describe('peer service computation' + (opts.desc ? ` ${opts.desc}` : ''), function () { @@ -214,16 +214,18 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service let traceAssertion /** - * @param {Array }>>} traces + * @param {Array }>>} traces */ function assertPeerServiceSpan (traces) { const expectedService = typeof service === 'function' ? service() : service + const expectedResource = typeof opts.resource === 'function' ? opts.resource() : opts.resource for (const trace of traces) { for (const span of trace) { if ( span.trace_id === traceId && span.meta.component === component && + (expectedResource === undefined || span.resource === expectedResource) && span.meta['peer.service'] === expectedService && span.meta['_dd.peer.service.source'] === serviceSource ) {