Skip to content

feat(openfeature): add agentless Feature Flagging configuration source#9397

Merged
BridgeAR merged 16 commits into
masterfrom
leo.romanovsky/ffl-2697-nodejs-agentless-configuration-source
Jul 21, 2026
Merged

feat(openfeature): add agentless Feature Flagging configuration source#9397
BridgeAR merged 16 commits into
masterfrom
leo.romanovsky/ffl-2697-nodejs-agentless-configuration-source

Conversation

@leoromanovsky

@leoromanovsky leoromanovsky commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

NOTE TO REVIEWERS

Review the current PR head from Files changed and leave feedback through a normal pull request review. The commit guide below is optional architecture context; reviewers are not expected to open or comment on individual commit pages.

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

Commit Layer and associated tests LOC (+/-)
7ffa2a3 Resolve delivery policy: source selection, managed/custom endpoints, staging and GovCloud routing, and bounded timing configuration. +321
7b0b67e Load snapshots: built-in fetch, authentication, tracing suppression, gzip, strict JSON:API, ETags, and last-known-good application. +421
b83caab Harden polling: fixed-delay scheduling, retries, independent timeouts, overlap prevention, credential safety, warnings, and shutdown. +449 / -12
ac9b55d Activate delivery: public configuration, provider lifecycle, explicit Remote Configuration opt-in, and packaged application integration. +459 / -7

What 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

  • Adds DD_FEATURE_FLAGS_CONFIGURATION_SOURCE with agentless as the default and remote_config as explicit opt-in.
  • Keeps offline startup bytes as future scope; offline is not currently accepted by the public or runtime configuration API.
  • Derives the managed URL as https://ufc-server.ff-cdn.<DD_SITE>/api/v2/feature-flagging/config/rules-based/server?dd_env=<DD_ENV>.
  • Treats DD_SITE=datad0g.com as the staging managed CDN site and regression-tests the exact derived URL.
  • Authenticates CDN requests with DD-API-KEY.
  • Omits the API key from cleartext non-local custom URLs while retaining controlled localhost and host.docker.internal test endpoints.
  • Uses the runtime's built-in fetch, advertises Accept-Encoding: gzip, and delegates successful-response decompression before JSON:API validation.
  • Decodes response bodies only for 200; non-success bodies are cancelled without text or JSON decoding.
  • Suppresses tracing around CDN polling so internal requests do not emit HTTP spans or inject trace-propagation headers.
  • Adds an optional custom agentless URL for controlled system-test, dogfood, and operator-managed endpoints.
  • Requires every managed or custom endpoint response to use JSON:API with data.type = universal-flag-configuration, then passes data.attributes to the existing evaluator.
  • Polls using ETags, If-None-Match, 304, bounded jittered retries, request timeouts, no overlap, last-known-good preservation, a one-hour interval cap, and shutdown cancellation.
  • Starts the Agent FFE_FLAGS Remote Configuration subscription only when remote_config is explicitly selected.
  • Derives the managed endpoint for every 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 with remote_config.

Staging uses the same managed contract. DD_SITE=datad0g.com derives https://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-configuration resource and UFC fields under data.attributes. A malformed response never replaces the last-known-good configuration.

Gzip is negotiated, not configurable. Agentless requests advertise Accept-Encoding: gzip. Built-in fetch decompresses 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, or host.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 fetch with no additional HTTP client dependency or runtime-version branch.

Explicit connection reuse is deferred. Built-in fetch owns 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 200 response advances the ETag only after parsing and local application succeed. An accepted 200 without 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/... from DD_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-Encoding header 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_SITE for every site rather than maintaining an SDK-side allowlist.

DD_SITE=ddog-gov.com
  -> https://ufc-server.ff-cdn.ddog-gov.com/api/v2/feature-flagging/config/rules-based/server?dd_env=<DD_ENV>

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]
Loading

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 timers
Loading

Network 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-in fetch decompresses a response declaring Content-Encoding: gzip before JSON:API validation; identity responses remain supported.

Only data.attributes is supplied to the existing UFC evaluator.

Customer Usage Examples

Default Datadog-managed CDN delivery:

export DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true
export DD_API_KEY=<datadog-api-key>
export DD_SITE=datadoghq.com
export DD_ENV=prod

# DD_FEATURE_FLAGS_CONFIGURATION_SOURCE=agentless is optional.
node app.js

Datadog staging CDN delivery:

export DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true
export DD_API_KEY=<staging-api-key>
export DD_SITE=datad0g.com
export DD_ENV=staging
node app.js

Application code for either managed site:

const tracer = require('dd-trace').init()
const { OpenFeature } = require('@openfeature/server-sdk')

await OpenFeature.setProviderAndWait(tracer.openfeature)

const client = OpenFeature.getClient()
const details = await client.getStringDetails(
  'checkout-flow',
  'control',
  {
    targetingKey: user.id,
    plan: user.plan
  }
)

Explicit Agent Remote Configuration delivery:

export DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true
export DD_FEATURE_FLAGS_CONFIGURATION_SOURCE=remote_config
export DD_REMOTE_CONFIGURATION_ENABLED=true
node app.js

Custom agentless endpoint:

export DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true
export DD_API_KEY=<backend-api-key>
export DD_ENV=prod
export DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL=https://flags.example.com/ufc

# Custom endpoints return the same JSON:API envelope as the managed CDN.
node app.js

GovCloud endpoint derivation (managed CDN availability may vary):

export DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true
export DD_API_KEY=<govcloud-api-key>
export DD_SITE=ddog-gov.com
export DD_ENV=prod

# The SDK attempts ufc-server.ff-cdn.ddog-gov.com.
# Until that CDN is provisioned, failures preserve defaults or last-known-good state.
node app.js

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:

PYTEST_XDIST_AUTO_NUM_WORKERS=4 TEST_LIBRARY=nodejs ./run.sh \
  PARAMETRIC tests/parametric/test_ffe/test_configuration_sources.py -q
dd-trace-js b99ca54:       15 passed in 85.28s (0:01:25)
Mock fixture self-test:    3 passed in 2.34s
OpenFeature unit suite:    217 passed
Packaged integration:      1 passed with built-in fetch

The suite covers source selection, managed dd_env URL validation, JSON:API delivery, authentication, gzip negotiation, ETags and 304, 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 adds Content-Length to the controlled HTTP/1.1 mock because Node's built-in fetch rejects an otherwise ambiguous terminated body.

Dogfooding Evidence

The real ffe-dogfooding Node app installed the packaged local tracer and evaluated against a controlled gzip JSON:API endpoint:

dd-trace=7.0.0-pre
PROVIDER_READY after_ms=19
value=true
reason=TARGETING_MATCH
transport=built-in fetch
payload=gzip JSON:API

The app installed the packaged local tracer, sent Accept-Encoding: gzip, authenticated the controlled endpoint, loaded the configuration, and evaluated locally with TARGETING_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_env mock contract is merged in DataDog/system-tests#7315. Standards-compliant response framing for fetch clients is tracked in draft DataDog/system-tests#7329.

@datadog-prod-us1-3

datadog-prod-us1-3 Bot commented Jul 15, 2026

Copy link
Copy Markdown

Tests

⚠️ Warnings

❄️ 2 New flaky tests detected

AgentlessConfigurationSource warns once per failure category from AgentlessConfigurationSource   View in Datadog
Possible EventTarget memory leak detected. 7 abort listeners added to [AbortSignal]. Use events.setMaxListeners() to increase limit

    at [kNewListener] (node:internal/event_target:534:17)
    at [kNewListener] (node:internal/abort_controller:239:24)
    at EventTarget.addEventListener (node:internal/event_target:645:23)
    at /home/runner/work/dd-trace-js/dd-trace-js/node_modules/@sinonjs/fake-timers/src/fake-timers-src.js:2899:52
    at new Promise (&lt;anonymous&gt;)
    at timersPromisesModule.setTimeout (node_modules/@sinonjs/fake-timers/src/fake-timers-src.js:2868:25)
    at wait (packages/dd-trace/src/openfeature/agentless_configuration_source.js:252:11)
    at #poll (packages/dd-trace/src/openfeature/agentless_configuration_source.js:96:15)

