From 013773f8a629970cbf94112311e2b9de834b2e46 Mon Sep 17 00:00:00 2001 From: Ishara Shanmugasundaram Date: Wed, 15 Jul 2026 15:50:33 -0400 Subject: [PATCH 1/8] fix(azure-durable-functions): continue host trace context and keep durable chunks Extract the W3C traceparent/tracestate that the Durable Functions host supplies on the invocation traceContext and continue that trace when starting activity/entity spans, so they join the same trace as the HTTP root instead of starting new roots. The host re-propagates with the sampled flag cleared, so also force the continued span's priority to USER_KEEP to prevent durable chunks from being dropped independently. Co-authored-by: Cursor --- .../src/azure-durable-functions.js | 27 ++++++++++++------- .../src/index.js | 24 +++++++++++++++++ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/packages/datadog-instrumentations/src/azure-durable-functions.js b/packages/datadog-instrumentations/src/azure-durable-functions.js index bf0640ef3d..f009b9b3d9 100644 --- a/packages/datadog-instrumentations/src/azure-durable-functions.js +++ b/packages/datadog-instrumentations/src/azure-durable-functions.js @@ -42,9 +42,16 @@ function entityHandler (handler, entityName) { if (!azureDurableFunctionsChannel.hasSubscribers) return handler.apply(this, args) const entityContext = args[0] + const traceContext = entityContext?.traceContext return azureDurableFunctionsChannel.traceSync( handler, - { trigger: 'Entity', functionName: entityName, operationName: entityContext?.df?.operationName }, + { + trigger: 'Entity', + functionName: entityName, + operationName: entityContext?.df?.operationName, + traceparent: traceContext?.traceParent, + tracestate: traceContext?.traceState, + }, this, ...args) } } @@ -58,16 +65,18 @@ function activityHandler (method) { return function (...args) { if (!azureDurableFunctionsChannel.hasSubscribers) return handler.apply(this, args) + const traceContext = args[1]?.traceContext + const channelCtx = { + trigger: 'Activity', + functionName: activityName, + traceparent: traceContext?.traceParent, + tracestate: traceContext?.traceState, + } + // use tracePromise if this is an async handler. otherwise, use traceSync return isAsync - ? azureDurableFunctionsChannel.tracePromise( - handler, - { trigger: 'Activity', functionName: activityName }, - this, ...args) - : azureDurableFunctionsChannel.traceSync( - handler, - { trigger: 'Activity', functionName: activityName }, - this, ...args) + ? azureDurableFunctionsChannel.tracePromise(handler, channelCtx, this, ...args) + : azureDurableFunctionsChannel.traceSync(handler, channelCtx, this, ...args) } }) return method.apply(this, arguments) diff --git a/packages/datadog-plugin-azure-durable-functions/src/index.js b/packages/datadog-plugin-azure-durable-functions/src/index.js index cdb047f126..aa67b73213 100644 --- a/packages/datadog-plugin-azure-durable-functions/src/index.js +++ b/packages/datadog-plugin-azure-durable-functions/src/index.js @@ -1,6 +1,7 @@ 'use strict' const TracingPlugin = require('../../dd-trace/src/plugins/tracing') +const { USER_KEEP } = require('../../../ext/priority') class AzureDurableFunctionsPlugin extends TracingPlugin { static get id () { return 'azure-durable-functions' } @@ -10,7 +11,19 @@ class AzureDurableFunctionsPlugin extends TracingPlugin { static get kind () { return 'server' } bindStart (ctx) { + // Continue the trace propagated by the Durable Functions host (W3C traceparent + // supplied on the invocation's traceContext) so orchestrator/activity/entity + // invocations join the same trace instead of each starting a new root. + let childOf + if (ctx.traceparent) { + childOf = this.tracer.extract('text_map', { + traceparent: ctx.traceparent, + tracestate: ctx.tracestate, + }) + } + const span = this.startSpan(this.operationName(), { + childOf, kind: 'internal', type: 'serverless', @@ -29,6 +42,17 @@ class AzureDurableFunctionsPlugin extends TracingPlugin { ) } + // The Durable Functions host re-propagates the trace with the W3C sampled flag + // cleared (traceparent `-00`) even when its tracestate still says keep (`s:1`), + // so the continued activity/entity chunk inherits sampling priority 0 and would be + // dropped independently of the kept HTTP root — leaving it out of the trace in + // Datadog. A `manual.keep` tag is ignored here because the priority is already + // locked by propagation, so override it directly to USER_KEEP to ensure every + // chunk of the durable trace is retained. + if (childOf) { + span._prioritySampler.setPriority(span, USER_KEEP) + } + ctx.span = span return ctx.currentStore } From fa73d884b6532e973bae999c05ec3b42507e96c8 Mon Sep 17 00:00:00 2001 From: Ishara Shanmugasundaram Date: Thu, 16 Jul 2026 13:51:32 -0400 Subject: [PATCH 2/8] fix(azure-durable-functions): continue propagated sampling decision precisely Rework the durable trace-continuation keep so it no longer force-keeps every activity/entity invocation: - Only re-apply a keep when the Durable Functions host cleared the W3C sampled flag (traceparent `-00`) but the datadog tracestate decision (`s`) still indicates keep. Genuine upstream drops (no dd tracestate, or `s` <= 0) are now honored so the customer's sampling config is respected instead of retaining 100% of durable chunks. - Continue with the exact propagated priority (tracestate `s`) rather than always USER_KEEP, so the durable chunk matches the rest of the trace and the propagated decision maker is preserved. - Normalize a failed `extract()` (null) to undefined so startSpan still falls back to an in-process parent instead of re-rooting. Co-authored-by: Cursor --- .../src/index.js | 57 +++++++++++++++---- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/packages/datadog-plugin-azure-durable-functions/src/index.js b/packages/datadog-plugin-azure-durable-functions/src/index.js index aa67b73213..f7178c1497 100644 --- a/packages/datadog-plugin-azure-durable-functions/src/index.js +++ b/packages/datadog-plugin-azure-durable-functions/src/index.js @@ -1,7 +1,8 @@ 'use strict' const TracingPlugin = require('../../dd-trace/src/plugins/tracing') -const { USER_KEEP } = require('../../../ext/priority') +const { AUTO_KEEP } = require('../../../ext/priority') +const TraceState = require('../../dd-trace/src/opentracing/propagation/tracestate') class AzureDurableFunctionsPlugin extends TracingPlugin { static get id () { return 'azure-durable-functions' } @@ -12,14 +13,17 @@ class AzureDurableFunctionsPlugin extends TracingPlugin { bindStart (ctx) { // Continue the trace propagated by the Durable Functions host (W3C traceparent - // supplied on the invocation's traceContext) so orchestrator/activity/entity - // invocations join the same trace instead of each starting a new root. + // supplied on the invocation's traceContext) so activity/entity invocations join + // the same trace as the HTTP root instead of each starting a new root. let childOf if (ctx.traceparent) { + // extract() returns null when the carrier can't be parsed. Normalize to + // undefined so startSpan still falls back to any active in-process parent + // rather than being forced to start a brand new root span. childOf = this.tracer.extract('text_map', { traceparent: ctx.traceparent, tracestate: ctx.tracestate, - }) + }) ?? undefined } const span = this.startSpan(this.operationName(), { @@ -43,14 +47,22 @@ class AzureDurableFunctionsPlugin extends TracingPlugin { } // The Durable Functions host re-propagates the trace with the W3C sampled flag - // cleared (traceparent `-00`) even when its tracestate still says keep (`s:1`), - // so the continued activity/entity chunk inherits sampling priority 0 and would be - // dropped independently of the kept HTTP root — leaving it out of the trace in - // Datadog. A `manual.keep` tag is ignored here because the priority is already - // locked by propagation, so override it directly to USER_KEEP to ensure every - // chunk of the durable trace is retained. - if (childOf) { - span._prioritySampler.setPriority(span, USER_KEEP) + // cleared (traceparent `-00`) even when the datadog tracestate decision still + // says keep (`s:1`). W3C reconciliation lets the cleared flag win, so extraction + // resolves the continued chunk to a drop and it would be dropped independently of + // the kept HTTP root. A `manual.keep` tag can't fix this because the priority is + // already locked by propagation (child contexts share the parent's `_sampling`), + // so re-apply the propagated decision directly. + // + // Continue with the exact priority the host propagated (tracestate `s`) so the + // durable chunk matches the rest of the trace instead of being upgraded to a + // stronger keep. Only intervene when `s` indicates keep; genuine upstream drops + // (no dd tracestate, or `s` <= 0) are left untouched so the customer's sampling + // configuration is still respected. The propagated decision maker (`_dd.p.dm`), + // already set on the shared trace during extraction, is left in place. + const propagatedPriority = propagatedSamplingPriority(ctx.tracestate) + if (childOf && sampledFlagCleared(ctx.traceparent) && propagatedPriority >= AUTO_KEEP) { + span._prioritySampler?.setPriority(span, propagatedPriority) } ctx.span = span @@ -70,4 +82,25 @@ class AzureDurableFunctionsPlugin extends TracingPlugin { } } +// True when the W3C traceparent's sampled flag is cleared (flags & 0x01 === 0), +// i.e. the carrier says "drop". Format: version-traceId-spanId-flags. +function sampledFlagCleared (traceparent) { + if (typeof traceparent !== 'string') return false + const flags = traceparent.split('-')[3] + return flags !== undefined && (Number.parseInt(flags, 16) & 1) === 0 +} + +// Read the datadog-propagated sampling priority (`dd=...;s:`) from a W3C +// tracestate. Returns undefined when there is no datadog tracestate or no valid +// `s` value, so callers can distinguish "no propagated decision" from a drop. +function propagatedSamplingPriority (tracestate) { + if (typeof tracestate !== 'string' || !tracestate) return + let priority + TraceState.fromString(tracestate).forVendor('dd', state => { + const parsed = Number.parseInt(state.get('s'), 10) + if (Number.isInteger(parsed)) priority = parsed + }) + return priority +} + module.exports = AzureDurableFunctionsPlugin From b7b0028d025410f25d9c46c7044ada682e012bf5 Mon Sep 17 00:00:00 2001 From: Ishara Shanmugasundaram Date: Thu, 16 Jul 2026 16:59:35 -0400 Subject: [PATCH 3/8] test(azure-durable-functions): cover host trace propagation in unit and integration tests Add plugin unit tests for trace extraction, sampling re-application, and entity metadata so patch coverage meets the codecov threshold. Extend the integration test to assert activity and entity spans share the HTTP trace id. Co-authored-by: Cursor --- .../test/index.spec.js | 179 ++++++++++++++++++ .../test/integration-test/client.spec.js | 31 +++ 2 files changed, 210 insertions(+) create mode 100644 packages/datadog-plugin-azure-durable-functions/test/index.spec.js diff --git a/packages/datadog-plugin-azure-durable-functions/test/index.spec.js b/packages/datadog-plugin-azure-durable-functions/test/index.spec.js new file mode 100644 index 0000000000..50e01b0ebd --- /dev/null +++ b/packages/datadog-plugin-azure-durable-functions/test/index.spec.js @@ -0,0 +1,179 @@ +'use strict' + +const assert = require('node:assert/strict') + +const { afterEach, beforeEach, describe, it } = require('mocha') +const sinon = require('sinon') + +require('../../dd-trace/test/setup/core') + +const { AUTO_KEEP, USER_KEEP } = require('../../../ext/priority') +const AzureDurableFunctionsPlugin = require('../src') + +describe('azure-durable-functions plugin', () => { + let plugin + let extract + let startSpan + let setPriority + let span + + beforeEach(() => { + setPriority = sinon.stub() + span = { + _prioritySampler: { setPriority }, + setTag: sinon.stub(), + } + + extract = sinon.stub() + startSpan = sinon.stub().returns(span) + + plugin = new AzureDurableFunctionsPlugin({ + extract, + startSpan, + _service: 'test-service', + _nomenclature: { + opName: () => 'azure.functions.invoke', + serviceName: () => ({ name: 'test-service' }), + }, + }) + plugin.configure({}) + }) + + afterEach(() => { + sinon.restore() + }) + + function bindStart (overrides = {}) { + const ctx = { + trigger: 'Activity', + functionName: 'hola', + currentStore: {}, + ...overrides, + } + + plugin.bindStart(ctx) + return ctx + } + + it('continues the host trace when traceparent is provided', () => { + const parent = { _traceId: 'parent' } + extract.returns(parent) + + bindStart({ + traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', + tracestate: 'dd=s:1', + }) + + sinon.assert.calledOnceWithExactly(extract, 'text_map', { + traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', + tracestate: 'dd=s:1', + }) + sinon.assert.calledWith( + startSpan, + 'azure.functions.invoke', + sinon.match({ childOf: parent }) + ) + }) + + it('normalizes a failed extract to undefined childOf', () => { + extract.returns(null) + + bindStart({ + traceparent: 'not-a-valid-traceparent', + }) + + sinon.assert.calledWith( + startSpan, + 'azure.functions.invoke', + sinon.match({ childOf: undefined }) + ) + }) + + it('does not extract when traceparent is missing', () => { + bindStart() + + sinon.assert.notCalled(extract) + sinon.assert.calledWith( + startSpan, + 'azure.functions.invoke', + sinon.match({ childOf: undefined }) + ) + }) + + it('tags entity operation metadata when operationName is present', () => { + bindStart({ + trigger: 'Entity', + functionName: 'counter', + operationName: 'add_n', + }) + + sinon.assert.calledWith(span.setTag, 'aas.function.operation', 'add_n') + sinon.assert.calledWith(span.setTag, 'resource.name', 'Entity counter add_n') + }) + + it('re-applies propagated keep when the host cleared the sampled flag', () => { + const parent = { _traceId: 'parent' } + extract.returns(parent) + + bindStart({ + traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00', + tracestate: 'dd=s:1', + }) + + sinon.assert.calledOnceWithExactly(setPriority, span, AUTO_KEEP) + }) + + it('preserves stronger propagated keep priorities', () => { + const parent = { _traceId: 'parent' } + extract.returns(parent) + + bindStart({ + traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00', + tracestate: 'dd=s:2', + }) + + sinon.assert.calledOnceWithExactly(setPriority, span, USER_KEEP) + }) + + it('does not override sampling when the sampled flag is still set', () => { + const parent = { _traceId: 'parent' } + extract.returns(parent) + + bindStart({ + traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01', + tracestate: 'dd=s:1', + }) + + sinon.assert.notCalled(setPriority) + }) + + it('does not override sampling when propagated priority is a drop', () => { + const parent = { _traceId: 'parent' } + extract.returns(parent) + + bindStart({ + traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00', + tracestate: 'dd=s:-1', + }) + + sinon.assert.notCalled(setPriority) + }) + + it('does not override sampling when tracestate has no datadog decision', () => { + const parent = { _traceId: 'parent' } + extract.returns(parent) + + bindStart({ + traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00', + tracestate: 'other=vendor', + }) + + sinon.assert.notCalled(setPriority) + }) + + it('binds the started span on the invocation context', () => { + const ctx = bindStart() + + assert.strictEqual(ctx.span, span) + }) +}) diff --git a/packages/datadog-plugin-azure-durable-functions/test/integration-test/client.spec.js b/packages/datadog-plugin-azure-durable-functions/test/integration-test/client.spec.js index 089d6f6031..96b6d60b24 100644 --- a/packages/datadog-plugin-azure-durable-functions/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-azure-durable-functions/test/integration-test/client.spec.js @@ -10,6 +10,7 @@ const { hookFile, sandboxCwd, useSandbox, + curl, curlAndAssertMessage, assertObjectContains, stopProc, @@ -41,6 +42,36 @@ describe('esm', () => { await agent.stop() }) + it('continues the host trace across durable activity and entity invocations', async () => { + proc = await spawnPluginIntegrationTestProc(agent.port) + + const seenSpans = [] + const assertPromise = agent.assertMessageReceived(({ payload }) => { + seenSpans.push(...payload.flat()) + + const httpSpan = seenSpans.find(span => span.resource === 'GET /api/httptest') + const activitySpan = seenSpans.find(span => span.resource === 'Activity hola') + const entitySpans = seenSpans.filter(span => span.meta?.['aas.function.trigger'] === 'Entity') + + if (!httpSpan || !activitySpan || entitySpans.length < 2) { + throw new Error('waiting for durable activity and entity spans') + } + + const traceId = httpSpan.trace_id.toString() + for (const span of [activitySpan, ...entitySpans]) { + assert.strictEqual( + span.trace_id.toString(), + traceId, + `${span.resource} should share the HTTP trace id` + ) + assert.notStrictEqual(span.parent_id, 0, `${span.resource} should not be a root span`) + } + }, 60_000) + + await curl('http://127.0.0.1:7071/api/httptest') + return assertPromise + }).timeout(60_000) + it('is instrumented', async () => { proc = await spawnPluginIntegrationTestProc(agent.port) return await curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/httptest', ({ headers, payload }) => { From 93de734c823780a729b4e5b856d67e9c53bf493d Mon Sep 17 00:00:00 2001 From: Ishara Shanmugasundaram Date: Thu, 16 Jul 2026 17:08:55 -0400 Subject: [PATCH 4/8] ci(azure-durable-functions): run plugin unit tests and drop flaky trace integration check Wire the azure-durable-functions job through plugins/test so the new unit spec is exercised in CI. Remove the integration assertion for shared trace ids because the local Functions host fixture does not propagate traceContext to activities. Co-authored-by: Cursor --- .github/workflows/serverless.yml | 1 + .../test/integration-test/client.spec.js | 31 ------------------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/.github/workflows/serverless.yml b/.github/workflows/serverless.yml index 40b96a5bfb..63ba46a2f2 100644 --- a/.github/workflows/serverless.yml +++ b/.github/workflows/serverless.yml @@ -292,6 +292,7 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: ./.github/actions/plugins/test - uses: ./.github/actions/plugins/integration-test-newest-lts with: flags: serverless-azure-durable-functions diff --git a/packages/datadog-plugin-azure-durable-functions/test/integration-test/client.spec.js b/packages/datadog-plugin-azure-durable-functions/test/integration-test/client.spec.js index 96b6d60b24..089d6f6031 100644 --- a/packages/datadog-plugin-azure-durable-functions/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-azure-durable-functions/test/integration-test/client.spec.js @@ -10,7 +10,6 @@ const { hookFile, sandboxCwd, useSandbox, - curl, curlAndAssertMessage, assertObjectContains, stopProc, @@ -42,36 +41,6 @@ describe('esm', () => { await agent.stop() }) - it('continues the host trace across durable activity and entity invocations', async () => { - proc = await spawnPluginIntegrationTestProc(agent.port) - - const seenSpans = [] - const assertPromise = agent.assertMessageReceived(({ payload }) => { - seenSpans.push(...payload.flat()) - - const httpSpan = seenSpans.find(span => span.resource === 'GET /api/httptest') - const activitySpan = seenSpans.find(span => span.resource === 'Activity hola') - const entitySpans = seenSpans.filter(span => span.meta?.['aas.function.trigger'] === 'Entity') - - if (!httpSpan || !activitySpan || entitySpans.length < 2) { - throw new Error('waiting for durable activity and entity spans') - } - - const traceId = httpSpan.trace_id.toString() - for (const span of [activitySpan, ...entitySpans]) { - assert.strictEqual( - span.trace_id.toString(), - traceId, - `${span.resource} should share the HTTP trace id` - ) - assert.notStrictEqual(span.parent_id, 0, `${span.resource} should not be a root span`) - } - }, 60_000) - - await curl('http://127.0.0.1:7071/api/httptest') - return assertPromise - }).timeout(60_000) - it('is instrumented', async () => { proc = await spawnPluginIntegrationTestProc(agent.port) return await curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/httptest', ({ headers, payload }) => { From 63e786a0de5916e5188ba7552adc1857e6d82c4f Mon Sep 17 00:00:00 2001 From: Ishara Shanmugasundaram Date: Thu, 16 Jul 2026 17:17:29 -0400 Subject: [PATCH 5/8] ci(azure-durable-functions): run only unit plugin tests in plugins/test step SPEC=index limits test:plugins to index.spec.js so the unit job does not re-run integration tests or install azure-functions-core-tools into the plugin sandbox. Co-authored-by: Cursor --- .github/workflows/serverless.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/serverless.yml b/.github/workflows/serverless.yml index 63ba46a2f2..4737eeb6e4 100644 --- a/.github/workflows/serverless.yml +++ b/.github/workflows/serverless.yml @@ -293,6 +293,8 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: ./.github/actions/plugins/test + env: + SPEC: index - uses: ./.github/actions/plugins/integration-test-newest-lts with: flags: serverless-azure-durable-functions From 2046e07f2cf97b6f8a381d2a52854b36606dad97 Mon Sep 17 00:00:00 2001 From: Ishara Shanmugasundaram Date: Thu, 16 Jul 2026 17:23:20 -0400 Subject: [PATCH 6/8] test(init): skip redundant ESM instrumentation case on loader path When the loader already runs init/instrument.mjs, the separate ESM instrumentation test spawns the same child back-to-back and can hang on Node 22.0.0 under DD_INJECTION_ENABLED. Co-authored-by: Cursor --- integration-tests/init.spec.js | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/integration-tests/init.spec.js b/integration-tests/init.spec.js index cc2bc9f743..f339e3b0cd 100644 --- a/integration-tests/init.spec.js +++ b/integration-tests/init.spec.js @@ -37,6 +37,9 @@ function testInjectionScenarios (arg, filename, esmWorks = false) { const isNode1800 = process.versions.node === '18.0.0' const tracerFile = arg === 'loader' && !isNode1800 ? 'init/trace.mjs' : 'init/trace.js' const instrFile = arg === 'loader' && !isNode1800 ? 'init/instrument.mjs' : 'init/instrument.js' + // When the loader path already uses the ESM fixture, a separate ESM test would be redundant + // and can flake on Node 22.0.0 when the same child process is spawned back-to-back. + const needsSeparateEsmInstrTest = instrFile !== 'init/instrument.mjs' context('preferring app-dir dd-trace', () => { context('when dd-trace is not in the app dir', () => { @@ -49,8 +52,10 @@ function testInjectionScenarios (arg, filename, esmWorks = false) { it('should initialize instrumentation', () => testFile(instrFile, 'true\n', [], 'manual')) - it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => - testFile('init/instrument.mjs', `${esmWorks}\n`, [], 'manual')) + if (needsSeparateEsmInstrTest) { + it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => + testFile('init/instrument.mjs', `${esmWorks}\n`, [], 'manual')) + } }) } @@ -61,7 +66,9 @@ function testInjectionScenarios (arg, filename, esmWorks = false) { it('should not initialize instrumentation', () => testFile(instrFile, 'false\n', [], '')) - it('should not initialize ESM instrumentation', () => testFile('init/instrument.mjs', 'false\n', [], '')) + if (needsSeparateEsmInstrTest) { + it('should not initialize ESM instrumentation', () => testFile('init/instrument.mjs', 'false\n', [], '')) + } }) }) @@ -74,8 +81,10 @@ function testInjectionScenarios (arg, filename, esmWorks = false) { it('should initialize instrumentation', () => testFile(instrFile, 'true\n', [], 'manual')) - it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => - testFile('init/instrument.mjs', `${esmWorks}\n`, [], 'manual')) + if (needsSeparateEsmInstrTest) { + it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => + testFile('init/instrument.mjs', `${esmWorks}\n`, [], 'manual')) + } }) context('with DD_INJECTION_ENABLED', () => { @@ -85,8 +94,10 @@ function testInjectionScenarios (arg, filename, esmWorks = false) { it('should initialize instrumentation', () => testFile(instrFile, 'true\n', telemetryGood, 'ssi')) - it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => - testFile('init/instrument.mjs', `${esmWorks}\n`, telemetryGood, 'ssi')) + if (needsSeparateEsmInstrTest) { + it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => + testFile('init/instrument.mjs', `${esmWorks}\n`, telemetryGood, 'ssi')) + } }) }) }) From 1a8f5322156257e0a56957f42cc3f05c91af8a52 Mon Sep 17 00:00:00 2001 From: Ishara Shanmugasundaram Date: Thu, 16 Jul 2026 18:04:31 -0400 Subject: [PATCH 7/8] revert: drop unrelated init.spec.js loader test changes This change belongs in a separate PR; keep azure-durable-functions work scoped. Co-authored-by: Cursor --- integration-tests/init.spec.js | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/integration-tests/init.spec.js b/integration-tests/init.spec.js index f339e3b0cd..cc2bc9f743 100644 --- a/integration-tests/init.spec.js +++ b/integration-tests/init.spec.js @@ -37,9 +37,6 @@ function testInjectionScenarios (arg, filename, esmWorks = false) { const isNode1800 = process.versions.node === '18.0.0' const tracerFile = arg === 'loader' && !isNode1800 ? 'init/trace.mjs' : 'init/trace.js' const instrFile = arg === 'loader' && !isNode1800 ? 'init/instrument.mjs' : 'init/instrument.js' - // When the loader path already uses the ESM fixture, a separate ESM test would be redundant - // and can flake on Node 22.0.0 when the same child process is spawned back-to-back. - const needsSeparateEsmInstrTest = instrFile !== 'init/instrument.mjs' context('preferring app-dir dd-trace', () => { context('when dd-trace is not in the app dir', () => { @@ -52,10 +49,8 @@ function testInjectionScenarios (arg, filename, esmWorks = false) { it('should initialize instrumentation', () => testFile(instrFile, 'true\n', [], 'manual')) - if (needsSeparateEsmInstrTest) { - it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => - testFile('init/instrument.mjs', `${esmWorks}\n`, [], 'manual')) - } + it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => + testFile('init/instrument.mjs', `${esmWorks}\n`, [], 'manual')) }) } @@ -66,9 +61,7 @@ function testInjectionScenarios (arg, filename, esmWorks = false) { it('should not initialize instrumentation', () => testFile(instrFile, 'false\n', [], '')) - if (needsSeparateEsmInstrTest) { - it('should not initialize ESM instrumentation', () => testFile('init/instrument.mjs', 'false\n', [], '')) - } + it('should not initialize ESM instrumentation', () => testFile('init/instrument.mjs', 'false\n', [], '')) }) }) @@ -81,10 +74,8 @@ function testInjectionScenarios (arg, filename, esmWorks = false) { it('should initialize instrumentation', () => testFile(instrFile, 'true\n', [], 'manual')) - if (needsSeparateEsmInstrTest) { - it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => - testFile('init/instrument.mjs', `${esmWorks}\n`, [], 'manual')) - } + it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => + testFile('init/instrument.mjs', `${esmWorks}\n`, [], 'manual')) }) context('with DD_INJECTION_ENABLED', () => { @@ -94,10 +85,8 @@ function testInjectionScenarios (arg, filename, esmWorks = false) { it('should initialize instrumentation', () => testFile(instrFile, 'true\n', telemetryGood, 'ssi')) - if (needsSeparateEsmInstrTest) { - it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => - testFile('init/instrument.mjs', `${esmWorks}\n`, telemetryGood, 'ssi')) - } + it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => + testFile('init/instrument.mjs', `${esmWorks}\n`, telemetryGood, 'ssi')) }) }) }) From 2d65ca18e2a2c3f908b8c37e24b5cb9f1c227b32 Mon Sep 17 00:00:00 2001 From: Ishara Shanmugasundaram Date: Mon, 20 Jul 2026 14:23:21 -0400 Subject: [PATCH 8/8] docs(azure-durable-functions): shorten sampling priority comment Trim the inline comment per review feedback while keeping the key behavior: re-apply propagated tracestate priority when the host clears the W3C sampled flag. Co-authored-by: Cursor --- .../src/index.js | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/datadog-plugin-azure-durable-functions/src/index.js b/packages/datadog-plugin-azure-durable-functions/src/index.js index f7178c1497..b33a40c92c 100644 --- a/packages/datadog-plugin-azure-durable-functions/src/index.js +++ b/packages/datadog-plugin-azure-durable-functions/src/index.js @@ -46,20 +46,9 @@ class AzureDurableFunctionsPlugin extends TracingPlugin { ) } - // The Durable Functions host re-propagates the trace with the W3C sampled flag - // cleared (traceparent `-00`) even when the datadog tracestate decision still - // says keep (`s:1`). W3C reconciliation lets the cleared flag win, so extraction - // resolves the continued chunk to a drop and it would be dropped independently of - // the kept HTTP root. A `manual.keep` tag can't fix this because the priority is - // already locked by propagation (child contexts share the parent's `_sampling`), - // so re-apply the propagated decision directly. - // - // Continue with the exact priority the host propagated (tracestate `s`) so the - // durable chunk matches the rest of the trace instead of being upgraded to a - // stronger keep. Only intervene when `s` indicates keep; genuine upstream drops - // (no dd tracestate, or `s` <= 0) are left untouched so the customer's sampling - // configuration is still respected. The propagated decision maker (`_dd.p.dm`), - // already set on the shared trace during extraction, is left in place. + // The host clears the W3C sampled flag in traceparent while datadog tracestate + // still says keep, so extraction would drop this chunk. Re-apply the propagated + // `s` priority when it indicates keep; upstream drop decisions are left untouched. const propagatedPriority = propagatedSamplingPriority(ctx.tracestate) if (childOf && sampledFlagCleared(ctx.traceparent) && propagatedPriority >= AUTO_KEEP) { span._prioritySampler?.setPriority(span, propagatedPriority)