feat(openfeature): add agentless Feature Flagging configuration source#9397
feat(openfeature): add agentless Feature Flagging configuration source#9397leoromanovsky wants to merge 10 commits into
Conversation
🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: 053684d | Docs | Datadog PR Page | Give us feedback! |
Overall package sizeSelf size: 6.79 MB Dependency sizes| name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.3.1 | 122.62 kB | 438.86 kB | | opentracing | 0.14.7 | 194.81 kB | 194.81 kB | | dc-polyfill | 0.1.11 | 25.74 kB | 25.74 kB |🤖 This report was automatically generated by heaviest-objects-in-the-universe |
BenchmarksBenchmark execution time: 2026-07-18 00:55:46 Comparing candidate commit 053684d in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 2319 metrics, 39 unstable metrics.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #9397 +/- ##
==========================================
+ Coverage 96.90% 98.35% +1.44%
==========================================
Files 923 927 +4
Lines 123011 123669 +658
Branches 21249 10629 -10620
==========================================
+ Hits 119209 121637 +2428
+ Misses 3802 2032 -1770 Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
3f66acc to
a363424
Compare
Define source selection, managed and custom endpoint derivation, staging and GovCloud routing, and bounded timing semantics. Keep this policy layer independent from transport and provider activation, with focused resolver tests.
Fetch one authenticated UFC snapshot with the runtime transport, suppress tracer instrumentation, validate the JSON:API resource, and only advance last-known-good state after successful application. Cover gzip, ETags, malformed payloads, and custom endpoints alongside the loader.
Turn snapshot loading into a fixed-delay lifecycle with bounded retries, independent timeouts, overlap prevention, rate-limited warnings, TLS-aware credential handling, and idempotent shutdown. Exercise timing, failure, and cancellation boundaries with fake timers.
bb4469e to
b99ca54
Compare
Expose the configuration surface, attach the selected source to the provider lifecycle, and keep Agent Remote Configuration behind explicit opt-in. Pair the activation path with fleet-wide config, provider, shutdown, and packaged-application tests.
b99ca54 to
ac9b55d
Compare
There was a problem hiding this comment.
Pull request overview
Adds an agentless (CDN) Universal Flag Configuration delivery path for the existing OpenFeature flagging provider in dd-trace, making agentless the default configuration source while keeping Agent Remote Config as an explicit opt-in.
Changes:
- Introduces a configuration-source resolver (
agentlessdefault vsremote_configopt-in) and wires it into OpenFeature feature registration/lifecycle. - Implements an agentless poller using built-in
fetch(ETags/304, gzip, retries, timeouts, no overlap, shutdown) and attaches it to the provider. - Adds config/env-var plumbing + TypeScript type updates and broad unit/integration test coverage for both delivery modes.
Reviewed changes
Copilot reviewed 16 out of 18 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/dd-trace/test/openfeature/remote_config.spec.js | Adds coverage for capability advertisement without handler registration when agentless is selected. |
| packages/dd-trace/test/openfeature/register.spec.js | Verifies configuration-source enablement is wired into OpenFeature registration and RC subscription is conditional. |
| packages/dd-trace/test/openfeature/flagging_provider.spec.js | Adds provider lifecycle tests for attaching/stopping a configuration source and duplicate attachment behavior. |
| packages/dd-trace/test/openfeature/configuration_source.spec.js | New unit tests for source selection normalization, endpoint derivation, GovCloud routing, URL validation, and timing bounds. |
| packages/dd-trace/test/openfeature/agentless_configuration_source.spec.js | New unit tests for the agentless poller (gzip, JSON:API validation, ETags, retries/timeouts, no overlap, shutdown). |
| packages/dd-trace/test/config/index.spec.js | Verifies default config values and new env var parsing for Feature Flagging configuration source. |
| packages/dd-trace/src/openfeature/remote_config.js | Adds a subscribe switch so RC delivery is only installed when explicitly selected. |
| packages/dd-trace/src/openfeature/register.js | Wires configuration-source enablement into OpenFeature startup and passes source selection into RC integration. |
| packages/dd-trace/src/openfeature/flagging_provider.js | Adds configuration-source lifecycle attachment and shutdown handling to the provider. |
| packages/dd-trace/src/openfeature/configuration_source.js | New resolver/enable module for selecting and starting the appropriate configuration delivery source. |
| packages/dd-trace/src/openfeature/agentless_configuration_source.js | New agentless polling implementation using built-in fetch with strict JSON:API parsing and robust polling controls. |
| packages/dd-trace/src/config/supported-configurations.json | Adds new env vars + defaults for selecting/configuring agentless FF config delivery. |
| packages/dd-trace/src/config/generated-config-types.d.ts | Updates generated config typings for the new env vars and flaggingProvider fields. |
| integration-tests/openfeature/openfeature-exposure-events.spec.js | Pins integration tests to remote_config source to preserve existing exposure path behavior. |
| integration-tests/openfeature/openfeature-agentless.spec.js | New integration test validating agentless delivery and local evaluation against a controlled endpoint. |
| integration-tests/openfeature/app/agentless-evaluation.js | Sandbox app used by the new agentless integration test. |
| index.d.v5.ts | Documents and types new flaggingProvider options for v5 surface (agentless/remote_config + agentless tuning). |
| index.d.ts | Documents and types new flaggingProvider options for current surface (agentless/remote_config + agentless tuning). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
sameerank
left a comment
There was a problem hiding this comment.
Nothing blocking from me and looks aligned overall with our design docs. The questions about signing and request parameters can be worked on later if needed
BridgeAR
left a comment
There was a problem hiding this comment.
Just had a brief glimpse at the config
| "default": "agentless" | ||
| } | ||
| ], | ||
| "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL": [ |
There was a problem hiding this comment.
I believe that endpoint transports keys, so we have to mark this as sensitive, otherwise it transports telemetry
There was a problem hiding this comment.
This particular configuration only creates a base URL; I dug into it and am not sure yet, as it does not explicitly contain headers or URL parameters. Is that still something you consider sensitive?
| function getClientLibraryHeaders (language = LANGUAGE, version = VERSION) { | ||
| return { | ||
| 'DD-Client-Library-Language': language, | ||
| 'DD-Client-Library-Version': version, | ||
| } | ||
| } |
There was a problem hiding this comment.
I checked the other tracer implementations: Go, Java, Python, Ruby, and .NET telemetry all use DD-Client-Library-Language and DD-Client-Library-Version.
HTTP field names are case-insensitive per RFC 9110, so the previous lowercase JS spelling was functionally equivalent.
Still, matching the established cross-SDK spelling lets us use one canonical helper for both telemetry and the new agentless UFC request.
I’ve updated the implementation accordingly and covered both request paths with tests. Datadog-Meta-* remains the trace-submission header family; the UFC request uses DD-Client-Library-*.
NOTE TO REVIEWERS
Commit Guide
7ffa2a37b0b67eb83caabac9b55dWhat does this PR do?
Adds a no-Agent Feature Flagging configuration path to
dd-trace-js: load Universal Flag Configuration from the Datadog UFC CDN and evaluate flags locally through the existing OpenFeature provider.Motivation
Node.js server and serverless applications need to use Feature Flagging without depending on a local Datadog Agent. Agentless CDN delivery becomes the default source, while Agent Remote Configuration remains available through explicit opt-in.
Changes
DD_FEATURE_FLAGS_CONFIGURATION_SOURCEwithagentlessas the default andremote_configas explicit opt-in.offlineis not currently accepted by the public or runtime configuration API.https://ufc-server.ff-cdn.<DD_SITE>/api/v2/feature-flagging/config/rules-based/server?dd_env=<DD_ENV>.DD_SITE=datad0g.comas the staging managed CDN site and regression-tests the exact derived URL.DD-API-KEY.host.docker.internaltest endpoints.fetch, advertisesAccept-Encoding: gzip, and delegates successful-response decompression before JSON:API validation.200; non-success bodies are cancelled without text or JSON decoding.data.type = universal-flag-configuration, then passesdata.attributesto the existing evaluator.If-None-Match,304, bounded jittered retries, request timeouts, no overlap, last-known-good preservation, a one-hour interval cap, and shutdown cancellation.FFE_FLAGSRemote Configuration subscription only whenremote_configis explicitly selected.DD_SITE, including GovCloud, without hard-coding current CDN availability into the SDK.Decisions
Agentless is the default. An unset, empty, whitespace-only, or case-variant source value resolves to
agentless. Agent Remote Configuration must be selected withremote_config.Staging uses the same managed contract.
DD_SITE=datad0g.comderiveshttps://ufc-server.ff-cdn.datad0g.com/api/v2/feature-flagging/config/rules-based/server?dd_env=<DD_ENV>; staging is not hidden behind a custom endpoint.Network delivery is strict JSON:API. Every managed or custom endpoint response must contain a
universal-flag-configurationresource and UFC fields underdata.attributes. A malformed response never replaces the last-known-good configuration.Gzip is negotiated, not configurable. Agentless requests advertise
Accept-Encoding: gzip. Built-infetchdecompresses successful gzip responses before JSON:API validation, while identity responses remain supported. Invalid gzip fails the poll without replacing the last-known-good configuration or ETag.Custom URLs only override the endpoint. A configured origin receives the standard rules-based path; a configured URL with a non-root path is used as the exact endpoint. All network endpoints use the same JSON:API contract; raw UFC is not accepted over the network.
API keys require TLS outside controlled local endpoints. HTTPS endpoints receive
DD-API-KEY. Cleartext HTTP endpoints receive it only for literal loopback hosts,localhost, orhost.docker.internal; other HTTP URLs are allowed for compatibility but the key is omitted and an error is logged.There is no SDK payload-size cap. Valid managed JSON:API payloads larger than 500 KB are accepted.
Transport stays simple. The poller uses the runtime's built-in
fetchwith no additional HTTP client dependency or runtime-version branch.Explicit connection reuse is deferred. Built-in
fetchowns connection behavior for this PR. A follow-up can add source-owned connection recycling once its lifecycle and system-test contract are designed.Polling is bounded. The default interval remains 30 seconds. Customer values above one hour are clamped to one hour with a warning.
Timeout completion does not depend on fetch abort behavior. The request timer marks the attempt failed and retryable before signalling
AbortController; a late response cannot apply configuration after the timeout has settled.ETags describe accepted configurations. A
200response advances the ETag only after parsing and local application succeed. An accepted200without a non-blank ETag clears the previous ETag.GovCloud remains SDK-compatible. Node follows Java and derives
https://ufc-server.ff-cdn.ddog-gov.com/...fromDD_SITE=ddog-gov.com. Current managed-CDN availability is a backend concern, not an SDK rejection; ordinary request failures preserve caller defaults or last-known-good configuration. An explicitly configured custom endpoint remains available.No custom headers. Agentless requests expose no custom-header configuration surface. The fixed standard
Accept-Encodingheader is internal and not customer-configurable.No emission changes. This PR only loads UFC and evaluates locally. It does not add or modify exposure emission, aggregate-evaluation emission, span enrichment, or evaluation metrics.
Offline is future scope, not an API value. Startup-provided UFC bytes will be added only when its lifecycle and system-test contract are ready.
GovCloud Compatibility
Node intentionally follows the Java implementation: managed delivery derives the UFC CDN hostname from
DD_SITEfor every site rather than maintaining an SDK-side allowlist.This is an SDK compatibility decision, not a claim that the managed GovCloud CDN is provisioned today. If the hostname is unavailable, normal polling-failure behavior keeps evaluations on caller defaults or last-known-good configuration. Once the CDN is enabled, GovCloud can begin working without another Node SDK release. Custom endpoints remain supported in the meantime.
Delivery Architecture
flowchart TD Gate{Flagging provider enabled?} Select{Configuration source} Gate -- no --> Disabled[Provider not started] Gate -- yes --> Select Select -- unset or agentless --> URL{Custom URL?} URL -- no --> CDN[Datadog UFC CDN<br/>strict JSON:API] URL -- yes --> Custom[Custom endpoint<br/>strict JSON:API] CDN --> Poller[Authenticated fetch poller<br/>gzip, ETag, retry, timeout, LKG] Custom --> Poller Poller --> UFC[Existing UFC pipeline] Select -- remote_config --> Agent[Datadog Agent RC<br/>FFE_FLAGS] Agent --> UFC FutureOffline[Future: offline startup bytes<br/>not currently selectable] FutureOffline -. later .-> UFC style FutureOffline stroke-dasharray: 5 5 FutureReuse[Future: explicit connection reuse<br/>not included in this PR] FutureReuse -. later .-> Poller style FutureReuse stroke-dasharray: 5 5 UFC --> Evaluator[OpenFeature local evaluator] Evaluator --> Result[Application result]Poll Lifecycle
sequenceDiagram participant App participant Source as Node agentless source participant CDN as UFC endpoint participant Evaluator as Local evaluator App->>Source: start Source->>CDN: GET .../server?dd_env=<env> Note over Source,CDN: DD-API-KEY and gzip with tracing suppressed CDN-->>Source: 200 JSON:API or gzip JSON:API plus optional ETag Source->>Evaluator: setConfiguration(data.attributes) Evaluator-->>App: local flag result loop fixed delay after completion Source->>CDN: GET with accepted If-None-Match alt unchanged CDN-->>Source: 304 else changed CDN-->>Source: 200 + configuration Source->>Evaluator: apply accepted configuration else retryable failure CDN-->>Source: timeout, 429, or 5xx Source->>CDN: bounded jittered retries end end App->>Source: close Source-->>Source: cancel request and timersNetwork JSON:API Contract
{ "data": { "id": "1", "type": "universal-flag-configuration", "attributes": { "createdAt": "2026-07-15T19:57:07.219869778Z", "environment": { "name": "Staging" }, "flags": {} } } }Requests advertise
Accept-Encoding: gzip. Built-infetchdecompresses a response declaringContent-Encoding: gzipbefore JSON:API validation; identity responses remain supported.Only
data.attributesis supplied to the existing UFC evaluator.Customer Usage Examples
Default Datadog-managed CDN delivery:
Datadog staging CDN delivery:
Application code for either managed site:
Explicit Agent Remote Configuration delivery:
Custom agentless endpoint:
GovCloud endpoint derivation (managed CDN availability may vary):
System Test Evidence
Using the merged DataDog/system-tests#7315 contract, the fetch-compatible framing follow-up in DataDog/system-tests#7329, and the Node.js manifest locally enabled:
The suite covers source selection, managed
dd_envURL validation, JSON:API delivery, authentication, gzip negotiation, ETags and304, malformed/cold/warm recovery, timeout and retry behavior, last-known-good preservation, and non-overlapping polls. Repo-native tests separately prove canonical resolved-site routing and the one-hour customer polling cap. The framing follow-up addsContent-Lengthto the controlled HTTP/1.1 mock because Node's built-infetchrejects an otherwise ambiguous terminated body.Dogfooding Evidence
The real
ffe-dogfoodingNode app installed the packaged local tracer and evaluated against a controlled gzip JSON:API endpoint:The app installed the packaged local tracer, sent
Accept-Encoding: gzip, authenticated the controlled endpoint, loaded the configuration, and evaluated locally withTARGETING_MATCH. A direct live-staging attempt from the local Docker network timed out while connecting to the CDN, so this dogfood run proves the packaged application path but does not claim live-CDN reachability from that machine.Additional Notes
The original JSON:API and
dd_envmock contract is merged in DataDog/system-tests#7315. Standards-compliant response framing for fetch clients is tracked in draft DataDog/system-tests#7329.