Skip to content

Commit 56d7d04

Browse files
committed
fix(openfeature): preserve unsupported configuration sources
1 parent 293cf61 commit 56d7d04

7 files changed

Lines changed: 66 additions & 27 deletions

File tree

integration-tests/openfeature/openfeature-configuration-sources.spec.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const AGENTLESS_RESPONSE = zlib.gzipSync(JSON.stringify({
6565
}))
6666

6767
/** @typedef {'absent'|'true'|'false'} BooleanSetting */
68-
/** @typedef {'agentless'|'remote_config'|'disabled'} Delivery */
68+
/** @typedef {'agentless'|'remote_config'|'disabled'|'error'} Delivery */
6969
/** @typedef {{ name: string, value?: string }} SourceSetting */
7070
/**
7171
* @typedef {object} ConfigurationCase
@@ -135,9 +135,10 @@ describe('OpenFeature configuration source contract', () => {
135135

136136
assert.strictEqual(configurationCases.length, 63)
137137
assert.deepStrictEqual(countDeliveries(configurationCases), {
138-
agentless: 16,
139-
remote_config: 16,
140-
disabled: 31,
138+
agentless: 12,
139+
remote_config: 12,
140+
disabled: 27,
141+
error: 12,
141142
})
142143

143144
await runCases(configurationCases)
@@ -241,12 +242,19 @@ async function runCase (testCase) {
241242
waitForObservation(agentlessRequests, testCase.identifier, 'agentless', hasObservation),
242243
sendCommand(proc, accessCommand),
243244
])
245+
} else if (testCase.expected === 'error') {
246+
await assert.rejects(
247+
sendCommand(proc, accessCommand),
248+
/Unsupported Feature Flagging configuration source: (offline|unsupported)/
249+
)
244250
} else {
245251
await sendCommand(proc, accessCommand)
246252
}
247253

248-
const { details } = await sendCommand(proc, { command: 'evaluate' })
249-
assertEvaluation(testCase, details)
254+
if (testCase.expected !== 'error') {
255+
const { details } = await sendCommand(proc, { command: 'evaluate' })
256+
assertEvaluation(testCase, details)
257+
}
250258

251259
await waitForObservation(
252260
remoteConfigRequests,
@@ -639,6 +647,7 @@ function countDeliveries (cases) {
639647
agentless: 0,
640648
remote_config: 0,
641649
disabled: 0,
650+
error: 0,
642651
}
643652
for (const testCase of cases) counts[testCase.expected]++
644653
return counts
@@ -677,6 +686,7 @@ function expectedDelivery (stable, source, legacy) {
677686
if (stable === 'false') return 'disabled'
678687
if (source.name === 'agentless') return 'agentless'
679688
if (source.name === 'remote_config') return 'remote_config'
689+
if (source.name === 'offline' || source.name === 'invalid') return 'error'
680690
if (legacy === 'true') return 'remote_config'
681691
if (legacy === 'false') return 'disabled'
682692
return 'agentless'

packages/dd-trace/src/config/defaults.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@ const parseErrors = new Map()
3232
* @param {string} source - The source of the value.
3333
* @param {string} baseMessage - The base message to use for the warning.
3434
* @param {Error} [error] - An error that was thrown while parsing the value.
35+
* @param {boolean} [pickedDefault] - Whether the invalid value was discarded in favor of a fallback.
3536
*/
36-
function warnInvalidValue (value, optionName, source, baseMessage, error) {
37+
function warnInvalidValue (value, optionName, source, baseMessage, error, pickedDefault = true) {
3738
const canonicalName = (optionsTable[optionName]?.canonicalName ?? optionName) + source
3839
// Lazy load log module to avoid circular dependency
3940
if (!parseErrors.has(canonicalName)) {
40-
// TODO: Rephrase: It will fallback to former source (or default if not set)
41-
let message = `${baseMessage}: ${util.inspect(value)} for ${optionName} (source: ${source}), picked default`
41+
let message = `${baseMessage}: ${util.inspect(value)} for ${optionName} (source: ${source})`
42+
if (pickedDefault) message += ', picked default'
4243
if (error) {
4344
error.stack = error.toString()
4445
message += `\n\n${util.inspect(error)}`
@@ -242,8 +243,10 @@ for (const [canonicalName, entries] of Object.entries(supportedConfigurations))
242243
const originalTransform = transformer
243244
transformer = (value, optionName, source) => {
244245
if (!allowed.test(value)) {
245-
warnInvalidValue(value, optionName, source, 'Invalid value')
246-
return
246+
const preserveInvalidSource = canonicalName === 'DD_FEATURE_FLAGS_CONFIGURATION_SOURCE' &&
247+
String(value).trim() !== ''
248+
warnInvalidValue(value, optionName, source, 'Invalid value', undefined, !preserveInvalidSource)
249+
if (!preserveInvalidSource) return
247250
}
248251
if (originalTransform) {
249252
value = originalTransform(value)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ export interface GeneratedConfig {
432432
};
433433
};
434434
featureFlags: {
435-
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: "agentless" | "remote_config";
435+
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: "agentless" | "remote_config" | "offline";
436436
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL: string | undefined;
437437
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS: number;
438438
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: number;
@@ -697,7 +697,7 @@ export interface GeneratedEnvVarConfig {
697697
DD_EXPERIMENTAL_TEST_OPT_VITEST_NO_WORKER_INIT: boolean | undefined;
698698
DD_EXPERIMENTAL_TEST_REQUESTS_FS_CACHE: boolean;
699699
DD_EXTERNAL_ENV: string | undefined;
700-
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: "agentless" | "remote_config";
700+
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: "agentless" | "remote_config" | "offline";
701701
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL: string | undefined;
702702
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS: number;
703703
DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: number;

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -840,8 +840,8 @@
840840
"implementation": "A",
841841
"type": "string",
842842
"default": "agentless",
843-
"allowed": "agentless|remote_config",
844-
"description": "Experimental: Select where Feature Flagging loads Universal Flag Configuration. Supported values are agentless and remote_config.",
843+
"allowed": "agentless|remote_config|offline",
844+
"description": "Select where Feature Flagging loads Universal Flag Configuration. Supported values are agentless and remote_config; offline is reserved and currently unsupported.",
845845
"namespace": "featureFlags",
846846
"transform": "toLowerCase"
847847
}
@@ -851,7 +851,7 @@
851851
"implementation": "A",
852852
"type": "string",
853853
"default": null,
854-
"description": "Experimental: Override the agentless Feature Flagging UFC endpoint or base URL.",
854+
"description": "Override the agentless Feature Flagging UFC endpoint or base URL.",
855855
"namespace": "featureFlags",
856856
"sensitive": true
857857
}
@@ -861,7 +861,7 @@
861861
"implementation": "A",
862862
"type": "int",
863863
"default": "30",
864-
"description": "Experimental: Set the agentless Feature Flagging UFC polling interval in seconds, capped at one hour.",
864+
"description": "Set the agentless Feature Flagging UFC polling interval in seconds, capped at one hour.",
865865
"allowed": "[1-9]\\d*",
866866
"namespace": "featureFlags"
867867
}
@@ -871,7 +871,7 @@
871871
"implementation": "A",
872872
"type": "int",
873873
"default": "5",
874-
"description": "Experimental: Set the agentless Feature Flagging UFC request timeout in seconds.",
874+
"description": "Set the agentless Feature Flagging UFC request timeout in seconds.",
875875
"allowed": "[1-9]\\d*",
876876
"namespace": "featureFlags"
877877
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ function create (config, applyConfiguration) {
2323
DD_FEATURE_FLAGS_ENABLED: enabled,
2424
} = config.featureFlags
2525

26-
if (!enabled || source !== 'agentless') {
26+
if (!enabled || source === 'remote_config') {
2727
return
2828
}
2929

30+
if (source !== 'agentless') {
31+
throw new Error(`Unsupported Feature Flagging configuration source: ${source}`)
32+
}
33+
3034
try {
3135
if (!config.DD_API_KEY) {
3236
throw new Error('DD_API_KEY is required for Feature Flagging agentless delivery')

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5034,16 +5034,26 @@ rules:
50345034
expected: { enabled: true, source: 'remote_config' },
50355035
},
50365036
{
5037-
name: 'falls back from an invalid source to legacy enablement',
5037+
name: 'fails closed for an invalid source',
5038+
source: 'other',
5039+
expected: { enabled: true, source: 'other' },
5040+
},
5041+
{
5042+
name: 'fails closed for the reserved offline source',
5043+
source: 'offline',
5044+
expected: { enabled: true, source: 'offline' },
5045+
},
5046+
{
5047+
name: 'fails closed for an invalid source despite legacy enablement',
50385048
source: 'other',
50395049
legacyEnabled: 'true',
5040-
expected: { enabled: true, source: 'remote_config' },
5050+
expected: { enabled: true, source: 'other' },
50415051
},
50425052
{
5043-
name: 'falls back from the reserved offline source to legacy enablement',
5053+
name: 'fails closed for the reserved offline source despite legacy enablement',
50445054
source: 'offline',
50455055
legacyEnabled: 'true',
5046-
expected: { enabled: true, source: 'remote_config' },
5056+
expected: { enabled: true, source: 'offline' },
50475057
},
50485058
]) {
50495059
it(name, () => {
@@ -5069,22 +5079,22 @@ rules:
50695079
})
50705080
}
50715081

5072-
it('falls back from an invalid source through calculated legacy precedence', () => {
5082+
it('preserves an explicit invalid source over calculated legacy precedence', () => {
50735083
process.env.DD_FEATURE_FLAGS_ENABLED = 'true'
50745084
process.env.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE = 'offline'
50755085
process.env.DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED = 'true'
50765086

50775087
const config = getConfig()
50785088

50795089
assert.strictEqual(config.featureFlags.DD_FEATURE_FLAGS_ENABLED, true)
5080-
assert.strictEqual(config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE, 'remote_config')
5090+
assert.strictEqual(config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE, 'offline')
50815091
assert.strictEqual(config.experimental.flaggingProvider.enabled, true)
50825092
assert.strictEqual(config.getOrigin('featureFlags.DD_FEATURE_FLAGS_ENABLED'), 'env_var')
5083-
assert.strictEqual(config.getOrigin('featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE'), 'calculated')
5093+
assert.strictEqual(config.getOrigin('featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE'), 'env_var')
50845094
assert.strictEqual(config.getOrigin('experimental.flaggingProvider.enabled'), 'env_var')
50855095
assertConfigUpdateContains(updateConfig.getCall(0).args[0], [
50865096
{ name: 'DD_FEATURE_FLAGS_ENABLED', value: true, origin: 'env_var' },
5087-
{ name: 'DD_FEATURE_FLAGS_CONFIGURATION_SOURCE', value: 'remote_config', origin: 'calculated' },
5097+
{ name: 'DD_FEATURE_FLAGS_CONFIGURATION_SOURCE', value: 'offline', origin: 'env_var' },
50885098
{ name: 'DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED', value: true, origin: 'env_var' },
50895099
])
50905100

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,18 @@ describe('OpenFeature configuration source', () => {
211211
sinon.assert.notCalled(AgentlessConfigurationSource)
212212
})
213213

214+
for (const source of ['offline', 'invalid']) {
215+
it(`throws for the unsupported ${source} source`, () => {
216+
config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE = source
217+
218+
assert.throws(
219+
() => configurationSource.create(config, sinon.spy()),
220+
new RegExp(`Unsupported Feature Flagging configuration source: ${source}`)
221+
)
222+
sinon.assert.notCalled(AgentlessConfigurationSource)
223+
})
224+
}
225+
214226
it('does not create an agentless source when Feature Flags are disabled', () => {
215227
config.featureFlags.DD_FEATURE_FLAGS_ENABLED = false
216228
delete config.site

0 commit comments

Comments
 (0)