Skip to content

Commit 7296d40

Browse files
rochdevclaude
andcommitted
feat(openfeature): return a ready provider from the openfeature entrypoint
dd-trace/openfeature previously only existed as a side-effect require for file tracers (#9324). Turn it into the real public entrypoint: it now returns a usable FlaggingProvider instance after tracer.init(), and tracer.openfeature is deprecated in its favor since it doesn't work in bundled applications. Extract the base provider class into a factory so it can be constructed from either the bundler-opaque require-provider wrapper (legacy tracer.openfeature) or a plain require (the new entrypoint) without duplicating the class body. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 58f3a84 commit 7296d40

13 files changed

Lines changed: 211 additions & 134 deletions

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,23 +93,24 @@ Regardless of where you open the issue, someone at Datadog will try to help.
9393

9494
If you would like to trace your bundled application then please read this page on [bundling and dd-trace](https://docs.datadoghq.com/tracing/trace_collection/automatic_instrumentation/dd_libraries/nodejs/#bundling). It includes information on how to use our ESBuild plugin and includes caveats for other bundlers.
9595

96-
When using the experimental OpenFeature provider, file-traced deployments can force the optional provider and
97-
its dependencies into the output with a side-effect import before accessing `tracer.openfeature`:
96+
When using the experimental OpenFeature provider in a bundled application, require it through the
97+
`dd-trace/openfeature` subpath. This subpath uses a plain `require`/`import` for its dependencies, so
98+
bundlers can resolve and include them at build time. It must be loaded after `tracer.init()`:
9899

99100
CommonJS:
100101

101102
```js
102-
require('dd-trace/openfeature')
103+
const tracer = require('dd-trace').init()
104+
const openfeatureProvider = require('dd-trace/openfeature')
103105
```
104106

105-
ES modules:
107+
ES modules: initialize the tracer with the `--import` flag (see the [ESM support](#ecmascript-modules-esm-support)
108+
section above) so it's ready before your application code runs, then import the provider normally:
106109

107110
```js
108-
import 'dd-trace/openfeature.js'
111+
import openfeatureProvider from 'dd-trace/openfeature.js'
109112
```
110113

111-
This is a fallback for build tools that do not recognize the provider's optional-require wrapper.
112-
113114

114115
## Security Vulnerabilities
115116

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ interface Tracer extends opentracing.Tracer {
163163
* @env DD_FEATURE_FLAGS_ENABLED
164164
* @env DD_FEATURE_FLAGS_CONFIGURATION_SOURCE
165165
* @beta This feature is in preview and not ready for production use
166+
* @deprecated Use `require('dd-trace/openfeature')` instead, which also works in bundled applications.
166167
*/
167168
openfeature: tracer.OpenFeatureProvider;
168169

index.d.v5.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ interface Tracer extends opentracing.Tracer {
163163
* @env DD_FEATURE_FLAGS_ENABLED
164164
* @env DD_FEATURE_FLAGS_CONFIGURATION_SOURCE
165165
* @beta This feature is in preview and not ready for production use
166+
* @deprecated Use `require('dd-trace/openfeature')` instead, which also works in bundled applications.
166167
*/
167168
openfeature: tracer.OpenFeatureProvider;
168169

openfeature.d.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,11 @@
1-
export {};
1+
import tracer = require('.')
2+
3+
/**
4+
* OpenFeature provider that integrates with Datadog's feature flagging system.
5+
* Must be required after `tracer.init()`.
6+
*
7+
* @beta This feature is in preview and not ready for production use
8+
*/
9+
declare const provider: tracer.OpenFeatureProvider
10+
11+
export = provider

openfeature.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
11
'use strict'
22

3-
// Static fallback for file tracers that do not recognize the optional-peer wrapper.
4-
require('@datadog/openfeature-node-server')
3+
const { DatadogNodeServerProvider } = require('@datadog/openfeature-node-server')
4+
5+
const tracer = require('./packages/dd-trace')
6+
const createFlaggingProviderClass = require('./packages/dd-trace/src/openfeature/flagging_provider')
7+
8+
const config = tracer._tracer._config
9+
10+
if (!config) {
11+
throw new Error('dd-trace/openfeature must be required after tracer.init().')
12+
}
13+
14+
const FlaggingProvider = createFlaggingProviderClass(DatadogNodeServerProvider)
15+
16+
module.exports = new FlaggingProvider(tracer._tracer, config)

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

Lines changed: 53 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,57 +7,65 @@ const { EXPOSURE_CHANNEL } = require('./constants/constants')
77
const EvalMetricsHook = require('./eval-metrics-hook')
88
const SpanEnrichmentHook = require('./span-enrichment-hook')
99

10-
const { DatadogNodeServerProvider } = require('./require-provider')
11-
1210
/**
13-
* OpenFeature provider that integrates with Datadog's feature flagging system.
14-
* Extends DatadogNodeServerProvider to add tracer integration and configuration management.
11+
* Builds the OpenFeature provider that integrates with Datadog's feature flagging system.
12+
* Takes the base provider class as a parameter so callers can supply it either through the
13+
* bundler-opaque `require-provider` wrapper (legacy `tracer.openfeature`) or a plain `require`
14+
* (the `dd-trace/openfeature` entrypoint), without duplicating this class.
15+
*
16+
* @param {typeof import('@datadog/openfeature-node-server').DatadogNodeServerProvider} DatadogNodeServerProvider
17+
* @returns {typeof FlaggingProvider}
1518
*/
16-
class FlaggingProvider extends DatadogNodeServerProvider {
17-
/** @type {SpanEnrichmentHook | undefined} */
18-
#spanEnrichmentHook
19-
20-
/** @type {{ start: Function, stop: Function } | undefined} */
21-
#configurationSource
22-
19+
module.exports = function createFlaggingProviderClass (DatadogNodeServerProvider) {
2320
/**
24-
* @param {import('../tracer')} tracer - Datadog tracer instance
25-
* @param {import('../config/config-base')} config - Tracer configuration object
21+
* Extends DatadogNodeServerProvider to add tracer integration and configuration management.
2622
*/
27-
constructor (tracer, config) {
28-
super({
29-
exposureChannel: channel(EXPOSURE_CHANNEL),
30-
initializationTimeoutMs: config.experimental.flaggingProvider.initializationTimeoutMs,
31-
})
32-
33-
this.hooks.push(new EvalMetricsHook(config))
34-
35-
if (config.experimental.flaggingProvider.spanEnrichment?.enabled) {
36-
this.#spanEnrichmentHook = new SpanEnrichmentHook(tracer)
37-
// @ts-expect-error The upstream constructor always initializes its optional hooks property.
38-
this.hooks.push(this.#spanEnrichmentHook)
39-
log.info('%s span enrichment enabled', this.constructor.name)
40-
} else {
41-
log.info('%s span enrichment disabled', this.constructor.name)
42-
}
23+
class FlaggingProvider extends DatadogNodeServerProvider {
24+
/** @type {SpanEnrichmentHook | undefined} */
25+
#spanEnrichmentHook
4326

44-
log.debug('%s created with timeout: %dms', this.constructor.name,
45-
config.experimental.flaggingProvider.initializationTimeoutMs)
27+
/** @type {{ start: Function, stop: Function } | undefined} */
28+
#configurationSource
4629

47-
this.#configurationSource = configurationSource.create(config, this.setConfiguration.bind(this))
48-
this.#configurationSource?.start()
49-
}
30+
/**
31+
* @param {import('../tracer')} tracer - Datadog tracer instance
32+
* @param {import('../config/config-base')} config - Tracer configuration object
33+
*/
34+
constructor (tracer, config) {
35+
super({
36+
exposureChannel: channel(EXPOSURE_CHANNEL),
37+
initializationTimeoutMs: config.experimental.flaggingProvider.initializationTimeoutMs,
38+
})
5039

51-
/**
52-
* Called when the provider is shut down.
53-
* Cleans up resources including channel subscriptions.
54-
*/
55-
onClose () {
56-
this.#configurationSource?.stop()
57-
this.#configurationSource = undefined
58-
this.#spanEnrichmentHook?.destroy()
59-
this.#spanEnrichmentHook = undefined
40+
this.hooks.push(new EvalMetricsHook(config))
41+
42+
if (config.experimental.flaggingProvider.spanEnrichment?.enabled) {
43+
this.#spanEnrichmentHook = new SpanEnrichmentHook(tracer)
44+
// @ts-expect-error The upstream constructor always initializes its optional hooks property.
45+
this.hooks.push(this.#spanEnrichmentHook)
46+
log.info('%s span enrichment enabled', this.constructor.name)
47+
} else {
48+
log.info('%s span enrichment disabled', this.constructor.name)
49+
}
50+
51+
log.debug('%s created with timeout: %dms', this.constructor.name,
52+
config.experimental.flaggingProvider.initializationTimeoutMs)
53+
54+
this.#configurationSource = configurationSource.create(config, this.setConfiguration.bind(this))
55+
this.#configurationSource?.start()
56+
}
57+
58+
/**
59+
* Called when the provider is shut down.
60+
* Cleans up resources including channel subscriptions.
61+
*/
62+
onClose () {
63+
this.#configurationSource?.stop()
64+
this.#configurationSource = undefined
65+
this.#spanEnrichmentHook?.destroy()
66+
this.#spanEnrichmentHook = undefined
67+
}
6068
}
61-
}
6269

63-
module.exports = FlaggingProvider
70+
return FlaggingProvider
71+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ registerFeature({
1010
name: 'openfeature',
1111
noop,
1212
factory: () => require('./index'),
13-
provider: () => require('./flagging_provider'),
13+
provider: () => require('./flagging_provider')(require('./require-provider').DatadogNodeServerProvider),
1414

1515
/** @param {import('../config/config-base')} config */
1616
isEnabled (config) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const RemoteConfigCapabilities = require('../remote_config/capabilities')
66
* Configures remote config handlers for openfeature feature flagging
77
*
88
* @param {import('../remote_config')} rc - RemoteConfig instance
9-
* @param {() => import('./flagging_provider')} getOpenfeatureProxy
9+
* @param {() => InstanceType<ReturnType<typeof import('./flagging_provider')>>} getOpenfeatureProxy
1010
* @param {boolean} subscribe - Whether Agent Remote Config owns UFC delivery
1111
*/
1212
function enable (rc, getOpenfeatureProxy, subscribe) {

packages/dd-trace/test/openfeature/file-tracing.spec.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,34 @@ async function assertTracesProvider (entrypoint) {
3838

3939
describe('OpenFeature file tracing', () => {
4040
it('traces the provider dependency tree through the runtime wrapper', async () => {
41-
await assertTracesProvider(path.join(repoRoot, 'packages/dd-trace/src/openfeature/flagging_provider.js'))
41+
await assertTracesProvider(path.join(repoRoot, 'packages/dd-trace/src/openfeature/require-provider.js'))
4242
})
4343

4444
it('traces the provider dependency tree through the explicit entrypoint', async () => {
4545
await assertTracesProvider(path.join(repoRoot, 'openfeature.js'))
4646
})
4747

4848
it('loads the provider through the explicit entrypoint', () => {
49-
require(path.join(repoRoot, 'openfeature.js'))
49+
const tracerPath = JSON.stringify(path.join(repoRoot, 'packages/dd-trace'))
50+
const entrypointPath = JSON.stringify(path.join(repoRoot, 'openfeature.js'))
51+
const result = spawnSync(
52+
process.execPath,
53+
['--eval', `require(${tracerPath}).init({ plugins: false }); require(${entrypointPath})`],
54+
{ encoding: 'utf8' }
55+
)
56+
57+
assert.strictEqual(result.status, 0, result.stderr)
58+
})
59+
60+
it('throws a clear error when required before tracer.init()', () => {
61+
const result = spawnSync(
62+
process.execPath,
63+
['--eval', `require(${JSON.stringify(path.join(repoRoot, 'openfeature.js'))})`],
64+
{ encoding: 'utf8' }
65+
)
66+
67+
assert.notStrictEqual(result.status, 0)
68+
assert.match(result.stderr, /must be required after tracer\.init\(\)/)
5069
})
5170

5271
it('loads the explicit entrypoint as a CommonJS and ESM package subpath', () => {
@@ -58,14 +77,18 @@ describe('OpenFeature file tracing', () => {
5877
symlinkSync(repoRoot, path.join(nodeModulesPath, 'dd-trace'), 'junction')
5978
const commonJsResult = spawnSync(
6079
process.execPath,
61-
['--eval', "require('dd-trace/openfeature')"],
80+
['--eval', "require('dd-trace').init({ plugins: false }); require('dd-trace/openfeature')"],
6281
{ cwd: fixtureRoot, encoding: 'utf8' }
6382
)
6483
assert.strictEqual(commonJsResult.status, 0, commonJsResult.stderr)
6584

6685
const esmResult = spawnSync(
6786
process.execPath,
68-
['--input-type=module', '--eval', "import 'dd-trace/openfeature.js'"],
87+
[
88+
'--import', 'dd-trace/initialize.mjs',
89+
'--input-type=module',
90+
'--eval', "import 'dd-trace/openfeature.js'",
91+
],
6992
{ cwd: fixtureRoot, encoding: 'utf8' }
7093
)
7194
assert.strictEqual(esmResult.status, 0, esmResult.stderr)

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

Lines changed: 10 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use strict'
22

33
const assert = require('node:assert/strict')
4-
const fs = require('node:fs')
54

6-
const { describe, it, beforeEach, afterEach } = require('mocha')
5+
const { DatadogNodeServerProvider } = require('@datadog/openfeature-node-server')
6+
const { describe, it, beforeEach } = require('mocha')
77
const sinon = require('sinon')
88
const proxyquire = require('proxyquire')
99

@@ -68,7 +68,7 @@ describe('FlaggingProvider', () => {
6868
}
6969
mockSpanEnrichmentHookClass = sinon.stub().returns(mockSpanEnrichmentHook)
7070

71-
FlaggingProvider = proxyquire('../../src/openfeature/flagging_provider', {
71+
const createFlaggingProviderClass = proxyquire('../../src/openfeature/flagging_provider', {
7272
'dc-polyfill': {
7373
channel: channelStub,
7474
},
@@ -77,6 +77,7 @@ describe('FlaggingProvider', () => {
7777
'./eval-metrics-hook': mockEvalMetricsHookClass,
7878
'./span-enrichment-hook': mockSpanEnrichmentHookClass,
7979
})
80+
FlaggingProvider = createFlaggingProviderClass(DatadogNodeServerProvider)
8081
})
8182

8283
describe('constructor', () => {
@@ -208,82 +209,20 @@ describe('FlaggingProvider', () => {
208209

209210
describe('inheritance', () => {
210211
it('should extend DatadogNodeServerProvider', () => {
211-
const { DatadogNodeServerProvider } = require('@datadog/openfeature-node-server')
212212
const provider = new FlaggingProvider(mockTracer, mockConfig)
213213

214214
assert.ok(provider instanceof DatadogNodeServerProvider)
215215
})
216216
})
217217

218-
// Pins the optional-peer gate against leaking the provider chain into customer bundles (#8635).
219-
// `file-tracing.spec.js` covers the same wrapper's nft contract.
220-
describe('optional-peer gate', () => {
221-
const modulePath = require.resolve('../../src/openfeature/flagging_provider')
222-
const providerModulePath = require.resolve('../../src/openfeature/require-provider')
223-
const peer = '@datadog/openfeature-node-server'
224-
225-
afterEach(() => {
226-
delete require.cache[modulePath]
227-
delete require.cache[providerModulePath]
228-
delete globalThis.__webpack_require__
229-
delete globalThis.__non_webpack_require__
230-
})
231-
232-
it('uses `require` outside a bundler', () => {
233-
assert.strictEqual(typeof globalThis.__webpack_require__, 'undefined')
234-
delete require.cache[modulePath]
235-
delete require.cache[providerModulePath]
236-
237-
const ReloadedFlaggingProvider = require(modulePath)
238-
239-
assert.strictEqual(typeof ReloadedFlaggingProvider, 'function')
240-
assert.strictEqual(ReloadedFlaggingProvider.name, 'FlaggingProvider')
241-
})
218+
describe('factory', () => {
219+
it('builds a distinct class per call, extending the given base class', () => {
220+
const createFlaggingProviderClass = require('../../src/openfeature/flagging_provider')
242221

243-
it('uses `__non_webpack_require__`, never `__webpack_require__`, under webpack', () => {
244-
const loadCalls = []
245-
globalThis.__webpack_require__ = () => {
246-
throw new Error('webpack require must not run for an optional peer')
247-
}
248-
/** @param {string} request */
249-
globalThis.__non_webpack_require__ = (request) => {
250-
loadCalls.push(request)
251-
return require(request)
252-
}
253-
254-
delete require.cache[modulePath]
255-
delete require.cache[providerModulePath]
256-
const ReloadedFlaggingProvider = require(modulePath)
257-
258-
assert.deepStrictEqual(loadCalls, [peer])
259-
assert.strictEqual(typeof ReloadedFlaggingProvider, 'function')
260-
})
261-
262-
it('falls back to `require` when `__non_webpack_require__` is absent', () => {
263-
globalThis.__webpack_require__ = () => {
264-
throw new Error('webpack require must not run for an optional peer')
265-
}
266-
267-
delete require.cache[modulePath]
268-
delete require.cache[providerModulePath]
269-
const ReloadedFlaggingProvider = require(modulePath)
270-
271-
assert.strictEqual(typeof ReloadedFlaggingProvider, 'function')
272-
})
222+
const OtherFlaggingProvider = createFlaggingProviderClass(DatadogNodeServerProvider)
273223

274-
it('keeps the provider load opaque to bundlers', () => {
275-
const source = fs.readFileSync(providerModulePath, 'utf8')
276-
277-
assert.doesNotMatch(
278-
source,
279-
/require\(\s*['"]@datadog\/openfeature-node-server['"]\s*\)/,
280-
'a literal require would let bundlers resolve the optional peer chain at build time'
281-
)
282-
assert.doesNotMatch(
283-
source,
284-
/\brequire\(\s*[^'"\s]/,
285-
'a dynamic require would create a webpack expression dependency'
286-
)
224+
assert.notStrictEqual(OtherFlaggingProvider, FlaggingProvider)
225+
assert.strictEqual(OtherFlaggingProvider.name, 'FlaggingProvider')
287226
})
288227
})
289228
})

0 commit comments

Comments
 (0)