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
34 changes: 33 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.
*
* @default false
Expand All @@ -818,6 +817,39 @@ declare namespace tracer {
* Programmatic configuration takes precedence over the environment variables listed above.
*/
initializationTimeoutMs?: number
/**
* Where Universal Flag Configuration is loaded from. Agentless delivery
* fetches from the Datadog UFC CDN endpoint and evaluates locally.
*
* @default 'agentless'
* @env DD_FEATURE_FLAGS_CONFIGURATION_SOURCE
* Programmatic configuration takes precedence over the environment variables listed above.
*/
configurationSource?: 'agentless' | 'remote_config'
/**
* Optional agentless configuration endpoint or base URL. A base URL
* receives the standard rules-based server path.
*
* @env DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL
* Programmatic configuration takes precedence over the environment variables listed above.
*/
agentlessBaseUrl?: string
/**
* Agentless configuration polling interval in seconds.
*
* @default 30
* @env DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS
* Programmatic configuration takes precedence over the environment variables listed above.
*/
agentlessPollIntervalSeconds?: number
/**
* Agentless configuration request timeout in seconds.
*
* @default 2
* @env DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS
* Programmatic configuration takes precedence over the environment variables listed above.
*/
agentlessRequestTimeoutSeconds?: number
/**
* Configuration for span enrichment with feature flag evaluation data.
*/
Expand Down
34 changes: 33 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 All @@ -888,6 +887,39 @@ declare namespace tracer {
* Programmatic configuration takes precedence over the environment variables listed above.
*/
initializationTimeoutMs?: number
/**
* Where Universal Flag Configuration is loaded from. Agentless delivery
* fetches from the Datadog UFC CDN endpoint and evaluates locally.
*
* @default 'agentless'
* @env DD_FEATURE_FLAGS_CONFIGURATION_SOURCE
* Programmatic configuration takes precedence over the environment variables listed above.
*/
configurationSource?: 'agentless' | 'remote_config'
/**
* Optional agentless configuration endpoint or base URL. A base URL
* receives the standard rules-based server path.
*
* @env DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL
* Programmatic configuration takes precedence over the environment variables listed above.
*/
agentlessBaseUrl?: string
/**
* Agentless configuration polling interval in seconds.
*
* @default 30
* @env DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS
* Programmatic configuration takes precedence over the environment variables listed above.
*/
agentlessPollIntervalSeconds?: number
/**
* Agentless configuration request timeout in seconds.
*
* @default 2
* @env DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS
* Programmatic configuration takes precedence over the environment variables listed above.
*/
agentlessRequestTimeoutSeconds?: number
/**
* Configuration for span enrichment with feature flag evaluation data.
*/
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 })
})
125 changes: 125 additions & 0 deletions integration-tests/openfeature/openfeature-agentless.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
'use strict'

const assert = require('node:assert/strict')
const http = require('node:http')
const path = require('node:path')
const { afterEach, before, beforeEach, describe, it } = require('mocha')
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',
ETag: '"agentless-integration"',
})
response.end(JSON.stringify({
data: {
id: '1',
type: 'universal-flag-configuration',
attributes: UFC,
},
}))
})
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_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED: 'true',
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL: backendUrl,
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS: '1',
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 < 150; 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['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 @@ -61,6 +61,7 @@ describe('OpenFeature Remote Config and Exposure Events Integration', () => {
DD_TRACE_AGENT_PORT: agent.port,
DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS: '0.1',
DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED: 'true',
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: 'remote_config',
},
})
})
Expand Down Expand Up @@ -160,6 +161,7 @@ describe('OpenFeature Remote Config and Exposure Events Integration', () => {
DD_TRACE_AGENT_PORT: agent.port,
DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS: '0.1',
DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED: 'true',
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: 'remote_config',
},
})
})
Expand Down Expand Up @@ -243,6 +245,7 @@ describe('OpenFeature Remote Config and Exposure Events Integration', () => {
DD_TRACE_AGENT_PORT: agent.port,
DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS: '0.1',
DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED: 'true',
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: 'remote_config',
},
})
})
Expand Down
8 changes: 8 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 @@ -424,6 +424,10 @@ export interface GeneratedConfig {
enableGetRumData: boolean;
exporter: string;
flaggingProvider: {
agentlessBaseUrl: string | undefined;
agentlessPollIntervalSeconds: number;
agentlessRequestTimeoutSeconds: number;
configurationSource: string;
enabled: boolean;
initializationTimeoutMs: number;
spanEnrichment: {
Expand Down Expand Up @@ -688,6 +692,10 @@ 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_GIT_BRANCH: string | undefined;
DD_GIT_COMMIT_AUTHOR_DATE: string | undefined;
DD_GIT_COMMIT_AUTHOR_EMAIL: string | undefined;
Expand Down
40 changes: 40 additions & 0 deletions packages/dd-trace/src/config/supported-configurations.json
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,46 @@
"default": "false"
}
],
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE": [
{
"implementation": "A",
"type": "string",
"configurationNames": [
"experimental.flaggingProvider.configurationSource"
],
"default": "agentless"
}
],
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL": [
{
"implementation": "A",
"type": "string",
"configurationNames": [
"experimental.flaggingProvider.agentlessBaseUrl"
],
"default": null
}
],
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS": [
{
"implementation": "A",
"type": "int",
"configurationNames": [
"experimental.flaggingProvider.agentlessPollIntervalSeconds"
],
"default": "30"
}
],
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS": [
{
"implementation": "A",
"type": "int",
"configurationNames": [
"experimental.flaggingProvider.agentlessRequestTimeoutSeconds"
],
"default": "2"
}
],
"DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": [
{
"implementation": "B",
Expand Down
Loading
Loading