Skip to content
Closed
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
1 change: 0 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -800,7 +800,6 @@ declare namespace tracer {
flaggingProvider?: {
/**
* Whether to enable the feature flagging provider.
* Requires Remote Config to be properly configured.
* Can be configured via DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED environment variable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this file needs more revising.

Would it be beneficial to mention DD_FEATURE_FLAGS_ENABLED to encourage the newer env variable instead? Also, is it worth documenting DD_FEATURE_FLAGS_CONFIGURATION_SOURCE here? Another place to add documentation about configuring the provider might be docs/API.md

Also the provider now defaults to enabled, and that legacy variable specifically selects Remote Config, so this might be confusing

dd-trace-js/index.d.ts

Lines 159 to 164 in 5b83654

* OpenFeature Provider with Remote Config integration.
*
* Extends DatadogNodeServerProvider with Remote Config integration for dynamic flag configuration.
* Enable with DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true.
*
* @env DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED

*
* @default false
Expand Down
1 change: 0 additions & 1 deletion index.d.v5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,6 @@ declare namespace tracer {
flaggingProvider?: {
/**
* Whether to enable the feature flagging provider.
* Requires Remote Config to be properly configured.
* Can be configured via DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED environment variable.
*
* @default false
Expand Down
34 changes: 34 additions & 0 deletions integration-tests/openfeature/app/agentless-evaluation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict'

const tracer = require('dd-trace')
const http = require('node:http')
const { OpenFeature } = require('@openfeature/server-sdk')

tracer.init({
env: 'integration',
service: 'ffe-agentless-integration',
})

OpenFeature.setProvider(tracer.openfeature)
const client = OpenFeature.getClient()

const server = http.createServer((request, response) => {
if (request.url !== '/evaluate') {
response.writeHead(404).end()
return
}

client.getStringDetails('agentless-integration-flag', 'default', {
targetingKey: 'integration-user',
}).then(details => {
response.setHeader('Content-Type', 'application/json')
response.end(JSON.stringify(details))
}, error => {
response.writeHead(500, { 'Content-Type': 'application/json' })
response.end(JSON.stringify({ error: error.message }))
})
})

server.listen(0, function () {
process.send?.({ port: this.address().port })
})
132 changes: 132 additions & 0 deletions integration-tests/openfeature/openfeature-agentless.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
'use strict'

const assert = require('node:assert/strict')
const http = require('node:http')
const path = require('node:path')
const zlib = require('node:zlib')
const { afterEach, before, beforeEach, describe, it } = require('mocha')
const { VERSION } = require('../../version')
const { sandboxCwd, useSandbox, spawnProc, stopProc } = require('../helpers')

const UFC = {
createdAt: '2026-01-01T00:00:00.000Z',
environment: { name: 'integration' },
flags: {
'agentless-integration-flag': {
key: 'agentless-integration-flag',
enabled: true,
variationType: 'STRING',
variations: {
local: { key: 'local', value: 'loaded-from-agentless' },
},
allocations: [
{
key: 'agentless-integration-allocation',
splits: [{ variationKey: 'local', shards: [] }],
doLog: false,
},
],
},
},
}

describe('OpenFeature agentless configuration integration', () => {
let appFile
let backend
let backendUrl
let cwd
let observedRequests
let proc

useSandbox(
['@openfeature/server-sdk', '@openfeature/core'],
false,
[path.join(__dirname, 'app')]
)

before(() => {
cwd = sandboxCwd()
appFile = path.join(cwd, 'app', 'agentless-evaluation.js')
})

beforeEach(async () => {
observedRequests = []
backend = http.createServer((request, response) => {
observedRequests.push({
url: request.url,
headers: request.headers,
})

if (request.headers['if-none-match'] === '"agentless-integration"') {
response.writeHead(304).end()
return
}

response.writeHead(200, {
'Content-Type': 'application/json',
'Content-Encoding': 'gzip',
ETag: '"agentless-integration"',
})
const body = JSON.stringify({
data: {
id: '1',
type: 'universal-flag-configuration',
attributes: UFC,
},
})
response.end(zlib.gzipSync(body))
})
await new Promise((resolve, reject) => {
backend.once('error', reject)
backend.listen(0, '127.0.0.1', resolve)
})
backendUrl = `http://127.0.0.1:${backend.address().port}`

proc = await spawnProc(appFile, {
cwd,
env: {
DD_API_KEY: 'integration-api-key',
DD_FEATURE_FLAGS_ENABLED: 'true',
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL: backendUrl,
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS: '5',
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: '1',
DD_INSTRUMENTATION_TELEMETRY_ENABLED: 'false',
DD_REMOTE_CONFIGURATION_ENABLED: 'false',
},
})
})

afterEach(async () => {
await stopProc(proc)
await new Promise(resolve => backend.close(resolve))
})

it('loads UFC from the default agentless source and evaluates locally', async () => {
let details
for (let attempt = 0; attempt < 50; attempt++) {
const response = await fetch(`${proc.url}/evaluate`)
details = await response.json()
if (details.value === 'loaded-from-agentless') break
await new Promise(resolve => setTimeout(resolve, 20))
}

assert.strictEqual(details.value, 'loaded-from-agentless')
assert.notStrictEqual(details.reason, 'ERROR')

for (let attempt = 0; observedRequests.length < 2 && attempt < 350; attempt++) {
await new Promise(resolve => setTimeout(resolve, 20))
}

assert.ok(observedRequests.length >= 2)
assert.strictEqual(
observedRequests[0].url,
'/api/v2/feature-flagging/config/rules-based/server'
)
assert.strictEqual(observedRequests[0].headers['dd-api-key'], 'integration-api-key')
assert.strictEqual(observedRequests[0].headers['accept-encoding'], 'gzip')
assert.strictEqual(observedRequests[0].headers['dd-client-library-language'], 'nodejs')
assert.strictEqual(observedRequests[0].headers['dd-client-library-version'], VERSION)
assert.strictEqual(observedRequests[0].headers['dd-flagging-source-mode'], undefined)
assert.strictEqual(observedRequests[1].headers['if-none-match'], '"agentless-integration"')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ describe('OpenFeature Remote Config and Exposure Events Integration', () => {
env: {
DD_TRACE_AGENT_PORT: agent.port,
DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS: '0.1',
DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED: 'true',
DD_FEATURE_FLAGS_ENABLED: 'true',
// Preserve the existing RC exposure path until agentless emission is supported.
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: 'remote_config',
},
})
})
Expand Down Expand Up @@ -159,7 +161,8 @@ describe('OpenFeature Remote Config and Exposure Events Integration', () => {
env: {
DD_TRACE_AGENT_PORT: agent.port,
DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS: '0.1',
DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED: 'true',
DD_FEATURE_FLAGS_ENABLED: 'true',
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: 'remote_config',
},
})
})
Expand Down Expand Up @@ -242,7 +245,8 @@ describe('OpenFeature Remote Config and Exposure Events Integration', () => {
env: {
DD_TRACE_AGENT_PORT: agent.port,
DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS: '0.1',
DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED: 'true',
DD_FEATURE_FLAGS_ENABLED: 'true',
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: 'remote_config',
},
})
})
Expand Down Expand Up @@ -303,7 +307,7 @@ describe('OpenFeature Remote Config and Exposure Events Integration', () => {
cwd,
env: {
DD_TRACE_AGENT_PORT: agent.port,
DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED: 'false',
DD_FEATURE_FLAGS_ENABLED: 'false',
},
})
})
Expand Down
10 changes: 10 additions & 0 deletions packages/dd-trace/src/config/generated-config-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ export interface GeneratedConfig {
DD_EXPERIMENTAL_TEST_OPT_VITEST_NO_WORKER_INIT: boolean | undefined;
DD_EXPERIMENTAL_TEST_REQUESTS_FS_CACHE: boolean;
DD_EXTERNAL_ENV: string | undefined;
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: string;
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL: string | undefined;
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS: number;
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: number;
DD_FEATURE_FLAGS_ENABLED: boolean;
DD_GIT_BRANCH: string | undefined;
DD_GIT_COMMIT_AUTHOR_DATE: string | undefined;
DD_GIT_COMMIT_AUTHOR_EMAIL: string | undefined;
Expand Down Expand Up @@ -688,6 +693,11 @@ export interface GeneratedEnvVarConfig {
DD_EXPERIMENTAL_TEST_OPT_VITEST_NO_WORKER_INIT: boolean | undefined;
DD_EXPERIMENTAL_TEST_REQUESTS_FS_CACHE: boolean;
DD_EXTERNAL_ENV: string | undefined;
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: string;
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL: string | undefined;
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS: number;
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: number;
DD_FEATURE_FLAGS_ENABLED: boolean;
DD_GIT_BRANCH: string | undefined;
DD_GIT_COMMIT_AUTHOR_DATE: string | undefined;
DD_GIT_COMMIT_AUTHOR_EMAIL: string | undefined;
Expand Down
1 change: 1 addition & 0 deletions packages/dd-trace/src/config/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* @property {string} [namespace] Nests the canonical env name under this property path (e.g. `telemetry`).
* @property {string} [transform]
* @property {string} [allowed]
* @property {string} [description]
* @property {string|boolean} [deprecated]
* @property {boolean} [sensitive] Excludes the configuration value from configuration telemetry.
*/
Expand Down
39 changes: 39 additions & 0 deletions packages/dd-trace/src/config/supported-configurations.json
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,13 @@
"default": "false"
}
],
"DD_FEATURE_FLAGS_ENABLED": [
{
"implementation": "A",
"type": "boolean",
"default": "true"
}
],
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED": [
{
"implementation": "A",
Expand All @@ -827,6 +834,38 @@
"default": "false"
}
],
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE": [
{
"implementation": "A",
"type": "string",
"default": "agentless",
"description": "Experimental: Select where Feature Flagging loads Universal Flag Configuration. Supported values are agentless and remote_config."
}
],
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL": [
{
"implementation": "A",
"type": "string",
"default": null,
"description": "Experimental: Override the agentless Feature Flagging UFC endpoint or base URL."
}
],
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS": [
{
"implementation": "A",
"type": "int",
"default": "30",
"description": "Experimental: Set the agentless Feature Flagging UFC polling interval in seconds, capped at one hour."
}
],
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS": [
{
"implementation": "A",
"type": "int",
"default": "2",
"description": "Experimental: Set the agentless Feature Flagging UFC request timeout in seconds."
}
],
"DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": [
{
"implementation": "B",
Expand Down
21 changes: 21 additions & 0 deletions packages/dd-trace/src/exporters/common/client-library-headers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict'

const { VERSION } = require('../../../../../version')

const LANGUAGE = 'nodejs'

/**
* Returns the canonical client-library identification headers.
*
* @param {string} [language] - Client library language name.
* @param {string} [version] - Client library version.
* @returns {Record<string, string>} Client library identification headers.
*/
function getClientLibraryHeaders (language = LANGUAGE, version = VERSION) {
return {
'DD-Client-Library-Language': language,
'DD-Client-Library-Version': version,
}
}

module.exports = { getClientLibraryHeaders }
Loading
Loading