Skip to content

Commit 4f57fe8

Browse files
committed
feat(openfeature): preserve configuration source semantics
1 parent 053684d commit 4f57fe8

13 files changed

Lines changed: 217 additions & 83 deletions

integration-tests/openfeature/openfeature-agentless.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ describe('OpenFeature agentless configuration integration', () => {
8686
cwd,
8787
env: {
8888
DD_API_KEY: 'integration-api-key',
89-
DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED: 'true',
89+
DD_FEATURE_FLAGS_ENABLED: 'true',
9090
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL: backendUrl,
9191
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS: '5',
9292
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: '1',

integration-tests/openfeature/openfeature-exposure-events.spec.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('OpenFeature Remote Config and Exposure Events Integration', () => {
6060
env: {
6161
DD_TRACE_AGENT_PORT: agent.port,
6262
DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS: '0.1',
63-
DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED: 'true',
63+
DD_FEATURE_FLAGS_ENABLED: 'true',
6464
// Preserve the existing RC exposure path until agentless emission is supported.
6565
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: 'remote_config',
6666
},
@@ -161,7 +161,7 @@ describe('OpenFeature Remote Config and Exposure Events Integration', () => {
161161
env: {
162162
DD_TRACE_AGENT_PORT: agent.port,
163163
DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS: '0.1',
164-
DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED: 'true',
164+
DD_FEATURE_FLAGS_ENABLED: 'true',
165165
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: 'remote_config',
166166
},
167167
})
@@ -245,7 +245,7 @@ describe('OpenFeature Remote Config and Exposure Events Integration', () => {
245245
env: {
246246
DD_TRACE_AGENT_PORT: agent.port,
247247
DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS: '0.1',
248-
DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED: 'true',
248+
DD_FEATURE_FLAGS_ENABLED: 'true',
249249
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: 'remote_config',
250250
},
251251
})
@@ -307,7 +307,7 @@ describe('OpenFeature Remote Config and Exposure Events Integration', () => {
307307
cwd,
308308
env: {
309309
DD_TRACE_AGENT_PORT: agent.port,
310-
DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED: 'false',
310+
DD_FEATURE_FLAGS_ENABLED: 'false',
311311
},
312312
})
313313
})

packages/dd-trace/src/config/generated-config-types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export interface GeneratedConfig {
8787
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL: string | undefined;
8888
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS: number;
8989
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: number;
90+
DD_FEATURE_FLAGS_ENABLED: boolean;
9091
DD_GIT_BRANCH: string | undefined;
9192
DD_GIT_COMMIT_AUTHOR_DATE: string | undefined;
9293
DD_GIT_COMMIT_AUTHOR_EMAIL: string | undefined;
@@ -696,6 +697,7 @@ export interface GeneratedEnvVarConfig {
696697
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL: string | undefined;
697698
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS: number;
698699
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: number;
700+
DD_FEATURE_FLAGS_ENABLED: boolean;
699701
DD_GIT_BRANCH: string | undefined;
700702
DD_GIT_COMMIT_AUTHOR_DATE: string | undefined;
701703
DD_GIT_COMMIT_AUTHOR_EMAIL: string | undefined;

packages/dd-trace/src/config/supported-configurations.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,13 @@
817817
"default": "false"
818818
}
819819
],
820+
"DD_FEATURE_FLAGS_ENABLED": [
821+
{
822+
"implementation": "A",
823+
"type": "boolean",
824+
"default": "true"
825+
}
826+
],
820827
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED": [
821828
{
822829
"implementation": "A",

packages/dd-trace/src/openfeature/configuration_source.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const log = require('../log')
44

55
const CONFIGURATION_SOURCE_AGENTLESS = 'agentless'
6+
const CONFIGURATION_SOURCE_DISABLED = 'disabled'
67
const CONFIGURATION_SOURCE_REMOTE_CONFIG = 'remote_config'
78

89
const DEFAULT_AGENTLESS_PATH = '/api/v2/feature-flagging/config/rules-based/server'
@@ -19,7 +20,7 @@ const MAX_POLL_INTERVAL_SECONDS = 60 * 60
1920
function resolve (config) {
2021
const mode = resolveMode(config)
2122

22-
if (mode === CONFIGURATION_SOURCE_REMOTE_CONFIG) {
23+
if (mode !== CONFIGURATION_SOURCE_AGENTLESS) {
2324
return { mode }
2425
}
2526

@@ -86,6 +87,23 @@ function isRemoteConfig (config) {
8687
}
8788
}
8889

90+
/**
91+
* Reports whether the provider should be exposed for the resolved source.
92+
*
93+
* Invalid source values fail closed.
94+
*
95+
* @param {import('../config/config-base')} config - Tracer configuration.
96+
* @returns {boolean} Whether Feature Flagging is enabled.
97+
*/
98+
function isEnabled (config) {
99+
try {
100+
return resolveMode(config) !== CONFIGURATION_SOURCE_DISABLED
101+
} catch (error) {
102+
log.error('Unable to configure Feature Flagging configuration source', error)
103+
return false
104+
}
105+
}
106+
89107
/**
90108
* Normalizes and validates source selection without resolving agentless
91109
* endpoint or timing configuration.
@@ -94,7 +112,25 @@ function isRemoteConfig (config) {
94112
* @returns {string} Selected configuration-source mode.
95113
*/
96114
function resolveMode (config) {
115+
if (config.DD_FEATURE_FLAGS_ENABLED === false) return CONFIGURATION_SOURCE_DISABLED
116+
97117
const value = config.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE
118+
const origin = config.getOrigin?.('DD_FEATURE_FLAGS_CONFIGURATION_SOURCE')
119+
const hasExplicitSource = origin ? origin !== 'default' : value !== undefined && value !== null
120+
121+
if (!hasExplicitSource) {
122+
const legacyEnabled = config.experimental?.flaggingProvider?.enabled
123+
const legacyOrigin = config.getOrigin?.('experimental.flaggingProvider.enabled')
124+
const hasExplicitLegacySetting = legacyOrigin
125+
? legacyOrigin !== 'default'
126+
: legacyEnabled !== undefined && legacyEnabled !== null
127+
128+
if (hasExplicitLegacySetting) {
129+
if (legacyEnabled === true) return CONFIGURATION_SOURCE_REMOTE_CONFIG
130+
if (legacyEnabled === false) return CONFIGURATION_SOURCE_DISABLED
131+
}
132+
}
133+
98134
const mode = String(value ?? '').trim().toLowerCase() || CONFIGURATION_SOURCE_AGENTLESS
99135
if (mode !== CONFIGURATION_SOURCE_AGENTLESS && mode !== CONFIGURATION_SOURCE_REMOTE_CONFIG) {
100136
throw new Error(`Unsupported Feature Flagging configuration source: ${mode}`)
@@ -176,6 +212,7 @@ function positiveMilliseconds (value, fallbackSeconds, setting, maximumSeconds)
176212

177213
module.exports = {
178214
enable,
215+
isEnabled,
179216
isRemoteConfig,
180217
resolve,
181218
}

packages/dd-trace/src/openfeature/index.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ function enable (config) {
4747
setAgentStrategy(config, hasAgent => {
4848
exposuresWriter?.setEnabled(hasAgent)
4949
})
50-
51-
log.debug('OpenFeature module enabled')
5250
}
5351

5452
/**

packages/dd-trace/src/openfeature/register.js

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,36 @@ const noop = new (require('./noop'))()
1111
function hasFlaggingProvider (proxy) {
1212
const descriptor = Reflect.getOwnPropertyDescriptor(proxy, 'openfeature')
1313

14-
return descriptor?.value !== undefined && descriptor.value !== noop
14+
return descriptor?.get !== undefined || (descriptor?.value !== undefined && descriptor.value !== noop)
15+
}
16+
17+
/**
18+
* Exposes the provider without constructing it until application code reads it.
19+
* The generic tracer lazy proxy is eager outside serverless environments, while
20+
* agentless delivery must remain silent until the application uses OpenFeature.
21+
*
22+
* @param {import('../proxy')} proxy
23+
* @param {import('../tracer')} tracer
24+
* @param {import('../config/config-base')} config
25+
* @param {object} configurationSource
26+
*/
27+
function defineFlaggingProvider (proxy, tracer, config, configurationSource) {
28+
Reflect.defineProperty(proxy, 'openfeature', {
29+
get () {
30+
const FlaggingProvider = require('./flagging_provider')
31+
const provider = new FlaggingProvider(tracer, config)
32+
33+
Reflect.defineProperty(proxy, 'openfeature', {
34+
value: provider,
35+
configurable: true,
36+
enumerable: true,
37+
})
38+
configurationSource.enable(config, () => provider)
39+
return provider
40+
},
41+
configurable: true,
42+
enumerable: true,
43+
})
1544
}
1645

1746
registerFeature({
@@ -27,28 +56,26 @@ registerFeature({
2756
remoteConfig (rc, config, proxy) {
2857
const configurationSource = require('./configuration_source')
2958
const openfeatureRemoteConfig = require('./remote_config')
59+
const subscribe = configurationSource.isRemoteConfig(config)
3060
openfeatureRemoteConfig.enable(
3161
rc,
32-
config,
3362
() => proxy.openfeature,
34-
configurationSource.isRemoteConfig(config)
63+
subscribe
3564
)
3665
},
3766

3867
/**
3968
* @param {import('../config/config-base')} config
4069
* @param {import('../tracer')} tracer
4170
* @param {import('../proxy')} proxy
42-
* @param {Function} lazyProxy
4371
*/
44-
enable (config, tracer, proxy, lazyProxy) {
45-
if (config.experimental.flaggingProvider.enabled) {
46-
proxy._modules.openfeature.enable(config)
47-
if (!hasFlaggingProvider(proxy)) {
48-
lazyProxy(proxy, 'openfeature', () => require('./flagging_provider'), tracer, config)
49-
}
50-
const configurationSource = require('./configuration_source')
51-
configurationSource.enable(config, () => proxy.openfeature)
72+
enable (config, tracer, proxy) {
73+
const configurationSource = require('./configuration_source')
74+
if (!configurationSource.isEnabled(config)) return
75+
76+
proxy._modules.openfeature.enable(config)
77+
if (!hasFlaggingProvider(proxy)) {
78+
defineFlaggingProvider(proxy, tracer, config, configurationSource)
5279
}
5380
},
5481
})

packages/dd-trace/src/openfeature/remote_config.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,15 @@ const RemoteConfigCapabilities = require('../remote_config/capabilities')
66
* Configures remote config handlers for openfeature feature flagging
77
*
88
* @param {object} rc - RemoteConfig instance
9-
* @param {object} config - Tracer config
109
* @param {Function} getOpenfeatureProxy - Function that returns the OpenFeature proxy from tracer
1110
* @param {boolean} [subscribe] - Whether Agent Remote Config owns UFC delivery
1211
*/
13-
function enable (rc, config, getOpenfeatureProxy, subscribe = true) {
14-
// Always enable capability for feature flag configuration
15-
// This indicates the library supports this capability via remote config
16-
rc.updateCapabilities(RemoteConfigCapabilities.FFE_FLAG_CONFIGURATION_RULES, true)
12+
function enable (rc, getOpenfeatureProxy, subscribe = true) {
13+
// Capability advertisement and product subscription both opt into the billed
14+
// Agent Remote Config delivery path.
15+
if (!subscribe) return
1716

18-
// Only register the product handler when the provider is enabled and Agent
19-
// Remote Config is the selected UFC source. The capability stays enabled
20-
// because it describes library support, not the active delivery mode.
21-
if (!config.experimental.flaggingProvider.enabled || !subscribe) return
17+
rc.updateCapabilities(RemoteConfigCapabilities.FFE_FLAG_CONFIGURATION_RULES, true)
2218

2319
// Set product handler for FFE_FLAGS
2420
rc.setProductHandler('FFE_FLAGS', (action, conf) => {

packages/dd-trace/test/config/index.spec.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4939,17 +4939,38 @@ rules:
49394939
})
49404940

49414941
context('Feature Flagging configuration source', () => {
4942-
it('defaults to agentless delivery with cross-SDK timings', () => {
4942+
it('defaults to an enabled provider with agentless delivery and cross-SDK timings', () => {
49434943
const config = getConfig()
49444944

49454945
assertObjectContains(config, {
4946+
DD_FEATURE_FLAGS_ENABLED: true,
49464947
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: 'agentless',
49474948
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL: undefined,
49484949
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS: 30,
49494950
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: 2,
49504951
})
4952+
assert.strictEqual(config.experimental.flaggingProvider.enabled, false)
49514953
})
49524954

4955+
it('reads the stable provider kill switch', () => {
4956+
process.env.DD_FEATURE_FLAGS_ENABLED = 'false'
4957+
4958+
const config = getConfig()
4959+
4960+
assert.strictEqual(config.DD_FEATURE_FLAGS_ENABLED, false)
4961+
})
4962+
4963+
for (const value of ['true', 'false']) {
4964+
it(`retains the deprecated experimental provider setting when set to ${value}`, () => {
4965+
process.env.DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED = value
4966+
4967+
const config = getConfig()
4968+
4969+
assert.strictEqual(config.DD_FEATURE_FLAGS_ENABLED, true)
4970+
assert.strictEqual(config.experimental.flaggingProvider.enabled, value === 'true')
4971+
})
4972+
}
4973+
49534974
it('reads the configuration source environment variable', () => {
49544975
process.env.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE = 'remote_config'
49554976

packages/dd-trace/test/openfeature/configuration_source.spec.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,17 @@ describe('OpenFeature configuration source', () => {
1616
beforeEach(() => {
1717
config = {
1818
DD_API_KEY: 'test-api-key',
19+
DD_FEATURE_FLAGS_ENABLED: true,
1920
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: 'agentless',
2021
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL: undefined,
2122
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS: 30,
2223
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: 2,
24+
experimental: {
25+
flaggingProvider: {
26+
enabled: false,
27+
},
28+
},
29+
getOrigin: sinon.stub().returns('default'),
2330
site: 'datadoghq.com',
2431
env: 'my env',
2532
}
@@ -43,6 +50,54 @@ describe('OpenFeature configuration source', () => {
4350
})
4451
}
4552

53+
it('grandfathers the legacy enabled setting onto Remote Config when the source is defaulted', () => {
54+
config.experimental.flaggingProvider.enabled = true
55+
config.getOrigin.withArgs('experimental.flaggingProvider.enabled').returns('env_var')
56+
57+
assert.deepStrictEqual(configurationSource.resolve(config), { mode: 'remote_config' })
58+
assert.strictEqual(configurationSource.isRemoteConfig(config), true)
59+
assert.strictEqual(configurationSource.isEnabled(config), true)
60+
})
61+
62+
it('keeps the provider disabled for the legacy false setting when the source is defaulted', () => {
63+
config.experimental.flaggingProvider.enabled = false
64+
config.getOrigin.withArgs('experimental.flaggingProvider.enabled').returns('env_var')
65+
66+
assert.deepStrictEqual(configurationSource.resolve(config), { mode: 'disabled' })
67+
assert.strictEqual(configurationSource.isRemoteConfig(config), false)
68+
assert.strictEqual(configurationSource.isEnabled(config), false)
69+
})
70+
71+
it('lets an explicit agentless source override legacy enablement', () => {
72+
config.experimental.flaggingProvider.enabled = true
73+
config.getOrigin.returns('env_var')
74+
75+
assert.strictEqual(configurationSource.resolve(config).mode, 'agentless')
76+
assert.strictEqual(configurationSource.isRemoteConfig(config), false)
77+
assert.strictEqual(configurationSource.isEnabled(config), true)
78+
})
79+
80+
it('lets an explicit Remote Config source override legacy disablement', () => {
81+
config.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE = 'remote_config'
82+
config.experimental.flaggingProvider.enabled = false
83+
config.getOrigin.returns('env_var')
84+
85+
assert.deepStrictEqual(configurationSource.resolve(config), { mode: 'remote_config' })
86+
assert.strictEqual(configurationSource.isRemoteConfig(config), true)
87+
assert.strictEqual(configurationSource.isEnabled(config), true)
88+
})
89+
90+
it('lets the stable kill switch override every configuration source', () => {
91+
config.DD_FEATURE_FLAGS_ENABLED = false
92+
config.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE = 'remote_config'
93+
config.experimental.flaggingProvider.enabled = true
94+
config.getOrigin.returns('env_var')
95+
96+
assert.deepStrictEqual(configurationSource.resolve(config), { mode: 'disabled' })
97+
assert.strictEqual(configurationSource.isRemoteConfig(config), false)
98+
assert.strictEqual(configurationSource.isEnabled(config), false)
99+
})
100+
46101
it('defaults to the Datadog UFC CDN endpoint and includes the environment', () => {
47102
config.DD_SITE = 'raw-env-key.invalid'
48103
const resolved = configurationSource.resolve(config)

0 commit comments

Comments
 (0)