Skip to content

Commit 2ea4035

Browse files
committed
feat(openfeature): select agentless exposure routes
1 parent 7e1d04d commit 2ea4035

8 files changed

Lines changed: 465 additions & 49 deletions

File tree

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@
367367
/integration-tests/import-variants.spec.js @DataDog/lang-platform-js
368368
/integration-tests/init/ @DataDog/lang-platform-js
369369
/integration-tests/init.spec.js @DataDog/lang-platform-js
370+
/integration-tests/helpers/fake-agent.js @DataDog/lang-platform-js @DataDog/feature-flagging-and-experimentation-sdk
370371
/integration-tests/memory-leak/ @DataDog/lang-platform-js
371372
/integration-tests/mocha-parallel-files-fixtures/ @DataDog/lang-platform-js
372373
/integration-tests/mocha-parallel-files.spec.js @DataDog/lang-platform-js

integration-tests/helpers/fake-agent.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ module.exports = class FakeAgent extends EventEmitter {
3131
port = 0
3232
advertiseDebuggerV2IntakeSupport = true
3333
debuggerV2IntakeStatusCode = 202
34+
evpProxyVersions = [2]
3435
/** @type {Set<import('net').Socket>} */
3536
#sockets = new Set()
3637
/** @type {Record<string, RemoteConfigFile>} */
@@ -50,6 +51,9 @@ module.exports = class FakeAgent extends EventEmitter {
5051
if (options.debuggerV2IntakeStatusCode !== undefined) {
5152
this.debuggerV2IntakeStatusCode = options.debuggerV2IntakeStatusCode
5253
}
54+
if (options.evpProxyVersions !== undefined) {
55+
this.evpProxyVersions = [...options.evpProxyVersions]
56+
}
5357
}
5458

5559
/**
@@ -376,7 +380,10 @@ function buildExpressServer (agent) {
376380
app.use(bodyParser.json({ limit: Infinity, type: 'application/json' }))
377381

378382
app.get('/info', (req, res) => {
379-
const endpoints = ['/evp_proxy/v2', '/debugger/v1/input']
383+
const endpoints = [
384+
...agent.evpProxyVersions.map(version => `/evp_proxy/v${version}`),
385+
'/debugger/v1/input',
386+
]
380387
if (agent.advertiseDebuggerV2IntakeSupport) {
381388
endpoints.push('/debugger/v2/input')
382389
}
@@ -565,10 +572,14 @@ function buildExpressServer (agent) {
565572
})
566573
})
567574

568-
app.post('/evp_proxy/v2/api/v2/exposures', (req, res) => {
575+
app.post([
576+
'/evp_proxy/v2/api/v2/exposures',
577+
'/evp_proxy/v4/api/v2/exposures',
578+
], (req, res) => {
569579
res.status(200).send()
570580
agent.emit('exposures', {
571581
headers: req.headers,
582+
path: req.path,
572583
payload: req.body,
573584
})
574585
})

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ describe('OpenFeature Remote Config and Exposure Events Integration', () => {
5454
let agent, proc
5555

5656
beforeEach(async () => {
57-
agent = await new FakeAgent().start()
57+
agent = await new FakeAgent(0, { evpProxyVersions: [2, 4] }).start()
5858
proc = await spawnProc(appFile, {
5959
cwd,
6060
env: {
@@ -78,7 +78,7 @@ describe('OpenFeature Remote Config and Exposure Events Integration', () => {
7878
let receivedAckUpdate = false
7979

8080
// Listen for exposure events
81-
agent.on('exposures', ({ payload, headers }) => {
81+
agent.on('exposures', ({ payload, headers, path }) => {
8282
assert.ok(Object.hasOwn(payload, 'exposures'), `Available keys: ${inspect(Object.keys(payload))}`)
8383
assertObjectContains(payload, {
8484
context: {
@@ -94,6 +94,7 @@ describe('OpenFeature Remote Config and Exposure Events Integration', () => {
9494
try {
9595
assert.strictEqual(headers['content-type'], 'application/json')
9696
assert.strictEqual(headers['x-datadog-evp-subdomain'], 'event-platform-intake')
97+
assert.strictEqual(path, '/evp_proxy/v2/api/v2/exposures')
9798

9899
// Verify we got exposure events from flag evaluations
99100
assert.strictEqual(exposureEvents.length, 2)

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const { channel } = require('dc-polyfill')
44
const log = require('../log')
55
const ExposuresWriter = require('./writers/exposures')
6-
const { setAgentStrategy } = require('./writers/util')
6+
const { setExposureDeliveryStrategy } = require('./writers/util')
77

88
const exposureSubmitCh = channel('ffe:exposure:submit')
99
const flushCh = channel('ffe:writers:flush')
@@ -45,10 +45,10 @@ function enable (config) {
4545
exposureSubmitCh.subscribe(_handleExposureSubmit)
4646
flushCh.subscribe(_handleFlush)
4747

48-
setAgentStrategy(config, (hasAgent, route) => {
48+
setExposureDeliveryStrategy(config, (enabled, route) => {
4949
if (exposuresWriter !== writer) return
5050

51-
writer.setEnabled(hasAgent, route)
51+
writer.setEnabled(enabled, route)
5252
})
5353
}
5454

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
11
'use strict'
22

3-
const logger = require('../../log')
4-
const { EVP_PROXY_PATH_V2 } = require('../../evp_proxy/constants')
3+
const {
4+
EVP_EVENT_PLATFORM_SUBDOMAIN,
5+
EVP_PROXY_PATH_V2,
6+
EVP_PROXY_PATH_V4,
7+
EVP_SUBDOMAIN_HEADER_NAME,
8+
} = require('../../evp_proxy/constants')
9+
const { createDirectEVPRoute } = require('../../evp_proxy/direct')
510
const { discoverEVPProxy } = require('../../evp_proxy/discovery')
11+
const logger = require('../../log')
12+
13+
let missingRouteWarningLogged = false
14+
15+
/**
16+
* Logs the unavailable exposure-delivery warning once.
17+
*
18+
* @returns {void}
19+
*/
20+
function warnExposureDeliveryUnavailable () {
21+
if (missingRouteWarningLogged) return
22+
missingRouteWarningLogged = true
23+
logger.warn(
24+
'Feature Flags exposure delivery is disabled because no compatible local EVP route or direct intake ' +
25+
'credentials are available.'
26+
)
27+
}
628

729
/**
8-
* Determines if the agent supports EVP proxy and sets the writer enabled state accordingly
30+
* Preserves Agent exposure delivery for the Remote Configuration source.
31+
*
932
* @param {import('../../config')} config - Tracer configuration object
1033
* @param {Function} setWriterEnabledValue - Callback to set the writer enabled state
34+
* @returns {void}
1135
*/
1236
function setAgentStrategy (config, setWriterEnabledValue) {
1337
discoverEVPProxy(config.url, {
1438
supportedPaths: [EVP_PROXY_PATH_V2],
15-
}, (err, route) => {
16-
if (err) {
17-
logger.debug('FFE Writer disabled - error getting agent info: %s', err.message)
39+
}, (error, route) => {
40+
if (error) {
41+
logger.debug('FFE Writer disabled - error getting agent info: %s', error.message)
1842
setWriterEnabledValue(false)
1943
return
2044
}
@@ -29,6 +53,67 @@ function setAgentStrategy (config, setWriterEnabledValue) {
2953
})
3054
}
3155

32-
module.exports = {
33-
setAgentStrategy,
56+
/**
57+
* Selects a local serverless receiver or authenticated direct intake.
58+
*
59+
* Local discovery is optional for delivery. A missing listener, discovery
60+
* error, or incompatible receiver selects direct intake when credentials exist.
61+
*
62+
* @param {import('../../config')} config - Tracer configuration object
63+
* @param {Function} setWriterEnabledValue - Callback to set the writer enabled state
64+
* @returns {void}
65+
*/
66+
function setAgentlessStrategy (config, setWriterEnabledValue) {
67+
const directRoute = createDirectEVPRoute(config, EVP_EVENT_PLATFORM_SUBDOMAIN)
68+
69+
discoverEVPProxy(config.url, {
70+
supportedPaths: [EVP_PROXY_PATH_V4, EVP_PROXY_PATH_V2],
71+
}, (error, localRoute) => {
72+
if (localRoute) {
73+
const route = {
74+
...localRoute,
75+
headers: {
76+
[EVP_SUBDOMAIN_HEADER_NAME]: EVP_EVENT_PLATFORM_SUBDOMAIN,
77+
},
78+
...(directRoute && { fallback: directRoute }),
79+
}
80+
logger.debug('FFE Writer enabled with local EVP route %s', route.basePath)
81+
setWriterEnabledValue(true, route)
82+
return
83+
}
84+
85+
if (directRoute) {
86+
if (error) {
87+
logger.debug('FFE Writer using direct EVP intake after local discovery failed: %s', error.message)
88+
} else {
89+
logger.debug('FFE Writer using direct EVP intake because no compatible local route was advertised')
90+
}
91+
setWriterEnabledValue(true, directRoute)
92+
return
93+
}
94+
95+
if (error) {
96+
logger.debug('FFE Writer disabled - error getting local receiver info: %s', error.message)
97+
}
98+
warnExposureDeliveryUnavailable()
99+
setWriterEnabledValue(false)
100+
})
101+
}
102+
103+
/**
104+
* Applies the exposure-delivery strategy for the configured Feature Flags source.
105+
*
106+
* @param {import('../../config')} config - Tracer configuration object
107+
* @param {Function} setWriterEnabledValue - Callback to set the writer enabled state
108+
* @returns {void}
109+
*/
110+
function setExposureDeliveryStrategy (config, setWriterEnabledValue) {
111+
if (config.featureFlags?.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE === 'agentless') {
112+
setAgentlessStrategy(config, setWriterEnabledValue)
113+
return
114+
}
115+
116+
setAgentStrategy(config, setWriterEnabledValue)
34117
}
118+
119+
module.exports = { setExposureDeliveryStrategy }

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe('OpenFeature Module', () => {
1414
let openfeatureModule
1515
let mockWriter
1616
let ExposuresWriterStub
17-
let setAgentStrategyStub
17+
let setExposureDeliveryStrategyStub
1818

1919
beforeEach(() => {
2020
config = {
@@ -30,11 +30,11 @@ describe('OpenFeature Module', () => {
3030
}
3131

3232
ExposuresWriterStub = sinon.stub().returns(mockWriter)
33-
setAgentStrategyStub = sinon.stub()
33+
setExposureDeliveryStrategyStub = sinon.stub()
3434

3535
openfeatureModule = proxyquire('../../src/openfeature', {
3636
'./writers/exposures': ExposuresWriterStub,
37-
'./writers/util': { setAgentStrategy: setAgentStrategyStub },
37+
'./writers/util': { setExposureDeliveryStrategy: setExposureDeliveryStrategyStub },
3838
})
3939
})
4040

@@ -52,17 +52,17 @@ describe('OpenFeature Module', () => {
5252
openfeatureModule.enable(config)
5353

5454
sinon.assert.calledOnceWithExactly(ExposuresWriterStub, config)
55-
sinon.assert.calledOnce(setAgentStrategyStub)
55+
sinon.assert.calledOnce(setExposureDeliveryStrategyStub)
5656
})
5757

58-
it('passes the discovered route to the writer', () => {
58+
it('configures the writer with the selected exposure route', () => {
59+
openfeatureModule.enable(config)
60+
const setWriterEnabled = setExposureDeliveryStrategyStub.firstCall.args[1]
5961
const route = {
60-
url: new URL('http://localhost:8126'),
61-
basePath: '/evp_proxy/v2',
62+
url: new URL('http://serverless-init:8126'),
63+
basePath: '/evp_proxy/v4',
6264
}
63-
setAgentStrategyStub.callsArgWith(1, true, route)
64-
65-
openfeatureModule.enable(config)
65+
setWriterEnabled(true, route)
6666

6767
sinon.assert.calledOnceWithExactly(mockWriter.setEnabled, true, route)
6868
})
@@ -85,10 +85,10 @@ describe('OpenFeature Module', () => {
8585
ExposuresWriterStub.onSecondCall().returns(replacementWriter)
8686

8787
openfeatureModule.enable(config)
88-
const staleCallback = setAgentStrategyStub.firstCall.args[1]
88+
const staleCallback = setExposureDeliveryStrategyStub.firstCall.args[1]
8989
openfeatureModule.disable()
9090
openfeatureModule.enable(config)
91-
const currentCallback = setAgentStrategyStub.secondCall.args[1]
91+
const currentCallback = setExposureDeliveryStrategyStub.secondCall.args[1]
9292

9393
staleCallback(true, staleRoute)
9494
sinon.assert.notCalled(replacementWriter.setEnabled)

0 commit comments

Comments
 (0)