New test introduced in this PR is flaky.

OpenFeature configuration source contract covers all 63 stable/source/legacy combinations without tracing CDN polling from OpenFeature configuration source contract   View in Datadog
1 configuration source cases failed:
stable=absent, source=invalid, legacy=absent -&gt; agentless: stable=absent, source=invalid, legacy=absent -&gt; agentless
&#43; actual - expected

&#43; &#39;configuration-source-default&#39;
- &#39;configuration-source-loaded&#39;
                        ^

AggregateError: 1 configuration source cases failed:
stable=absent, source=invalid, legacy=absent -&gt; agentless: stable=absent, source=invalid, legacy=absent -&gt; agentless
...

New test introduced in this PR is flaky.

View in Flaky Test Management

ℹ️ Info

No other issues found (see more)

🧪 All tests passed

🔄 Datadog auto-retried 1 job - 1 passed on retry View in Datadog

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 98.43% (+0.01%)

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 3033836 | Docs | Datadog PR Page | Give us feedback!

@dd-octo-sts

dd-octo-sts Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Overall package size

Self size: 7.5 MB
Deduped: 8.16 MB
No deduping: 8.16 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

@pr-commenter

pr-commenter Bot commented Jul 15, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-07-21 21:55:55

Comparing candidate commit 3033836 in PR branch leo.romanovsky/ffl-2697-nodejs-agentless-configuration-source with baseline commit d35bc28 in branch master.

📊 Benchmarking dashboard

Found 0 performance improvements and 0 performance regressions! Performance is the same for 2318 metrics, 40 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:appsec-appsec-enabled-24

  • unstable execution_time [-206414.968µs; +206485.335µs] or [-7.714%; +7.717%]

scenario:appsec-appsec-enabled-26

  • unstable execution_time [-234.352ms; +232.249ms] or [-9.210%; +9.127%]

scenario:appsec-appsec-enabled-with-attacks-24

  • unstable execution_time [-167389.978µs; +168707.378µs] or [-5.402%; +5.444%]

scenario:appsec-appsec-enabled-with-attacks-26

  • unstable execution_time [-186.736ms; +190.386ms] or [-6.356%; +6.480%]

scenario:appsec-control-20

  • unstable execution_time [-121070.756µs; +122324.122µs] or [-7.303%; +7.378%]

scenario:appsec-control-24

  • unstable execution_time [-114943.664µs; +115815.497µs] or [-9.218%; +9.288%]

scenario:appsec-control-26

  • unstable execution_time [-127.831ms; +117.851ms] or [-10.312%; +9.507%]

scenario:appsec-iast-no-vulnerability-control-20

  • unstable execution_time [-15.049ms; +11.922ms] or [-5.763%; +4.566%]

scenario:appsec-iast-no-vulnerability-iast-enabled-always-active-20

  • unstable execution_time [-13.082ms; +18.378ms] or [-5.164%; +7.255%]

scenario:appsec-iast-startup-time-control-20

  • unstable execution_time [-16.099ms; +24.886ms] or [-3.976%; +6.146%]

scenario:debugger-line-probe-with-snapshot-default-20

  • unstable cpu_user_time [-2409.723ms; +4083.556ms] or [-15.698%; +26.602%]
  • unstable execution_time [-2354.367ms; +4069.183ms] or [-14.623%; +25.273%]
  • unstable throughput [-376.811op/s; +209.902op/s] or [-18.662%; +10.396%]

scenario:debugger-line-probe-with-snapshot-default-24

  • unstable cpu_user_time [-1760.323ms; +583.183ms] or [-21.158%; +7.010%]
  • unstable execution_time [-1834.960ms; +642.636ms] or [-20.314%; +7.114%]
  • unstable instructions [-15.0G instructions; +4.9G instructions] or [-22.072%; +7.167%]
  • unstable throughput [-176.985op/s; +474.816op/s] or [-4.842%; +12.990%]

