From 92802fae86a7a410c79652472c890bcfc0cbb483 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Fri, 4 Sep 2026 18:03:58 +0200 Subject: [PATCH] fix(remote-config): queue identity updates during polls An agentless poll holds the fetcher across an asynchronous request, so an identity refresh cannot borrow it until that poll completes. Queue the latest identity for the next poll to preserve backend, file, acknowledgment, and configuration state. Cargo must resolve the shared libdatadog types from one source, so the related dependencies use the same reviewed commit. Refs: https://github.com/DataDog/dd-trace-js/pull/10067#discussion_r3934876183 --- crates/remote_config/src/lib.rs | 20 ++++++++ packages/libdatadog/test/types.test.ts | 1 + packages/libdatadog/wasm/remote-config.d.ts | 1 + test/remote-config.js | 52 +++++++++++++++++++-- 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/crates/remote_config/src/lib.rs b/crates/remote_config/src/lib.rs index 6c69eef8..9a00f7ba 100644 --- a/crates/remote_config/src/lib.rs +++ b/crates/remote_config/src/lib.rs @@ -123,11 +123,18 @@ fn to_change_record(change: Change>>, Vec>) -> ChangeRec /// take it -- they would find it already borrowed and have nowhere to put the update. #[derive(Default)] struct PendingUpdates { + identity: Option, product_capabilities: Option<(Vec, Vec)>, extra_services: Option>, config_states: Vec<(RemoteConfigPath, ConfigApplyState)>, } +struct ClientIdentity { + client_id: String, + runtime_id: String, + tags: Vec, +} + /// Everything needed to build the fetcher, kept around because building it is deferred. struct FetcherConfig { target: Target, @@ -251,6 +258,9 @@ impl RemoteConfigFetcher { }; let updates = std::mem::take(&mut *pending.borrow_mut()); + if let Some(identity) = updates.identity { + fetcher.set_identity(identity.client_id, identity.runtime_id, identity.tags); + } if let Some((products, capabilities)) = updates.product_capabilities { fetcher.set_product_capabilities(products, capabilities); } @@ -302,6 +312,16 @@ impl RemoteConfigFetcher { self.pending.borrow_mut().extra_services = Some(services); } + /// Replaces the client identity reported on the next poll. + #[wasm_bindgen(js_name = "setIdentity")] + pub fn set_identity(&self, client_id: String, runtime_id: String, tags: Vec) { + self.pending.borrow_mut().identity = Some(ClientIdentity { + client_id, + runtime_id, + tags, + }); + } + /// Replaces the set of subscribed products and capabilities. /// /// Names this build does not know are skipped and returned, so that a tracer whose own list has diff --git a/packages/libdatadog/test/types.test.ts b/packages/libdatadog/test/types.test.ts index 9fecffa1..a1da79d5 100644 --- a/packages/libdatadog/test/types.test.ts +++ b/packages/libdatadog/test/types.test.ts @@ -67,6 +67,7 @@ const remoteConfigFetcher = new RemoteConfigFetcher({ }) remoteConfigFetcher.setExtraServices([]) +remoteConfigFetcher.setIdentity('client-id-2', 'runtime-id-2', []) remoteConfigFetcher.setProductCapabilities([], []) setStorage(runInStorage) diff --git a/packages/libdatadog/wasm/remote-config.d.ts b/packages/libdatadog/wasm/remote-config.d.ts index e182181c..791a6264 100644 --- a/packages/libdatadog/wasm/remote-config.d.ts +++ b/packages/libdatadog/wasm/remote-config.d.ts @@ -29,6 +29,7 @@ export class RemoteConfigFetcher { fetchChanges(): Promise setConfigState(path: string, applyState: number, applyError?: string): void setExtraServices(services: string[]): void + setIdentity(clientId: string, runtimeId: string, tags: string[]): void setProductCapabilities(products: string[], capabilities: string[]): string[] } diff --git a/test/remote-config.js b/test/remote-config.js index a5e94524..4b5e03b4 100644 --- a/test/remote-config.js +++ b/test/remote-config.js @@ -3,6 +3,7 @@ const assert = require('node:assert') const { createHash } = require('node:crypto') const { execFileSync } = require('node:child_process') +const { once } = require('node:events') const { createServer } = require('node:http') const { test } = require('node:test') @@ -75,10 +76,11 @@ async function withAgent (run) { const chunks = [] req .on('data', chunk => chunks.push(chunk)) - .on('end', () => { + .on('end', async () => { requests.push(JSON.parse(Buffer.concat(chunks).toString('utf8'))) + const response = await (responses.shift() ?? '{}') res.writeHead(200, { 'content-type': 'application/json' }) - res.end(responses.shift() ?? '{}') + res.end(Array.isArray(response) ? response[0].data : response) }) }) @@ -89,7 +91,7 @@ async function withAgent (run) { })) try { - await run({ fetcher, requests, responses }) + await run({ fetcher, requests, responses, server }) } finally { await new Promise(resolve => server.close(resolve)) } @@ -116,6 +118,50 @@ test('reports the client identity, products and capabilities', async () => { }) }) +test('updates identity without resetting remote config state', async () => { + await withAgent(async ({ fetcher, requests, responses, server }) => { + fetcher.setProductCapabilities(['ASM_FEATURES'], ['ASM_ACTIVATION']) + const responseEvents = new EventTarget() + responses.push(once(responseEvents, 'response')) + + const requestStarted = once(server, 'request') + const firstPoll = fetcher.fetchChanges() + await requestStarted + try { + fetcher.setIdentity( + 'intermediate-client-id', + 'intermediate-runtime-id', + ['runtime-id:intermediate-runtime-id'], + ) + fetcher.setIdentity( + 'client-id-2', + 'runtime-id-2', + ['runtime-id:runtime-id-2', '_dd.rc.client_id:client-id-2'], + ) + } finally { + responseEvents.dispatchEvent(new MessageEvent('response', { + data: agentResponse([{ path: CONFIG_PATH, file: { asm: { enabled: true } }, version: 1 }], 1), + })) + } + const [change] = await firstPoll + fetcher.setConfigState(change.path, APPLY_STATE_ACKNOWLEDGED, '') + + responses.push(agentResponse([{ path: CONFIG_PATH, file: { asm: { enabled: true } }, version: 1 }], 2)) + assert.deepStrictEqual(await fetcher.fetchChanges(), []) + + assert.strictEqual(requests[0].client.id, 'client-id-1') + const { client } = requests[1] + assert.strictEqual(client.id, 'client-id-2') + assert.strictEqual(client.client_tracer.runtime_id, 'runtime-id-2') + assert.deepStrictEqual(client.client_tracer.tags, [ + 'runtime-id:runtime-id-2', + '_dd.rc.client_id:client-id-2', + ]) + assert.strictEqual(Buffer.from(client.state.backend_client_state).toString(), 'backend-state-1') + assert.strictEqual(client.state.config_states[0].apply_state, APPLY_STATE_ACKNOWLEDGED) + }) +}) + test('diffs successive polls into add, update and remove changes', async () => { await withAgent(async ({ fetcher, responses }) => { fetcher.setProductCapabilities(['ASM_FEATURES'], [])