From 51cae1bdd2378ecf0f3c77147e2934e33ca2ceb4 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 21 Jul 2026 22:38:28 -0600 Subject: [PATCH 1/3] fix(openfeature): support custom agentless endpoints --- .../dd-trace/src/exporters/common/request.js | 6 ++- .../agentless_configuration_source.js | 7 ++- .../src/openfeature/configuration_source.js | 12 +++--- .../test/exporters/common/request.spec.js | 17 ++++++++ .../agentless_configuration_source.spec.js | 23 +++++++++- .../openfeature/configuration_source.spec.js | 43 ++++++++++++------- 6 files changed, 79 insertions(+), 29 deletions(-) diff --git a/packages/dd-trace/src/exporters/common/request.js b/packages/dd-trace/src/exporters/common/request.js index dc050a4522..251cda8938 100644 --- a/packages/dd-trace/src/exporters/common/request.js +++ b/packages/dd-trace/src/exporters/common/request.js @@ -50,11 +50,13 @@ function request (data, options, callback) { } // Never put the Datadog API key on a cleartext connection to a non-loopback host; that would - // expose it on the wire. Loopback (local agent, dev proxy, tests) is exempt. Strip the key + // expose it on the wire. Loopback (local agent, dev proxy, tests) and explicit operator-owned + // endpoints are exempt. Strip the key // rather than drop the request: the agent proxies telemetry with its own key, while an https // intake URL is required to authenticate agentless traffic. const hasApiKey = options.headers['dd-api-key'] !== undefined || options.headers['DD-API-KEY'] !== undefined - if (hasApiKey && options.protocol === 'http:' && !isLoopbackHost(options.hostname)) { + const insecureApiKey = options.protocol === 'http:' && !isLoopbackHost(options.hostname) + if (hasApiKey && insecureApiKey && !options.allowInsecureApiKey) { log.error( 'Not sending the Datadog API key over a non-TLS connection to %s. Configure an https intake URL.', options.hostname diff --git a/packages/dd-trace/src/openfeature/agentless_configuration_source.js b/packages/dd-trace/src/openfeature/agentless_configuration_source.js index fc50bf1711..f11c581373 100644 --- a/packages/dd-trace/src/openfeature/agentless_configuration_source.js +++ b/packages/dd-trace/src/openfeature/agentless_configuration_source.js @@ -18,9 +18,10 @@ const RETRY_JITTER = 0.2 /** * @typedef {object} AgentlessSourceConfig * @property {URL} endpoint + * @property {boolean} allowInsecureApiKey * @property {number} pollIntervalMs * @property {number} requestTimeoutMs - * @property {string} apiKey + * @property {string | undefined} apiKey */ /** @@ -138,7 +139,7 @@ class AgentlessConfigurationSource { #request (signal) { const headers = getClientLibraryHeaders() headers['Accept-Encoding'] = 'gzip' - headers['DD-API-KEY'] = this.#config.apiKey + if (this.#config.apiKey) headers['DD-API-KEY'] = this.#config.apiKey if (this.#etag) headers['If-None-Match'] = this.#etag /** @@ -164,6 +165,8 @@ class AgentlessConfigurationSource { url: this.#config.endpoint, method: 'GET', headers, + // An explicit operator override may be a cleartext development or dogfooding proxy. + allowInsecureApiKey: this.#config.allowInsecureApiKey, retry: false, signal, timeout: this.#config.requestTimeoutMs, diff --git a/packages/dd-trace/src/openfeature/configuration_source.js b/packages/dd-trace/src/openfeature/configuration_source.js index 0bd7327953..e2e2ee6e1a 100644 --- a/packages/dd-trace/src/openfeature/configuration_source.js +++ b/packages/dd-trace/src/openfeature/configuration_source.js @@ -1,6 +1,5 @@ 'use strict' -const { isLoopbackHost } = require('../exporters/common/url') const log = require('../log') const DEFAULT_AGENTLESS_PATH = '/api/v2/feature-flagging/config/rules-based/server' @@ -27,14 +26,17 @@ function create (config, applyConfiguration) { return } + const hasCustomEndpoint = Boolean(baseUrl?.trim()) + try { - if (!config.DD_API_KEY) { - throw new Error('DD_API_KEY is required for Feature Flagging agentless delivery') + if (!hasCustomEndpoint && !config.DD_API_KEY) { + throw new Error('DD_API_KEY is required for the Datadog Feature Flagging API') } const AgentlessConfigurationSource = require('./agentless_configuration_source') return new AgentlessConfigurationSource({ endpoint: endpoint(config, baseUrl), + allowInsecureApiKey: hasCustomEndpoint, pollIntervalMs: Math.min(pollIntervalSeconds, MAX_POLL_INTERVAL_SECONDS) * 1000, requestTimeoutMs: requestTimeoutSeconds * 1000, apiKey: config.DD_API_KEY, @@ -74,10 +76,6 @@ function endpoint (config, configuredBaseUrl) { if (url.protocol !== 'https:' && url.protocol !== 'http:') { throw new Error('Feature Flagging agentless URL must use HTTP or HTTPS') } - if (url.protocol === 'http:' && !isLoopbackHost(url.hostname)) { - throw new Error('Feature Flagging agentless URL must use HTTPS unless it targets loopback') - } - if (url.pathname === '' || url.pathname === '/') { url.pathname = DEFAULT_AGENTLESS_PATH } diff --git a/packages/dd-trace/test/exporters/common/request.spec.js b/packages/dd-trace/test/exporters/common/request.spec.js index eceaf687c6..f4b8d058ff 100644 --- a/packages/dd-trace/test/exporters/common/request.spec.js +++ b/packages/dd-trace/test/exporters/common/request.spec.js @@ -847,6 +847,23 @@ describe('request', function () { }) }) + it('keeps dd-api-key over http for an explicitly trusted custom endpoint', (done) => { + nock('http://flags.dev.internal', { reqheaders: { 'dd-api-key': 'secret-key' } }) + .post('/v1/input') + .reply(200, 'OK') + + request(Buffer.from(''), { + method: 'POST', + url: new URL('http://flags.dev.internal/v1/input'), + headers: { 'dd-api-key': 'secret-key' }, + allowInsecureApiKey: true, + }, (err, res) => { + assert.strictEqual(res, 'OK') + sinon.assert.notCalled(log.error) + done(err) + }) + }) + for (const loopbackHost of ['127.0.0.1', '127.1.2.3', 'localhost', '[::1]']) { it(`keeps dd-api-key over http to the loopback host ${loopbackHost}`, (done) => { nock(`http://${loopbackHost}:9999`, { diff --git a/packages/dd-trace/test/openfeature/agentless_configuration_source.spec.js b/packages/dd-trace/test/openfeature/agentless_configuration_source.spec.js index d8816abb8f..e5f1e6fb18 100644 --- a/packages/dd-trace/test/openfeature/agentless_configuration_source.spec.js +++ b/packages/dd-trace/test/openfeature/agentless_configuration_source.spec.js @@ -48,6 +48,7 @@ describe('AgentlessConfigurationSource', () => { applyConfiguration = sinon.stub() config = { endpoint: new URL('http://127.0.0.1:8080/api/v2/feature-flagging/config/rules-based/server'), + allowInsecureApiKey: true, pollIntervalMs: 30_000, requestTimeoutMs: 2000, apiKey: 'test-api-key', @@ -146,6 +147,7 @@ describe('AgentlessConfigurationSource', () => { assert.strictEqual(requests[0].data, '') assert.strictEqual(requests[0].options.url, config.endpoint) assert.strictEqual(requests[0].options.method, 'GET') + assert.strictEqual(requests[0].options.allowInsecureApiKey, config.allowInsecureApiKey) assert.strictEqual(requests[0].options.retry, false) assert.strictEqual(requests[0].options.timeout, 2000) assert.strictEqual(requests[0].options.headers['DD-API-KEY'], 'test-api-key') @@ -181,13 +183,14 @@ describe('AgentlessConfigurationSource', () => { clock.restore() clock = undefined const body = zlib.gzipSync(responseBody()) - nock('http://127.0.0.1:8080', { + config.endpoint = new URL('http://flags.dev.internal:8080/custom/ufc') + nock('http://flags.dev.internal:8080', { reqheaders: { 'accept-encoding': 'gzip', 'dd-api-key': 'test-api-key', }, }) - .get('/api/v2/feature-flagging/config/rules-based/server') + .get('/custom/ufc') .reply(200, body, { 'content-encoding': 'gzip', etag: '"real-path"', @@ -491,6 +494,22 @@ describe('AgentlessConfigurationSource', () => { sinon.assert.notCalled(applyConfiguration) }) + it('omits a missing API key and reports the endpoint authentication failure', async () => { + delete config.apiKey + responses.push({ statusCode: 401 }) + + source().start() + await flush() + + assert.strictEqual(Object.hasOwn(requests[0].options.headers, 'DD-API-KEY'), false) + sinon.assert.calledOnceWithExactly( + log.warn, + 'Feature Flagging agentless endpoint returned HTTP %d; verify DD_API_KEY is configured and valid', + 401 + ) + sinon.assert.notCalled(applyConfiguration) + }) + it('uses fixed-delay polling after a request completes', async () => { responses.push( { pending: true }, diff --git a/packages/dd-trace/test/openfeature/configuration_source.spec.js b/packages/dd-trace/test/openfeature/configuration_source.spec.js index 8ac81dba43..a1260ca118 100644 --- a/packages/dd-trace/test/openfeature/configuration_source.spec.js +++ b/packages/dd-trace/test/openfeature/configuration_source.spec.js @@ -52,6 +52,7 @@ describe('OpenFeature configuration source', () => { 'https://ufc-server.ff-cdn.datadoghq.com/api/v2/feature-flagging/config/rules-based/server?dd_env=my+env' ) assert.strictEqual(resolved.apiKey, 'test-api-key') + assert.strictEqual(resolved.allowInsecureApiKey, false) assert.strictEqual(resolved.pollIntervalMs, 30_000) assert.strictEqual(resolved.requestTimeoutMs, 5000) }) @@ -93,6 +94,18 @@ describe('OpenFeature configuration source', () => { }) } + it('allows a cleartext custom endpoint for local development and proxies', () => { + config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = + 'http://flags.dev.internal:8080' + + const resolved = createSourceConfig() + assert.strictEqual( + resolved.endpoint.toString(), + 'http://flags.dev.internal:8080/api/v2/feature-flagging/config/rules-based/server' + ) + assert.strictEqual(resolved.allowInsecureApiKey, true) + }) + it('preserves an exact configured path and query', () => { config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = 'https://example.com/custom/ufc?tenant=one' @@ -156,20 +169,6 @@ describe('OpenFeature configuration source', () => { sinon.assert.notCalled(AgentlessConfigurationSource) }) - it('rejects cleartext non-loopback endpoints', () => { - config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = 'http://flags.example.test' - - const source = configurationSource.create(config, sinon.spy()) - - assert.strictEqual(source, undefined) - sinon.assert.calledOnceWithMatch( - log.error, - 'Unable to configure Feature Flagging configuration source', - sinon.match.instanceOf(Error) - ) - sinon.assert.notCalled(AgentlessConfigurationSource) - }) - it('rejects malformed endpoints without logging their sensitive value', () => { const sentinel = 'sensitive-value' config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = `https://${sentinel} value` @@ -187,7 +186,19 @@ describe('OpenFeature configuration source', () => { assert.strictEqual(log.error.firstCall.args[1].cause, undefined) }) - it('requires an API key without enabling a source', () => { + it('creates a source for a custom API without a Datadog API key', () => { + delete config.DD_API_KEY + config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = + 'https://flags.example.test/custom/ufc' + + const source = configurationSource.create(config, sinon.spy()) + + assert.ok(source instanceof AgentlessConfigurationSource) + assert.strictEqual(AgentlessConfigurationSource.firstCall.args[0].apiKey, undefined) + sinon.assert.notCalled(log.error) + }) + + it('requires a Datadog API key for the default Datadog API', () => { delete config.DD_API_KEY const source = configurationSource.create(config, sinon.spy()) @@ -195,7 +206,7 @@ describe('OpenFeature configuration source', () => { sinon.assert.calledOnceWithMatch( log.error, 'Unable to configure Feature Flagging configuration source', - sinon.match.instanceOf(Error) + sinon.match.has('message', 'DD_API_KEY is required for the Datadog Feature Flagging API') ) assert.strictEqual(source, undefined) sinon.assert.notCalled(AgentlessConfigurationSource) From 783aee47e4b192c7a20c595f7ef9ccc08779a68d Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 22 Jul 2026 18:43:24 -0600 Subject: [PATCH 2/3] fix(openfeature): never send API keys to custom endpoints --- .../openfeature-configuration-sources.spec.js | 2 +- .../dd-trace/src/exporters/common/request.js | 6 ++---- .../agentless_configuration_source.js | 8 +------- .../src/openfeature/configuration_source.js | 3 +-- .../test/exporters/common/request.spec.js | 17 ----------------- .../agentless_configuration_source.spec.js | 11 +++++------ .../openfeature/configuration_source.spec.js | 11 +++++++---- 7 files changed, 17 insertions(+), 41 deletions(-) diff --git a/integration-tests/openfeature/openfeature-configuration-sources.spec.js b/integration-tests/openfeature/openfeature-configuration-sources.spec.js index 566c36639b..9923933241 100644 --- a/integration-tests/openfeature/openfeature-configuration-sources.spec.js +++ b/integration-tests/openfeature/openfeature-configuration-sources.spec.js @@ -341,7 +341,7 @@ function assertDeliveryTraffic (testCase) { for (const request of cdnRequests) { assert.strictEqual(request.url, `${AGENTLESS_PATH}?case=${testCase.identifier}`, testCase.label) assert.strictEqual(request.headers['accept-encoding'], 'gzip', testCase.label) - assert.strictEqual(request.headers['dd-api-key'], 'integration-api-key', testCase.label) + assert.strictEqual(request.headers['dd-api-key'], undefined, testCase.label) assert.strictEqual(request.headers['dd-client-library-language'], 'nodejs', testCase.label) assert.strictEqual(request.headers['dd-client-library-version'], VERSION, testCase.label) } diff --git a/packages/dd-trace/src/exporters/common/request.js b/packages/dd-trace/src/exporters/common/request.js index 251cda8938..dc050a4522 100644 --- a/packages/dd-trace/src/exporters/common/request.js +++ b/packages/dd-trace/src/exporters/common/request.js @@ -50,13 +50,11 @@ function request (data, options, callback) { } // Never put the Datadog API key on a cleartext connection to a non-loopback host; that would - // expose it on the wire. Loopback (local agent, dev proxy, tests) and explicit operator-owned - // endpoints are exempt. Strip the key + // expose it on the wire. Loopback (local agent, dev proxy, tests) is exempt. Strip the key // rather than drop the request: the agent proxies telemetry with its own key, while an https // intake URL is required to authenticate agentless traffic. const hasApiKey = options.headers['dd-api-key'] !== undefined || options.headers['DD-API-KEY'] !== undefined - const insecureApiKey = options.protocol === 'http:' && !isLoopbackHost(options.hostname) - if (hasApiKey && insecureApiKey && !options.allowInsecureApiKey) { + if (hasApiKey && options.protocol === 'http:' && !isLoopbackHost(options.hostname)) { log.error( 'Not sending the Datadog API key over a non-TLS connection to %s. Configure an https intake URL.', options.hostname diff --git a/packages/dd-trace/src/openfeature/agentless_configuration_source.js b/packages/dd-trace/src/openfeature/agentless_configuration_source.js index f11c581373..fd86a16acc 100644 --- a/packages/dd-trace/src/openfeature/agentless_configuration_source.js +++ b/packages/dd-trace/src/openfeature/agentless_configuration_source.js @@ -18,7 +18,6 @@ const RETRY_JITTER = 0.2 /** * @typedef {object} AgentlessSourceConfig * @property {URL} endpoint - * @property {boolean} allowInsecureApiKey * @property {number} pollIntervalMs * @property {number} requestTimeoutMs * @property {string | undefined} apiKey @@ -165,8 +164,6 @@ class AgentlessConfigurationSource { url: this.#config.endpoint, method: 'GET', headers, - // An explicit operator override may be a cleartext development or dogfooding proxy. - allowInsecureApiKey: this.#config.allowInsecureApiKey, retry: false, signal, timeout: this.#config.requestTimeoutMs, @@ -231,10 +228,7 @@ class AgentlessConfigurationSource { this.#failureWarnings.add(category) if (statusCode === 401 || statusCode === 403) { - log.warn( - 'Feature Flagging agentless endpoint returned HTTP %d; verify DD_API_KEY is configured and valid', - statusCode - ) + log.warn('Feature Flagging agentless endpoint returned HTTP %d; verify endpoint authentication', statusCode) } else if (statusCode) { log.warn('Feature Flagging agentless endpoint returned HTTP %d after %d attempts', statusCode, attempts) } else if (attempts > 1) { diff --git a/packages/dd-trace/src/openfeature/configuration_source.js b/packages/dd-trace/src/openfeature/configuration_source.js index e2e2ee6e1a..92c2154f0a 100644 --- a/packages/dd-trace/src/openfeature/configuration_source.js +++ b/packages/dd-trace/src/openfeature/configuration_source.js @@ -36,10 +36,9 @@ function create (config, applyConfiguration) { const AgentlessConfigurationSource = require('./agentless_configuration_source') return new AgentlessConfigurationSource({ endpoint: endpoint(config, baseUrl), - allowInsecureApiKey: hasCustomEndpoint, pollIntervalMs: Math.min(pollIntervalSeconds, MAX_POLL_INTERVAL_SECONDS) * 1000, requestTimeoutMs: requestTimeoutSeconds * 1000, - apiKey: config.DD_API_KEY, + apiKey: hasCustomEndpoint ? undefined : config.DD_API_KEY, }, applyConfiguration) } catch (error) { log.error('Unable to configure Feature Flagging configuration source', error) diff --git a/packages/dd-trace/test/exporters/common/request.spec.js b/packages/dd-trace/test/exporters/common/request.spec.js index f4b8d058ff..eceaf687c6 100644 --- a/packages/dd-trace/test/exporters/common/request.spec.js +++ b/packages/dd-trace/test/exporters/common/request.spec.js @@ -847,23 +847,6 @@ describe('request', function () { }) }) - it('keeps dd-api-key over http for an explicitly trusted custom endpoint', (done) => { - nock('http://flags.dev.internal', { reqheaders: { 'dd-api-key': 'secret-key' } }) - .post('/v1/input') - .reply(200, 'OK') - - request(Buffer.from(''), { - method: 'POST', - url: new URL('http://flags.dev.internal/v1/input'), - headers: { 'dd-api-key': 'secret-key' }, - allowInsecureApiKey: true, - }, (err, res) => { - assert.strictEqual(res, 'OK') - sinon.assert.notCalled(log.error) - done(err) - }) - }) - for (const loopbackHost of ['127.0.0.1', '127.1.2.3', 'localhost', '[::1]']) { it(`keeps dd-api-key over http to the loopback host ${loopbackHost}`, (done) => { nock(`http://${loopbackHost}:9999`, { diff --git a/packages/dd-trace/test/openfeature/agentless_configuration_source.spec.js b/packages/dd-trace/test/openfeature/agentless_configuration_source.spec.js index e5f1e6fb18..11d8032d82 100644 --- a/packages/dd-trace/test/openfeature/agentless_configuration_source.spec.js +++ b/packages/dd-trace/test/openfeature/agentless_configuration_source.spec.js @@ -48,7 +48,6 @@ describe('AgentlessConfigurationSource', () => { applyConfiguration = sinon.stub() config = { endpoint: new URL('http://127.0.0.1:8080/api/v2/feature-flagging/config/rules-based/server'), - allowInsecureApiKey: true, pollIntervalMs: 30_000, requestTimeoutMs: 2000, apiKey: 'test-api-key', @@ -147,7 +146,6 @@ describe('AgentlessConfigurationSource', () => { assert.strictEqual(requests[0].data, '') assert.strictEqual(requests[0].options.url, config.endpoint) assert.strictEqual(requests[0].options.method, 'GET') - assert.strictEqual(requests[0].options.allowInsecureApiKey, config.allowInsecureApiKey) assert.strictEqual(requests[0].options.retry, false) assert.strictEqual(requests[0].options.timeout, 2000) assert.strictEqual(requests[0].options.headers['DD-API-KEY'], 'test-api-key') @@ -184,10 +182,11 @@ describe('AgentlessConfigurationSource', () => { clock = undefined const body = zlib.gzipSync(responseBody()) config.endpoint = new URL('http://flags.dev.internal:8080/custom/ufc') + delete config.apiKey nock('http://flags.dev.internal:8080', { + badheaders: ['dd-api-key'], reqheaders: { 'accept-encoding': 'gzip', - 'dd-api-key': 'test-api-key', }, }) .get('/custom/ufc') @@ -417,7 +416,7 @@ describe('AgentlessConfigurationSource', () => { 'third', ]) assert.deepStrictEqual(log.warn.thirdCall.args, [ - 'Feature Flagging agentless endpoint returned HTTP %d; verify DD_API_KEY is configured and valid', + 'Feature Flagging agentless endpoint returned HTTP %d; verify endpoint authentication', 401, ]) }) @@ -483,7 +482,7 @@ describe('AgentlessConfigurationSource', () => { assert.strictEqual(requests.length, 1) sinon.assert.calledOnceWithExactly( log.warn, - 'Feature Flagging agentless endpoint returned HTTP %d; verify DD_API_KEY is configured and valid', + 'Feature Flagging agentless endpoint returned HTTP %d; verify endpoint authentication', 401 ) @@ -504,7 +503,7 @@ describe('AgentlessConfigurationSource', () => { assert.strictEqual(Object.hasOwn(requests[0].options.headers, 'DD-API-KEY'), false) sinon.assert.calledOnceWithExactly( log.warn, - 'Feature Flagging agentless endpoint returned HTTP %d; verify DD_API_KEY is configured and valid', + 'Feature Flagging agentless endpoint returned HTTP %d; verify endpoint authentication', 401 ) sinon.assert.notCalled(applyConfiguration) diff --git a/packages/dd-trace/test/openfeature/configuration_source.spec.js b/packages/dd-trace/test/openfeature/configuration_source.spec.js index a1260ca118..dfbc53a80e 100644 --- a/packages/dd-trace/test/openfeature/configuration_source.spec.js +++ b/packages/dd-trace/test/openfeature/configuration_source.spec.js @@ -52,7 +52,6 @@ describe('OpenFeature configuration source', () => { 'https://ufc-server.ff-cdn.datadoghq.com/api/v2/feature-flagging/config/rules-based/server?dd_env=my+env' ) assert.strictEqual(resolved.apiKey, 'test-api-key') - assert.strictEqual(resolved.allowInsecureApiKey, false) assert.strictEqual(resolved.pollIntervalMs, 30_000) assert.strictEqual(resolved.requestTimeoutMs, 5000) }) @@ -87,10 +86,12 @@ describe('OpenFeature configuration source', () => { it(`allows the loopback endpoint ${baseUrl}`, () => { config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = baseUrl + const resolved = createSourceConfig() assert.strictEqual( - createSourceConfig().endpoint.toString(), + resolved.endpoint.toString(), `${baseUrl}/api/v2/feature-flagging/config/rules-based/server` ) + assert.strictEqual(resolved.apiKey, undefined) }) } @@ -103,17 +104,19 @@ describe('OpenFeature configuration source', () => { resolved.endpoint.toString(), 'http://flags.dev.internal:8080/api/v2/feature-flagging/config/rules-based/server' ) - assert.strictEqual(resolved.allowInsecureApiKey, true) + assert.strictEqual(resolved.apiKey, undefined) }) it('preserves an exact configured path and query', () => { config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = 'https://example.com/custom/ufc?tenant=one' + const resolved = createSourceConfig() assert.strictEqual( - createSourceConfig().endpoint.toString(), + resolved.endpoint.toString(), 'https://example.com/custom/ufc?tenant=one' ) + assert.strictEqual(resolved.apiKey, undefined) }) it('derives and creates the managed GovCloud endpoint without hard-coding availability', () => { From d82bd2b04dc56209139bd24d3534b63967875324 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 22 Jul 2026 21:48:20 -0600 Subject: [PATCH 3/3] fix(openfeature): clarify default endpoint error --- packages/dd-trace/src/openfeature/configuration_source.js | 2 +- .../dd-trace/test/openfeature/configuration_source.spec.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/dd-trace/src/openfeature/configuration_source.js b/packages/dd-trace/src/openfeature/configuration_source.js index 92c2154f0a..4510cfdf79 100644 --- a/packages/dd-trace/src/openfeature/configuration_source.js +++ b/packages/dd-trace/src/openfeature/configuration_source.js @@ -30,7 +30,7 @@ function create (config, applyConfiguration) { try { if (!hasCustomEndpoint && !config.DD_API_KEY) { - throw new Error('DD_API_KEY is required for the Datadog Feature Flagging API') + throw new Error('DD_API_KEY is required for the default Datadog Feature Flagging endpoint') } const AgentlessConfigurationSource = require('./agentless_configuration_source') diff --git a/packages/dd-trace/test/openfeature/configuration_source.spec.js b/packages/dd-trace/test/openfeature/configuration_source.spec.js index dfbc53a80e..17aa2ae9b3 100644 --- a/packages/dd-trace/test/openfeature/configuration_source.spec.js +++ b/packages/dd-trace/test/openfeature/configuration_source.spec.js @@ -201,7 +201,7 @@ describe('OpenFeature configuration source', () => { sinon.assert.notCalled(log.error) }) - it('requires a Datadog API key for the default Datadog API', () => { + it('requires a Datadog API key for the default Datadog Feature Flagging endpoint', () => { delete config.DD_API_KEY const source = configurationSource.create(config, sinon.spy()) @@ -209,7 +209,7 @@ describe('OpenFeature configuration source', () => { sinon.assert.calledOnceWithMatch( log.error, 'Unable to configure Feature Flagging configuration source', - sinon.match.has('message', 'DD_API_KEY is required for the Datadog Feature Flagging API') + sinon.match.has('message', 'DD_API_KEY is required for the default Datadog Feature Flagging endpoint') ) assert.strictEqual(source, undefined) sinon.assert.notCalled(AgentlessConfigurationSource)