scenario:debugger-line-probe-with-snapshot-default-26

  • unstable cpu_user_time [-5.494s; +2.369s] or [-46.356%; +19.993%]
  • unstable execution_time [-5.530s; +2.403s] or [-43.898%; +19.079%]
  • unstable instructions [-48.6G instructions; +21.2G instructions] or [-48.499%; +21.121%]
  • unstable max_rss_usage [-16.895MB; +7.505MB] or [-10.120%; +4.495%]
  • unstable throughput [-473.561op/s; +1081.789op/s] or [-17.047%; +38.942%]

scenario:debugger-line-probe-with-snapshot-minimal-24

  • unstable cpu_user_time [-2019.389ms; +3184.272ms] or [-24.326%; +38.359%]
  • unstable execution_time [-2129.756ms; +3311.829ms] or [-23.632%; +36.749%]
  • unstable instructions [-17.2G instructions; +27.2G instructions] or [-25.366%; +40.236%]
  • unstable max_rss_usage [-8.520MB; +12.735MB] or [-5.435%; +8.123%]
  • unstable throughput [-864.451op/s; +565.130op/s] or [-23.610%; +15.435%]

scenario:dogstatsd-with-tags-20

  • unstable cpu_user_time [-429.443ms; +190.007ms] or [-9.170%; +4.057%]
  • unstable execution_time [-422.206ms; +186.887ms] or [-8.864%; +3.924%]
  • unstable throughput [-66394.991op/s; +150470.779op/s] or [-3.770%; +8.544%]

scenario:plugin-aws-sdk-lambda-inject-with-context-24

  • unstable cpu_user_time [-123.317ms; +254.407ms] or [-3.290%; +6.788%]
  • unstable execution_time [-129.379ms; +261.088ms] or [-3.429%; +6.919%]

scenario:plugin-graphql-long-with-depth-and-collapse-off-20

  • unstable max_rss_usage [-25.843MB; +55.612MB] or [-6.745%; +14.514%]

scenario:plugin-graphql-long-with-depth-off-26

  • unstable max_rss_usage [-38.470MB; +20.955MB] or [-20.439%; +11.133%]

scenario:plugin-graphql-long-with-depth-on-max-20

  • unstable execution_time [-596.743ms; +582.387ms] or [-5.069%; +4.947%]
  • unstable throughput [-3.409op/s; +3.475op/s] or [-4.988%; +5.085%]

scenario:plugin-memcached-get-24

  • unstable execution_time [-117.280ms; +182.762ms] or [-4.869%; +7.588%]

scenario:plugin-mongodb-core-binary-hash-20

  • unstable execution_time [-131.575ms; +224.716ms] or [-4.305%; +7.352%]

scenario:plugin-pg-service-26

  • unstable execution_time [-110.425ms; +28.675ms] or [-12.031%; +3.124%]
  • unstable throughput [-131696.435op/s; +543640.747op/s] or [-1.965%; +8.110%]

