Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions crates/pipeline/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ impl WasmSpanState {
env: &str,
app_version: &str,
runtime_id: &str,
client_computed_stats: bool,
) -> Result<WasmSpanState, JsValue> {
let mut builder = TraceExporterBuilder::<LocalRuntime>::new();
builder
Expand All @@ -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(
Expand Down
56 changes: 56 additions & 0 deletions test/pipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down
Loading