feat(openfeature): add agentless Feature Flagging configuration source#9397
Conversation
|
Overall package sizeSelf size: 7.5 MB Dependency sizes| name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.3.2 | 124.41 kB | 440.65 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-21 21:55:55 Comparing candidate commit 3033836 in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 2318 metrics, 40 unstable metrics.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #9397 +/- ##
========================================
Coverage 98.42% 98.43%
========================================
Files 939 942 +3
Lines 126551 127027 +476
Branches 10710 10663 -47
========================================
+ Hits 124561 125037 +476
Misses 1990 1990
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
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
| 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-*.
leoromanovsky
left a comment
There was a problem hiding this comment.
Migrated commit-review feedback from @aarsilv
Authorship note: I am reposting Aaron's 20 comments from the four commit pages so they are visible and trackable on the main PR. GitHub records this review as authored by me; the quoted feedback and linked originals are Aaron's. No interpretation or response has been added.
Fourteen substantive comments are attached inline to the current head. The remaining six are preserved here because their target code was later removed or because they were praise-only:
Target code later removed
- Original: "Does this repo not believe in whitespace lines to separate things?"
- Original: "Does this include domain? We should be explicit either way."
Praise and reactions
| const value = config.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE | ||
| const mode = String(value ?? '').trim().toLowerCase() || CONFIGURATION_SOURCE_AGENTLESS | ||
| if (mode !== CONFIGURATION_SOURCE_AGENTLESS && mode !== CONFIGURATION_SOURCE_REMOTE_CONFIG) { | ||
| throw new Error(`Unsupported Feature Flagging configuration source: ${mode}`) |
There was a problem hiding this comment.
Migrated feedback from @aarsilv — original commit comment
I wonder if we should log an error but then fall back
CONFIGURATION_SOURCE_AGENTLESS, same default behavior as if it were not provided (vs. crashing)
There was a problem hiding this comment.
Sarah, in the Java review, raised a similar concern about typos here. I’d like the policy to remain simple and deterministic rather than trying to infer customer intent:
- Unset or empty value -> default to agentless
- agentless or remote_config -> use the selected source
- Any other non-empty value -> should we log an error and fall back to agentless, or log an error and leave the configuration source disabled?
The current implementation does the latter; it does not stop init. You seem to be implying the former.
| statusCode | ||
| ) | ||
| } else if (statusCode) { | ||
| log.warn('Feature Flagging agentless endpoint returned HTTP %d after %d attempts', statusCode, MAX_ATTEMPTS) |
There was a problem hiding this comment.
Migrated feedback from @aarsilv — original commit comment
👍
Consider including the endpoint it was trying to hit too
There was a problem hiding this comment.
I'll leave this open pending the resolution of Ruben's request to make the env var sensitive.
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.
Feature flag configuration can now be delivered from the agentless endpoint while keeping startup lazy and polling cancellable. The source retains the last known-good response when polling fails and avoids assembling unrelated JSON fields during streaming responses.
Agentless UFC snapshots are expected to be small, so buffering them through the shared request transport removes duplicate gzip and streaming parser machinery while preserving fixed-delay retries, cancellation, and last-known-good updates. Malformed diagnostics redact string contents before telemetry logging. Native parsing reduced 10 KB / 100 KB / 1 MB snapshots from 1.47 ms / 14.72 ms / 185.26 ms to 21.8 µs / 242 µs / 2.85 ms on Node 24.18.0 (V8 13.6), across seven trials after a one-second warmup with extremes dropped.
Partial HTTP responses could leave polling pending because the shared transport settled only on end. Every terminal response event now settles once, and mixed-case gzip encodings remain supported. Cleartext non-loopback overrides could apply unauthenticated flag data after the transport stripped the API key. They are now rejected before a request, and diagnostics exclude sensitive endpoint and payload data. Feature Flags now activate independently from APM tracing, so Remote Config cannot target the noop provider. Inactive processes still avoid loading the provider, source, and agentless header helper.
Windows could exhaust ten immediate evaluations before the provider applied the agentless response, while the request test double retained abort listeners after completion. Await provider readiness and clean up settled listeners so the tests observe the production lifecycle. Increase the default request timeout from two to five seconds so slow CDN responses can complete before retrying.
05df5bd to
3033836
Compare
aarsilv
left a comment
There was a problem hiding this comment.
Approving as I was on the pairing session ✅
Feature flag configuration can now be delivered from the agentless endpoint while keeping startup lazy and polling cancellable. The source retains the last known-good response when polling fails and avoids assembling unrelated JSON fields during streaming responses.
Feature flag configuration can now be delivered from the agentless endpoint while keeping startup lazy and polling cancellable. The source retains the last known-good response when polling fails and avoids assembling unrelated JSON fields during streaming responses.
Feature flag configuration can now be delivered from the agentless endpoint while keeping startup lazy and polling cancellable. The source retains the last known-good response when polling fails and avoids assembling unrelated JSON fields during streaming responses.
Feature flag configuration can now be delivered from the agentless endpoint while keeping startup lazy and polling cancellable. The source retains the last known-good response when polling fails and avoids assembling unrelated JSON fields during streaming responses.
NOTE TO REVIEWERS
Commit comments are anchored to historical SHAs and do not become tracked PR review threads. In this PR, @aarsilv followed the previous instruction and left 20 comments across the four layer commits, but none appeared as trackable threads in the main PR review.
For a concrete example, Aaron asked whether the new base URL option included a domain. A later commit removed that programmatic option entirely, while the comment remained attached to obsolete code on the earlier commit. The full review is now mirrored against the current PR head.
The four layer commits still pair code with the tests for that layer and can be used to understand the implementation sequence. The review target is the combined current diff.
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.