@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.43%. Comparing base (d35bc28) to head (3033836).
⚠️ Report is 10 commits behind head on master.

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            
Flag Coverage Δ
aiguard 57.89% <84.02%> (+0.04%) ⬆️
aiguard-integration 56.29% <73.96%> (+0.03%) ⬆️
apm-bucket-0 57.74% <84.02%> (+0.04%) ⬆️
apm-bucket-1 64.06% <84.02%> (+0.02%) ⬆️
apm-bucket-2 62.82% <84.02%> (+0.03%) ⬆️
apm-bucket-3 60.17% <84.02%> (+0.03%) ⬆️
apm-capabilities-tracing 64.20% <100.00%> (+0.03%) ⬆️
apm-integrations-aerospike 56.75% <84.02%> (+0.04%) ⬆️
apm-integrations-confluentinc-kafka-javascript 61.70% <84.02%> (+0.03%) ⬆️
apm-integrations-couchbase 57.18% <84.02%> (+0.03%) ⬆️
apm-integrations-http 62.85% <84.02%> (+0.03%) ⬆️
apm-integrations-kafkajs 62.26% <84.02%> (+0.03%) ⬆️
apm-integrations-next 59.29% <84.02%> (+0.03%) ⬆️
apm-integrations-prisma 58.76% <84.02%> (+0.03%) ⬆️
appsec 73.04% <84.02%> (+0.06%) ⬆️
appsec-express_fastify_graphql 70.48% <84.02%> (+0.01%) ⬆️
appsec-integration 51.46% <74.55%> (+0.03%) ⬆️
appsec-kafka_ldapjs_lodash 64.06% <84.02%> (+0.02%) ⬆️
appsec-mongodb-core_mongoose_mysql 67.79% <84.02%> (+0.02%) ⬆️
appsec-next 57.59% <84.02%> (+0.03%) ⬆️
appsec-node-serialize_passport_postgres 67.46% <84.02%> (+0.02%) ⬆️
appsec-sourcing_stripe_template 65.80% <84.02%> (+0.02%) ⬆️
debugger 64.98% <84.02%> (+0.02%) ⬆️
instrumentations-bucket-0 51.69% <75.73%> (+0.02%) ⬆️
instrumentations-bucket-1 60.30% <84.02%> (+0.03%) ⬆️
instrumentations-bucket-10 61.87% <84.02%> (+0.03%) ⬆️
instrumentations-bucket-11 56.77% <84.02%> (+0.04%) ⬆️
instrumentations-bucket-12 52.06% <84.02%> (+0.04%) ⬆️
instrumentations-bucket-13 52.08% <84.02%> (+0.04%) ⬆️
instrumentations-bucket-2 53.39% <84.02%> (+0.04%) ⬆️
instrumentations-bucket-3 53.68% <84.02%> (+0.04%) ⬆️
instrumentations-bucket-4 59.30% <84.02%> (+0.03%) ⬆️
instrumentations-bucket-5 50.08% <84.02%> (+0.04%) ⬆️
instrumentations-bucket-6 61.02% <84.02%> (+0.03%) ⬆️
instrumentations-bucket-7 58.44% <84.02%> (+0.03%) ⬆️
instrumentations-bucket-8 59.53% <84.02%> (+0.04%) ⬆️
instrumentations-bucket-9 52.07% <75.73%> (+0.02%) ⬆️
instrumentations-instrumentation-couchbase 51.13% <75.73%> (+0.02%) ⬆️
instrumentations-instrumentation-zlib 51.72% <84.02%> (+0.05%) ⬆️
instrumentations-integration-esbuild 34.12% <25.32%> (-0.05%) ⬇️
llmobs-ai_anthropic_bedrock 62.65% <84.02%> (+0.02%) ⬆️
llmobs-bucket-1 61.61% <84.02%> (+0.02%) ⬆️
llmobs-openai 62.78% <84.02%> (+0.03%) ⬆️
llmobs-sdk 66.30% <84.02%> (+0.02%) ⬆️
llmobs-vertex-ai 59.27% <84.02%> (+0.03%) ⬆️
master-coverage 98.43% <100.00%> (?)
openfeature 56.26% <81.75%> (+2.25%) ⬆️
openfeature-unit 53.51% <97.55%> (+0.72%) ⬆️
platform-core_esbuild_instrumentations-misc 40.42% <75.73%> (+0.02%) ⬆️
platform-integration 61.56% <74.55%> (+0.04%) ⬆️
platform-shimmer_unit-guardrails_webpack 39.12% <75.73%> (+0.02%) ⬆️
plugins-bucket-0 57.14% <84.02%> (+0.03%) ⬆️
plugins-bucket-1 54.50% <68.63%> (+0.04%) ⬆️
plugins-bucket-11 62.36% <84.02%> (+0.03%) ⬆️
plugins-bucket-18 62.16% <84.02%> (+0.03%) ⬆️
plugins-bucket-19 60.17% <84.02%> (+0.02%) ⬆️
plugins-bucket-20 62.14% <84.02%> (+0.03%) ⬆️
plugins-bucket-4 58.68% <84.02%> (+0.03%) ⬆️
plugins-bullmq_cassandra_cookie 61.88% <84.02%> (+0.03%) ⬆️
plugins-cookie-parser_crypto_dd-trace-api 56.85% <84.02%> (+0.04%) ⬆️
plugins-fetch_fs_generic-pool 58.89% <84.02%> (-0.01%) ⬇️
plugins-google-cloud-pubsub_grpc_handlebars 64.83% <84.02%> (+0.02%) ⬆️
plugins-hapi_hono_ioredis 60.35% <84.02%> (+0.03%) ⬆️
plugins-jest_knex_langgraph 55.71% <84.02%> (+0.04%) ⬆️
plugins-ldapjs_light-my-request_limitd-client 58.62% <84.02%> (+0.03%) ⬆️
plugins-lodash_mariadb_memcached 58.12% <84.02%> (+0.04%) ⬆️
plugins-moleculer_mongodb_mongodb-core 62.04% <84.02%> (+0.03%) ⬆️
plugins-mongoose_multer_mysql 59.13% <84.02%> (+0.03%) ⬆️
plugins-mysql2_nats_node-serialize 60.84% <84.02%> (+0.03%) ⬆️
plugins-opensearch_passport-http_pino 59.66% <84.02%> (+0.03%) ⬆️
plugins-postgres_process_pug 58.39% <84.02%> (+0.04%) ⬆️
plugins-redis_router_sequelize 62.18% <84.02%> (+0.03%) ⬆️
plugins-test-and-upstream-rhea_undici_url 61.78% <84.02%> (+0.06%) ⬆️
plugins-valkey_vm_winston 58.12% <84.02%> (+0.03%) ⬆️
plugins-ws 59.71% <84.02%> (+0.03%) ⬆️
profiling 62.28% <83.43%> (+0.03%) ⬆️
serverless-aws-sdk-aws-sdk 55.16% <84.02%> (+0.03%) ⬆️
serverless-aws-sdk-bedrockruntime 54.86% <84.02%> (+0.04%) ⬆️
serverless-aws-sdk-client 56.53% <84.02%> (+0.04%) ⬆️
serverless-aws-sdk-dynamodb 55.76% <84.02%> (+0.03%) ⬆️
serverless-aws-sdk-eventbridge 49.46% <75.73%> (+0.02%) ⬆️
serverless-aws-sdk-kinesis 59.46% <84.02%> (+0.03%) ⬆️
serverless-aws-sdk-lambda 57.50% <84.02%> (+0.03%) ⬆️
serverless-aws-sdk-s3 55.71% <84.02%> (+0.03%) ⬆️
serverless-aws-sdk-serverless-peer-service 59.87% <84.02%> (+0.03%) ⬆️
serverless-aws-sdk-sns 60.26% <84.02%> (+0.03%) ⬆️
serverless-aws-sdk-sqs 60.69% <84.02%> (+0.03%) ⬆️
serverless-aws-sdk-stepfunctions 55.69% <84.02%> (+0.04%) ⬆️
serverless-aws-sdk-util 51.58% <75.73%> (+0.02%) ⬆️
serverless-bucket-0 54.40% <69.82%> (+0.04%) ⬆️
serverless-bucket-1 59.37% <84.02%> (+0.03%) ⬆️
test-optimization-cucumber 71.87% <82.46%> (+0.09%) ⬆️
test-optimization-cypress 65.87% <77.92%> (+0.10%) ⬆️
test-optimization-jest 73.30% <83.43%> (+<0.01%) ⬆️
test-optimization-mocha 73.49% <83.43%> (+0.06%) ⬆️
test-optimization-playwright-playwright-atr 60.42% <72.07%> (+0.01%) ⬆️
test-optimization-playwright-playwright-efd 60.66% <77.27%> (+0.08%) ⬆️
test-optimization-playwright-playwright-final-status 60.57% <72.07%> (+0.01%) ⬆️
test-optimization-playwright-playwright-impacted-tests 60.30% <72.07%> (+0.17%) ⬆️
test-optimization-playwright-playwright-reporting 61.59% <72.72%> (-0.10%) ⬇️
test-optimization-playwright-playwright-test-management 61.11% <72.07%> (-0.10%) ⬇️
test-optimization-playwright-playwright-test-span 60.34% <72.07%> (-0.04%) ⬇️
test-optimization-selenium 59.86% <72.07%> (-0.09%) ⬇️
test-optimization-testopt 58.49% <78.10%> (+0.11%) ⬆️
test-optimization-vitest 70.23% <76.62%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@leoromanovsky leoromanovsky changed the title Add agentless Feature Flagging configuration source feat(openfeature): add agentless Feature Flagging configuration source Jul 16, 2026
@leoromanovsky
leoromanovsky force-pushed the leo.romanovsky/ffl-2697-nodejs-agentless-configuration-source branch from 3f66acc to a363424 Compare July 16, 2026 12:33
@leoromanovsky
leoromanovsky force-pushed the leo.romanovsky/ffl-2697-nodejs-agentless-configuration-source branch 2 times, most recently from b99ca54 to ac9b55d Compare July 17, 2026 03:50
@leoromanovsky
leoromanovsky marked this pull request as ready for review July 17, 2026 04:06
@leoromanovsky
leoromanovsky requested review from a team as code owners July 17, 2026 04:06
@leoromanovsky
leoromanovsky requested review from BridgeAR, pavlokhrebto, rochdev and sameerank and removed request for a team July 17, 2026 04:06
@aarsilv
aarsilv requested a review from Copilot July 17, 2026 14:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (agentless default vs remote_config opt-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.

Comment thread packages/dd-trace/src/openfeature/configuration_source.js Outdated
sameerank
sameerank previously approved these changes Jul 17, 2026

@sameerank sameerank left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/dd-trace/src/openfeature/agentless_configuration_source.js
Comment thread packages/dd-trace/src/openfeature/configuration_source.js Outdated
Comment thread packages/dd-trace/src/openfeature/configuration_source.js

@BridgeAR BridgeAR left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just had a brief glimpse at the config

Comment thread packages/dd-trace/src/config/supported-configurations.json
Comment thread packages/dd-trace/src/config/supported-configurations.json Outdated
Comment on lines +14 to +19
function getClientLibraryHeaders (language = LANGUAGE, version = VERSION) {
return {
'DD-Client-Library-Language': language,
'DD-Client-Library-Version': version,
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 leoromanovsky left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}`)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Migrated feedback from @aarsilvoriginal 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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/dd-trace/src/openfeature/configuration_source.js Outdated
Comment thread packages/dd-trace/src/openfeature/configuration_source.js Outdated
Comment thread packages/dd-trace/test/openfeature/configuration_source.spec.js Outdated
Comment thread packages/dd-trace/src/openfeature/agentless_configuration_source.js Outdated
statusCode
)
} else if (statusCode) {
log.warn('Feature Flagging agentless endpoint returned HTTP %d after %d attempts', statusCode, MAX_ATTEMPTS)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Migrated feedback from @aarsilvoriginal commit comment

👍

Consider including the endpoint it was trying to hit too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave this open pending the resolution of Ruben's request to make the env var sensitive.

Comment thread packages/dd-trace/src/openfeature/agentless_configuration_source.js
Comment thread packages/dd-trace/src/openfeature/configuration_source.js Outdated
Comment thread packages/dd-trace/src/openfeature/flagging_provider.js
Comment thread packages/dd-trace/src/openfeature/flagging_provider.js Outdated
leoromanovsky and others added 15 commits July 21, 2026 23:42
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.
@BridgeAR
BridgeAR force-pushed the leo.romanovsky/ffl-2697-nodejs-agentless-configuration-source branch from 05df5bd to 3033836 Compare July 21, 2026 21:43

@aarsilv aarsilv left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving as I was on the pairing session ✅

@BridgeAR
BridgeAR merged commit 90a87d5 into master Jul 21, 2026
693 of 694 checks passed
@BridgeAR
BridgeAR deleted the leo.romanovsky/ffl-2697-nodejs-agentless-configuration-source branch July 21, 2026 22:03
dd-octo-sts Bot pushed a commit that referenced this pull request Jul 21, 2026
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.
This was referenced Jul 21, 2026
dd-octo-sts Bot pushed a commit that referenced this pull request Jul 21, 2026
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.
This was referenced Jul 22, 2026
leoromanovsky added a commit that referenced this pull request Jul 22, 2026
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.
leoromanovsky added a commit that referenced this pull request Jul 22, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants