From da441e7f246afa67dee0d2f7a60d87008de8ba49 Mon Sep 17 00:00:00 2001 From: Bryan English Date: Wed, 8 Jul 2026 12:00:51 -0400 Subject: [PATCH 1/2] feat(pipeline): send Datadog-Client-Computed-Stats when enabled (#164) Add a `client_computed_stats` parameter to `WasmSpanState::new` and set `builder.set_client_computed_stats()` so the exporter sends the `Datadog-Client-Computed-Stats: true` header. The tracer needs this when it runs in APM-standalone (apmTracingEnabled=false) so the agent skips its own APM stats/sampling. The header is also sent whenever `stats_enabled` is set: computing stats client-side requires telling the agent to skip its own, otherwise the same traces are counted twice. Enabling stats therefore always implies the header, so the flags are OR'd in the binding rather than relying on the caller to keep them in sync. libdatadog's TraceExporterBuilder already supports the flag; the WASM binding just never called it. Adds pipeline tests asserting the header is present when client_computed_stats OR stats_enabled is set, and absent when both are off. --- crates/pipeline/src/lib.rs | 14 ++++++++++ test/pipeline.js | 56 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/crates/pipeline/src/lib.rs b/crates/pipeline/src/lib.rs index 28cd78b..54b03e8 100644 --- a/crates/pipeline/src/lib.rs +++ b/crates/pipeline/src/lib.rs @@ -236,6 +236,7 @@ impl WasmSpanState { env: &str, app_version: &str, runtime_id: &str, + client_computed_stats: bool, ) -> Result { let mut builder = TraceExporterBuilder::::new(); builder @@ -255,6 +256,19 @@ impl WasmSpanState { .set_runtime_id(runtime_id) .enable_agent_rates_payload_version(); + // Advertise `Datadog-Client-Computed-Stats` so the agent skips its own + // APM stats/sampling for these traces. This is required in two cases: + // - `stats_enabled`: we build a StatsCollector and send client-side + // stats, so the agent MUST NOT also compute them (double counting); + // - `client_computed_stats`: set independently for APM-standalone + // (apmTracingEnabled=false), where the agent should skip APM stats + // even though we don't compute them client-side. + // Enabling stats therefore always implies the header, so OR the flags + // rather than relying on the caller to keep them in sync. + if client_computed_stats || stats_enabled { + builder.set_client_computed_stats(); + } + let mut change_queue = vec![0u8; change_queue_size as usize]; let change_buffer = unsafe { ChangeBuffer::from_raw_parts( diff --git a/test/pipeline.js b/test/pipeline.js index da8fda5..cad84e8 100644 --- a/test/pipeline.js +++ b/test/pipeline.js @@ -167,6 +167,7 @@ class NativeSpansInterface { options.env || 'test-env', options.appVersion || '1.0.0', options.runtimeId || '00000000-0000-0000-0000-000000000000', + options.clientComputedStats ?? false, ) // Get pointers into WASM memory for direct buffer access @@ -1016,6 +1017,61 @@ describe('pipeline', { skip }, () => { }) }) + describe('client-computed-stats header', () => { + async function captureTraceHeader (nsOptions) { + const http = require('node:http') + let header + let sawTraces = false + const server = http.createServer((req, res) => { + req.on('data', () => {}) + req.on('end', () => { + if (req.url === '/v0.4/traces') { + sawTraces = true + header = req.headers['datadog-client-computed-stats'] + } + res.writeHead(200, { 'content-type': 'application/json' }) + res.end('{}') + }) + }) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const { port } = server.address() + const ns = new NativeSpansInterface({ agentUrl: `http://127.0.0.1:${port}`, ...nsOptions }) + const span = ns.createSpan() + span.name = 'span' + span.service = 'test-service' + span.resource = 'test-resource' + span.type = 'web' + span.duration = 1_000_000n + try { + await ns.flushSpans(span) + return { header, sawTraces } + } finally { + server.closeAllConnections?.() + server.close() + } + } + + it('sends Datadog-Client-Computed-Stats: true when enabled', async () => { + const { header, sawTraces } = await captureTraceHeader({ clientComputedStats: true }) + assert.ok(sawTraces, 'expected a POST to /v0.4/traces') + assert.strictEqual(header, 'true') + }) + + it('omits the header when both flags are disabled', async () => { + const { header, sawTraces } = await captureTraceHeader({ clientComputedStats: false, statsEnabled: false }) + assert.ok(sawTraces, 'expected a POST to /v0.4/traces') + assert.strictEqual(header, undefined) + }) + + it('sends the header when stats are enabled (client-side stats imply it)', async () => { + // Enabling client-side stats without clientComputedStats must still send + // the header, otherwise the agent double-counts APM stats. + const { header, sawTraces } = await captureTraceHeader({ statsEnabled: true, clientComputedStats: false }) + assert.ok(sawTraces, 'expected a POST to /v0.4/traces') + assert.strictEqual(header, 'true') + }) + }) + describe('client-side stats', () => { it('aggregates and flushes stats to /v0.6/stats', async () => { const http = require('node:http') From e1fbb216dee9a95af7ddf70687e9226a950a995c Mon Sep 17 00:00:00 2001 From: Bryan English Date: Wed, 8 Jul 2026 12:05:31 -0400 Subject: [PATCH 2/2] v0.13.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97f1059..020736a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/libdatadog", - "version": "0.12.2", + "version": "0.13.0", "description": "Node.js binding for libdatadog", "main": "index.js", "scripts": {