Skip to content

feat(ffe): evaluate dependent flags with prerequisite telemetry - #2748

Open
hhan2024 wants to merge 7 commits into
mainfrom
helen/ffe-dependent-flags-evaluator
Open

hhan2024 wants to merge 7 commits into
mainfrom
helen/ffe-dependent-flags-evaluator

Conversation

@hhan2024

@hhan2024 hhan2024 commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

This PR is the shared rust engine equivalent of the core evaluation work in DataDog/openfeature-js-client#414 and implements the same main behavior:

Evaluate flagEvaluation conditions by comparing prerequisite variant keys with ONE_OF / NOT_ONE_OF, preserving authored order and short-circuiting. A mismatch rejects the rule; a reached missing, disabled, malformed, unassigned, cyclic, or over-depth prerequisite fails the root evaluation and preserves the underlying cause.

Dependency conditions accept empty string arrays and require a key-only flagEvaluation reference with no attribute selector. Flags without prerequisite conditions retain an allocation-only fast path; validation and assignment construction are shared with the dependency path.

Each invocation shares its configuration, context, timestamp, and experimentation opt-out policy. Memoized results retain dependency height, including rejected branches, so reuse cannot bypass evaluatorParams.maxDependencyDepth (default 10; explicit 0–255). The raw depth setting is retained at ingestion and validated only when a dependency condition executes. Invalid values, including explicit null, fail that evaluation with ConfigurationParseError; independent flags and skipped dependency branches remain usable.

Motivation

SDKs need prerequisite evaluation and the information required to report it correctly before advertising dependent-flag support. The new eval_flag_with_trace API returns unique prerequisite results, including errors and prerequisites rejected by the depth guard, and marks only the final successful rule/split path as eligible for exposures. SDKs still apply do_log, privacy, and deduplication policies; existing evaluation APIs remain available.

Additional notes

Stacked on #2745 for the native RC capability enum. Neither PR automatically opts consumers in. Python integration must consume the new API and explicitly activate capability reporting; a native library release remains separate from this PR.

Validation

  • 137 unit tests and 10 documentation tests passed with Python, exposure, EVP, and evaluation-metrics features enabled.
  • Covers matching/nonmatching variant keys and empty comparison lists, mixed and malformed conditions, per-flag error isolation, missing prerequisites, exact depth boundaries through 255, cycles, depth-sensitive memoization, context/time/opt-out propagation, and rejected-rule/split/root-error exposure suppression.
  • Regression cases cover invalid/null limits with independent roots, short-circuited conditions, inactive allocations, earlier winning rules/allocations, and experimentation opt-out; depth-error traces retain completion order without duplicate or root records.
  • Stable clippy for the evaluator with all targets and warnings denied, nightly formatting, structural lint, and diff checks passed.
  • Added standalone runtime benchmarks for independent flags, one prerequisite, and repeated memoized references. Python benchmarking identified fixed overhead for dependency-free flags; those now use an allocation-only fast path. Python integration #21346 tracks the release-wheel comparison.

hhan2024 and others added 2 commits October 28, 2026 15:21
Add an opt-in capability for consumers that implement dependent flag evaluation, and verify its wire encoding alongside the existing rules capability.

Environment: Datadog workspace

Co-Authored-By: Codex GPT-6 <noreply@localhost>
Evaluate variant-key dependencies in authored order with depth-safe memoization and preserve successful-path exposure eligibility for SDKs.

Environment: Datadog workspace

Co-Authored-By: Codex GPT-6 <noreply@localhost>
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

📚 Documentation Check Results

⚠️ 362 documentation warning(s) found

📦 libdd-ffe-test-suite - ✅ No warnings

📦 libdd-ffe - 362 warning(s)


Updated: 2026-11-02 14:22:38 UTC | Commit: e61ac5f | missing-docs job results

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

🔒 Cargo Deny Results

⚠️ 13 issue(s) found, showing only errors (advisories, bans, sources)

📦 libdd-ffe-test-suite - 6 error(s)

Show output
info: syncing channel updates for 1.91.1-x86_64-unknown-linux-gnu
info: latest update on 2025-11-10 for version 1.91.1 (ed61e7d7e 2025-11-07)
info: downloading 6 components
warn: the missing active toolchain `1.91.1-x86_64-unknown-linux-gnu` has been auto-installed
warn: this might cause rustup commands to take longer time to finish than expected
info: you may opt out with `RUSTUP_AUTO_INSTALL=0` or `rustup set auto-install disable`
error[unmaintained]: Bincode is unmaintained
   ┌─ /home/runner/work/libdatadog/libdatadog/Cargo.lock:24:1
   │
24 │ bincode 1.3.3 registry+https://github.com/rust-lang/crates.io-index
   │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ unmaintained advisory detected
   │
   ├ ID: RUSTSEC-2025-0141
   ├ Advisory: https://rustsec.org/advisories/RUSTSEC-2025-0141
   ├ Due to a doxxing and harassment incident, the bincode team has taken the decision to cease development permanently.
     
     The team considers version 1.3.3 a complete version of bincode that is not in need of any updates.
     
     ## Alternatives to consider
     
     * [wincode](https://crates.io/crates/wincode)
     * [postcard](https://crates.io/crates/postcard)
     * [bitcode](https://crates.io/crates/bitcode)
     * [rkyv](https://crates.io/crates/rkyv)
   ├ Announcement: https://git.sr.ht/~stygianentity/bincode/tree/v3.0/item/README.md
   ├ Solution: No safe upgrade is available!
   ├ bincode v1.3.3
     └── (dev) libdd-ffe v2.0.0
         └── (dev) libdd-ffe-test-suite v47.0.0

error[vulnerability]: TLS 1.3 handshake messages incorrectly accepted across encryption level boundaries
    ┌─ /home/runner/work/libdatadog/libdatadog/Cargo.lock:196:1
    │
196 │ rustls 0.23.37 registry+https://github.com/rust-lang/crates.io-index
    │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ security vulnerability detected
    │
    ├ ID: RUSTSEC-2026-0285
    ├ Advisory: https://rustsec.org/advisories/RUSTSEC-2026-0285
    ├ Rustls accepted TLS 1.3 handshake messages sent at the wrong encryption level
      when they followed a key-changing message in the same record. For example,
      a plaintext `EncryptedExtensions` message packed into the same record as the
      `ServerHello` was accepted.
      
      RFC 8446 section 5.1 requires that handshake messages do not span key changes,
      and that implementations terminate the connection with an "unexpected_message"
      alert if they do.
      
      The handshake transcript is still authenticated, so a network-position attacker
      cannot use this to alter or complete a handshake; the practical effect is that
      a peer could send handshake messages that should be encrypted in plaintext
      without rustls rejecting the connection.
      
      This is functionally the same bug as Go's
      [GO-2026-4340](https://pkg.go.dev/vuln/GO-2026-4340) (CVE-2025-61730).
    ├ Announcement: https://github.com/rustls/rustls/pull/3265
    ├ Solution: Upgrade to >=0.23.45 (try `cargo update -p rustls`)
    ├ rustls v0.23.37
      ├── hyper-rustls v0.27.7
      │   └── libdd-common v9.0.0
      │       ├── libdd-capabilities-impl v8.0.0
      │       │   ├── (dev) libdd-ffe v2.0.0
      │       │   │   └── (dev) libdd-ffe-test-suite v47.0.0
      │       │   └── libdd-remote-config v8.0.0
      │       │       ├── libdd-ffe v2.0.0 (*)
      │       │       └── (dev) libdd-remote-config v8.0.0 (*)
      │       ├── libdd-ffe v2.0.0 (*)
      │       └── libdd-remote-config v8.0.0 (*)
      ├── libdd-common v9.0.0 (*)
      ├── rustls-platform-verifier v0.6.2
      │   └── libdd-common v9.0.0 (*)
      └── tokio-rustls v0.26.0
          ├── hyper-rustls v0.27.7 (*)
          └── (dev) libdd-common v9.0.0 (*)

error[vulnerability]: `Debug` impls for client session values expose TLS secrets
    ┌─ /home/runner/work/libdatadog/libdatadog/Cargo.lock:196:1
    │
196 │ rustls 0.23.37 registry+https://github.com/rust-lang/crates.io-index
    │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ security vulnerability detected
    │
    ├ ID: RUSTSEC-2026-0346
    ├ Advisory: https://rustsec.org/advisories/RUSTSEC-2026-0346
    ├ The `Debug` implementations of `Tls12ClientSessionValue` and
      `Tls13ClientSessionValue` were derived, and so included the TLS 1.2 master
      secret or TLS 1.3 resumption PSK in their output.
      
      Custom `ClientSessionStore` implementations that format these values with
      `Debug` (for example, when logging) could disclose these secrets to readers of
      the logs. This conflicts with rustls's
      [key-material redaction policy](https://github.com/rustls/rustls/blob/99f2358cae2954837dbb866faf6727de75489ab9/SECURITY.md#boundary-public-api).
      
      Ordinary connection and built-in session cache `Debug` output is redacted and
      is not affected.
    ├ Announcement: https://github.com/rustls/rustls/pull/3299
    ├ Solution: Upgrade to >=0.23.46 (try `cargo update -p rustls`)
    ├ rustls v0.23.37
      ├── hyper-rustls v0.27.7
      │   └── libdd-common v9.0.0
      │       ├── libdd-capabilities-impl v8.0.0
      │       │   ├── (dev) libdd-ffe v2.0.0
      │       │   │   └── (dev) libdd-ffe-test-suite v47.0.0
      │       │   └── libdd-remote-config v8.0.0
      │       │       ├── libdd-ffe v2.0.0 (*)
      │       │       └── (dev) libdd-remote-config v8.0.0 (*)
      │       ├── libdd-ffe v2.0.0 (*)
      │       └── libdd-remote-config v8.0.0 (*)
      ├── libdd-common v9.0.0 (*)
      ├── rustls-platform-verifier v0.6.2
      │   └── libdd-common v9.0.0 (*)
      └── tokio-rustls v0.26.0
          ├── hyper-rustls v0.27.7 (*)
          └── (dev) libdd-common v9.0.0 (*)

error[vulnerability]: Brotli certificate decompression can allocate up to 1 GiB before authentication
    ┌─ /home/runner/work/libdatadog/libdatadog/Cargo.lock:196:1
    │
196 │ rustls 0.23.37 registry+https://github.com/rust-lang/crates.io-index
    │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ security vulnerability detected
    │
    ├ ID: RUSTSEC-2026-0348
    ├ Advisory: https://rustsec.org/advisories/RUSTSEC-2026-0348
    ├ The built-in Brotli certificate decompressor (enabled by the non-default
      `brotli` crate feature) bounded the size of the decompressed output, but
      accepted a peer-selected history window. Brotli's large-window extension was
      enabled, so a five-byte header could cause an allocation of 1 GiB. A peer can
      abort a process whose allocator cannot satisfy that request.
      
      No trusted certificate is required by the attacker. Affected configurations are:
      
      - clients that enable the `brotli` feature and connect to a malicious server, and
      - servers that enable the `brotli` feature and request client certificates.
      
      Applications that do not enable the `brotli` feature are not affected.
    ├ Announcement: https://github.com/rustls/rustls/pull/3297
    ├ Solution: Upgrade to >=0.23.46 (try `cargo update -p rustls`)
    ├ rustls v0.23.37
      ├── hyper-rustls v0.27.7
      │   └── libdd-common v9.0.0
      │       ├── libdd-capabilities-impl v8.0.0
      │       │   ├── (dev) libdd-ffe v2.0.0
      │       │   │   └── (dev) libdd-ffe-test-suite v47.0.0
      │       │   └── libdd-remote-config v8.0.0
      │       │       ├── libdd-ffe v2.0.0 (*)
      │       │       └── (dev) libdd-remote-config v8.0.0 (*)
      │       ├── libdd-ffe v2.0.0 (*)
      │       └── libdd-remote-config v8.0.0 (*)
      ├── libdd-common v9.0.0 (*)
      ├── rustls-platform-verifier v0.6.2
      │   └── libdd-common v9.0.0 (*)
      └── tokio-rustls v0.26.0
          ├── hyper-rustls v0.27.7 (*)
          └── (dev) libdd-common v9.0.0 (*)

error[vulnerability]: Unauthenticated ServerHello can downgrade later connections to classical key exchange
    ┌─ /home/runner/work/libdatadog/libdatadog/Cargo.lock:196:1
    │
196 │ rustls 0.23.37 registry+https://github.com/rust-lang/crates.io-index
    │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ security vulnerability detected
    │
    ├ ID: RUSTSEC-2026-0349
    ├ Advisory: https://rustsec.org/advisories/RUSTSEC-2026-0349
    ├ The client recorded the server-selected key exchange group for each server
      name when processing the ServerHello, before the server was authenticated.
      This record is used to predict which key share to send in future ClientHellos
      to that server name.
      
      A network attacker can answer one connection attempt with a ServerHello
      selecting X25519, and then close the connection. Later ClientHellos for that
      name carry only an X25519 key share, while `supported_groups` still lists
      X25519MLKEM768 first. If the genuine server accepts that share rather than
      sending a HelloRetryRequest for its preferred hybrid group, the handshake
      completes with classical-only key exchange and no error. This persists for as
      long as the entry stays in the client's session store.
      
      The impact is loss of post-quantum confidentiality for affected connections
      ("harvest now, decrypt later"). There is no key compromise and no
      authentication bypass.
      
      The following are not affected:
      
      - connections to servers which send a HelloRetryRequest for their preferred group,
      - clients with resumption disabled (as nothing is stored), and
      - other server names.
      
      This behaviour is discussed in
      [draft-ietf-tls-key-share-prediction](https://www.ietf.org/archive/id/draft-ietf-tls-key-share-prediction-01.html#name-security-considerations),
      so the practical impact depends largely on server policy.
    ├ Announcement: https://github.com/rustls/rustls/pull/3355
    ├ Solution: Upgrade to >=0.23.46 (try `cargo update -p rustls`)
    ├ rustls v0.23.37
      ├── hyper-rustls v0.27.7
      │   └── libdd-common v9.0.0
      │       ├── libdd-capabilities-impl v8.0.0
      │       │   ├── (dev) libdd-ffe v2.0.0
      │       │   │   └── (dev) libdd-ffe-test-suite v47.0.0
      │       │   └── libdd-remote-config v8.0.0
      │       │       ├── libdd-ffe v2.0.0 (*)
      │       │       └── (dev) libdd-remote-config v8.0.0 (*)
      │       ├── libdd-ffe v2.0.0 (*)
      │       └── libdd-remote-config v8.0.0 (*)
      ├── libdd-common v9.0.0 (*)
      ├── rustls-platform-verifier v0.6.2
      │   └── libdd-common v9.0.0 (*)
      └── tokio-rustls v0.26.0
          ├── hyper-rustls v0.27.7 (*)
          └── (dev) libdd-common v9.0.0 (*)

error[unsound]: `AsyncFd::new` and related constructors violate I/O safety
    ┌─ /home/runner/work/libdatadog/libdatadog/Cargo.lock:248:1
    │
248 │ tokio 1.49.0 registry+https://github.com/rust-lang/crates.io-index
    │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ unsound advisory detected
    │
    ├ ID: RUSTSEC-2026-0386
    ├ Advisory: https://rustsec.org/advisories/RUSTSEC-2026-0386
    ├ `tokio::io::unix::AsyncFd` provides safe constructors (`new`, `with_interest`, `try_new`, and `try_with_interest`) that accept any `T: AsRawFd` rather than requiring proof of ownership or validity of the file descriptor. Because `RawFd` implements `AsRawFd`, safe code can construct an `AsyncFd` from an arbitrary or closed file descriptor, or close/replace the underlying file descriptor while the `AsyncFd` is still alive.
      
      This violates I/O safety in two ways:
      
      1. `AsyncFd<T>` implements `AsFd` for all `T: AsRawFd` by calling `BorrowedFd::borrow_raw(self.as_raw_fd())`, allowing safe code to obtain an arbitrary `BorrowedFd<'_>` without owning the file descriptor.
      2. Tokio's I/O driver stores a pointer to an internal allocation in the `epoll`/`kqueue` user data for registered file descriptors. If the underlying file descriptor is duplicated and then closed while an `AsyncFd` is alive, and the OS reuses that file descriptor number for a another IO resource on the same runtime, dropping the first `AsyncFd` deregisters the reused file descriptor number and frees the first while the original file description remains registered in `epoll`, leading to a use-after-free when a readiness event fires.
      
      Starting in Tokio 1.51.6, 1.53.3, and 1.54.0, `AsyncFd::new`, `AsyncFd::with_interest`, `AsyncFd::try_new`, and `AsyncFd::try_with_interest` are deprecated in favor of the `unsafe` constructors `AsyncFd::register` and `AsyncFd::register_with_interest`. Callers must ensure that the file descriptor remains valid, open, and unchanged for the lifetime of the `AsyncFd`.
    ├ Announcement: https://github.com/tokio-rs/tokio/pull/8587
    ├ Solution: Upgrade to >=1.51.6, <1.52.0 OR >=1.53.3 (try `cargo update -p tokio`)
    ├ tokio v1.49.0
      ├── h2 v0.4.19
      │   └── hyper v1.11.1
      │       ├── httpmock v0.8.0-alpha.1
      │       │   └── (dev) libdd-ffe v2.0.0
      │       │       └── (dev) libdd-ffe-test-suite v47.0.0
      │       ├── hyper-rustls v0.27.7
      │       │   └── libdd-common v9.0.0
      │       │       ├── libdd-capabilities-impl v8.0.0
      │       │       │   ├── (dev) libdd-ffe v2.0.0 (*)
      │       │       │   └── libdd-remote-config v8.0.0
      │       │       │       ├── libdd-ffe v2.0.0 (*)
      │       │       │       └── (dev) libdd-remote-config v8.0.0 (*)
      │       │       ├── libdd-ffe v2.0.0 (*)
      │       │       └── libdd-remote-config v8.0.0 (*)
      │       ├── hyper-util v0.1.17
      │       │   ├── httpmock v0.8.0-alpha.1 (*)
      │       │   ├── hyper-rustls v0.27.7 (*)
      │       │   ├── libdd-common v9.0.0 (*)
      │       │   └── libdd-remote-config v8.0.0 (*)
      │       ├── libdd-common v9.0.0 (*)
      │       └── libdd-remote-config v8.0.0 (*)
      ├── httpmock v0.8.0-alpha.1 (*)
      ├── hyper v1.11.1 (*)
      ├── hyper-rustls v0.27.7 (*)
      ├── hyper-util v0.1.17 (*)
      ├── (dev) libdd-capabilities-impl v8.0.0 (*)
      ├── (dev) libdd-common v9.0.0 (*)
      ├── libdd-ffe v2.0.0 (*)
      ├── libdd-remote-config v8.0.0 (*)
      ├── (dev) libdd-trace-protobuf v5.1.1
      │   └── libdd-remote-config v8.0.0 (*)
      ├── tokio-rustls v0.26.0
      │   ├── hyper-rustls v0.27.7 (*)
      │   └── (dev) libdd-common v9.0.0 (*)
      ├── tokio-util v0.7.12
      │   ├── h2 v0.4.19 (*)
      │   └── libdd-remote-config v8.0.0 (*)
      └── (dev) tools v47.0.0
          └── (dev) libdd-common v9.0.0 (*)

advisories FAILED, bans ok, sources ok

📦 libdd-ffe - 7 error(s)

Show output
error[unmaintained]: Bincode is unmaintained
   ┌─ /home/runner/work/libdatadog/libdatadog/Cargo.lock:22:1
   │
22 │ bincode 1.3.3 registry+https://github.com/rust-lang/crates.io-index
   │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ unmaintained advisory detected
   │
   ├ ID: RUSTSEC-2025-0141
   ├ Advisory: https://rustsec.org/advisories/RUSTSEC-2025-0141
   ├ Due to a doxxing and harassment incident, the bincode team has taken the decision to cease development permanently.
     
     The team considers version 1.3.3 a complete version of bincode that is not in need of any updates.
     
     ## Alternatives to consider
     
     * [wincode](https://crates.io/crates/wincode)
     * [postcard](https://crates.io/crates/postcard)
     * [bitcode](https://crates.io/crates/bitcode)
     * [rkyv](https://crates.io/crates/rkyv)
   ├ Announcement: https://git.sr.ht/~stygianentity/bincode/tree/v3.0/item/README.md
   ├ Solution: No safe upgrade is available!
   ├ bincode v1.3.3
     └── (dev) libdd-ffe v2.0.0

error[unsound]: Potential use-after-free due to lack of panic safety in `LruCache::pop()`
    ┌─ /home/runner/work/libdatadog/libdatadog/Cargo.lock:128:1
    │
128 │ lru 0.16.4 registry+https://github.com/rust-lang/crates.io-index
    │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ unsound advisory detected
    │
    ├ ID: RUSTSEC-2026-0253
    ├ Advisory: https://rustsec.org/advisories/RUSTSEC-2026-0253
    ├ `LruCache::pop()` in `lru` was not panic-safe. If the `Drop` implementation of a stored key panics during `pop()`, `self.detach()` is never called, leaving dangling pointers in the internal doubly-linked list.
      
      A subsequent cache operation that triggers eviction can then dereference these dangling pointers:
      - The node is freed from the map, but remains linked in the LRU list due to the skipped `detach()` call
      - When a new insertion causes eviction, the LRU traversal encounters the dangling pointer
      - This results in a write to already-freed memory during the eviction process
      
      ## Impact
      
      - **CWE-416 (Use-After-Free):** memory corruption when subsequent cache operations access freed node pointers in the linked list
      - **CWE-415 (Double Free):** potential heap corruption when the same memory is freed multiple times
      
      Both types of undefined behavior can be invoked in safe Rust, but only if unwinding panics are enabled and `std::panic::catch_unwind` is used with key types that have potentially-panicking `Drop` implementations.
      
      ## Fix
      
      Fixed in `lru` 0.18.2 by detaching the node from the linked list before freeing it and dropping the key ([lru-rs#238](https://github.com/jeromefroe/lru-rs/pull/238)).
    ├ Announcement: https://github.com/jeromefroe/lru-rs/pull/238
    ├ Solution: Upgrade to >=0.18.2 (try `cargo update -p lru`)
    ├ lru v0.16.4
      └── libdd-ffe v2.0.0

error[vulnerability]: TLS 1.3 handshake messages incorrectly accepted across encryption level boundaries
    ┌─ /home/runner/work/libdatadog/libdatadog/Cargo.lock:172:1
    │
172 │ rustls 0.23.37 registry+https://github.com/rust-lang/crates.io-index
    │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ security vulnerability detected
    │
    ├ ID: RUSTSEC-2026-0285
    ├ Advisory: https://rustsec.org/advisories/RUSTSEC-2026-0285
    ├ Rustls accepted TLS 1.3 handshake messages sent at the wrong encryption level
      when they followed a key-changing message in the same record. For example,
      a plaintext `EncryptedExtensions` message packed into the same record as the
      `ServerHello` was accepted.
      
      RFC 8446 section 5.1 requires that handshake messages do not span key changes,
      and that implementations terminate the connection with an "unexpected_message"
      alert if they do.
      
      The handshake transcript is still authenticated, so a network-position attacker
      cannot use this to alter or complete a handshake; the practical effect is that
      a peer could send handshake messages that should be encrypted in plaintext
      without rustls rejecting the connection.
      
      This is functionally the same bug as Go's
      [GO-2026-4340](https://pkg.go.dev/vuln/GO-2026-4340) (CVE-2025-61730).
    ├ Announcement: https://github.com/rustls/rustls/pull/3265
    ├ Solution: Upgrade to >=0.23.45 (try `cargo update -p rustls`)
    ├ rustls v0.23.37
      ├── hyper-rustls v0.27.7
      │   └── libdd-common v9.0.0
      │       ├── libdd-capabilities-impl v8.0.0
      │       │   ├── (dev) libdd-ffe v2.0.0
      │       │   └── libdd-remote-config v8.0.0
      │       │       ├── libdd-ffe v2.0.0 (*)
      │       │       └── (dev) libdd-remote-config v8.0.0 (*)
      │       ├── libdd-ffe v2.0.0 (*)
      │       └── libdd-remote-config v8.0.0 (*)
      ├── libdd-common v9.0.0 (*)
      ├── rustls-platform-verifier v0.6.2
      │   └── libdd-common v9.0.0 (*)
      └── tokio-rustls v0.26.0
          ├── hyper-rustls v0.27.7 (*)
          └── (dev) libdd-common v9.0.0 (*)

error[vulnerability]: `Debug` impls for client session values expose TLS secrets
    ┌─ /home/runner/work/libdatadog/libdatadog/Cargo.lock:172:1
    │
172 │ rustls 0.23.37 registry+https://github.com/rust-lang/crates.io-index
    │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ security vulnerability detected
    │
    ├ ID: RUSTSEC-2026-0346
    ├ Advisory: https://rustsec.org/advisories/RUSTSEC-2026-0346
    ├ The `Debug` implementations of `Tls12ClientSessionValue` and
      `Tls13ClientSessionValue` were derived, and so included the TLS 1.2 master
      secret or TLS 1.3 resumption PSK in their output.
      
      Custom `ClientSessionStore` implementations that format these values with
      `Debug` (for example, when logging) could disclose these secrets to readers of
      the logs. This conflicts with rustls's
      [key-material redaction policy](https://github.com/rustls/rustls/blob/99f2358cae2954837dbb866faf6727de75489ab9/SECURITY.md#boundary-public-api).
      
      Ordinary connection and built-in session cache `Debug` output is redacted and
      is not affected.
    ├ Announcement: https://github.com/rustls/rustls/pull/3299
    ├ Solution: Upgrade to >=0.23.46 (try `cargo update -p rustls`)
    ├ rustls v0.23.37
      ├── hyper-rustls v0.27.7
      │   └── libdd-common v9.0.0
      │       ├── libdd-capabilities-impl v8.0.0
      │       │   ├── (dev) libdd-ffe v2.0.0
      │       │   └── libdd-remote-config v8.0.0
      │       │       ├── libdd-ffe v2.0.0 (*)
      │       │       └── (dev) libdd-remote-config v8.0.0 (*)
      │       ├── libdd-ffe v2.0.0 (*)
      │       └── libdd-remote-config v8.0.0 (*)
      ├── libdd-common v9.0.0 (*)
      ├── rustls-platform-verifier v0.6.2
      │   └── libdd-common v9.0.0 (*)
      └── tokio-rustls v0.26.0
          ├── hyper-rustls v0.27.7 (*)
          └── (dev) libdd-common v9.0.0 (*)

error[vulnerability]: Brotli certificate decompression can allocate up to 1 GiB before authentication
    ┌─ /home/runner/work/libdatadog/libdatadog/Cargo.lock:172:1
    │
172 │ rustls 0.23.37 registry+https://github.com/rust-lang/crates.io-index
    │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ security vulnerability detected
    │
    ├ ID: RUSTSEC-2026-0348
    ├ Advisory: https://rustsec.org/advisories/RUSTSEC-2026-0348
    ├ The built-in Brotli certificate decompressor (enabled by the non-default
      `brotli` crate feature) bounded the size of the decompressed output, but
      accepted a peer-selected history window. Brotli's large-window extension was
      enabled, so a five-byte header could cause an allocation of 1 GiB. A peer can
      abort a process whose allocator cannot satisfy that request.
      
      No trusted certificate is required by the attacker. Affected configurations are:
      
      - clients that enable the `brotli` feature and connect to a malicious server, and
      - servers that enable the `brotli` feature and request client certificates.
      
      Applications that do not enable the `brotli` feature are not affected.
    ├ Announcement: https://github.com/rustls/rustls/pull/3297
    ├ Solution: Upgrade to >=0.23.46 (try `cargo update -p rustls`)
    ├ rustls v0.23.37
      ├── hyper-rustls v0.27.7
      │   └── libdd-common v9.0.0
      │       ├── libdd-capabilities-impl v8.0.0
      │       │   ├── (dev) libdd-ffe v2.0.0
      │       │   └── libdd-remote-config v8.0.0
      │       │       ├── libdd-ffe v2.0.0 (*)
      │       │       └── (dev) libdd-remote-config v8.0.0 (*)
      │       ├── libdd-ffe v2.0.0 (*)
      │       └── libdd-remote-config v8.0.0 (*)
      ├── libdd-common v9.0.0 (*)
      ├── rustls-platform-verifier v0.6.2
      │   └── libdd-common v9.0.0 (*)
      └── tokio-rustls v0.26.0
          ├── hyper-rustls v0.27.7 (*)
          └── (dev) libdd-common v9.0.0 (*)

error[vulnerability]: Unauthenticated ServerHello can downgrade later connections to classical key exchange
    ┌─ /home/runner/work/libdatadog/libdatadog/Cargo.lock:172:1
    │
172 │ rustls 0.23.37 registry+https://github.com/rust-lang/crates.io-index
    │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ security vulnerability detected
    │
    ├ ID: RUSTSEC-2026-0349
    ├ Advisory: https://rustsec.org/advisories/RUSTSEC-2026-0349
    ├ The client recorded the server-selected key exchange group for each server
      name when processing the ServerHello, before the server was authenticated.
      This record is used to predict which key share to send in future ClientHellos
      to that server name.
      
      A network attacker can answer one connection attempt with a ServerHello
      selecting X25519, and then close the connection. Later ClientHellos for that
      name carry only an X25519 key share, while `supported_groups` still lists
      X25519MLKEM768 first. If the genuine server accepts that share rather than
      sending a HelloRetryRequest for its preferred hybrid group, the handshake
      completes with classical-only key exchange and no error. This persists for as
      long as the entry stays in the client's session store.
      
      The impact is loss of post-quantum confidentiality for affected connections
      ("harvest now, decrypt later"). There is no key compromise and no
      authentication bypass.
      
      The following are not affected:
      
      - connections to servers which send a HelloRetryRequest for their preferred group,
      - clients with resumption disabled (as nothing is stored), and
      - other server names.
      
      This behaviour is discussed in
      [draft-ietf-tls-key-share-prediction](https://www.ietf.org/archive/id/draft-ietf-tls-key-share-prediction-01.html#name-security-considerations),
      so the practical impact depends largely on server policy.
    ├ Announcement: https://github.com/rustls/rustls/pull/3355
    ├ Solution: Upgrade to >=0.23.46 (try `cargo update -p rustls`)
    ├ rustls v0.23.37
      ├── hyper-rustls v0.27.7
      │   └── libdd-common v9.0.0
      │       ├── libdd-capabilities-impl v8.0.0
      │       │   ├── (dev) libdd-ffe v2.0.0
      │       │   └── libdd-remote-config v8.0.0
      │       │       ├── libdd-ffe v2.0.0 (*)
      │       │       └── (dev) libdd-remote-config v8.0.0 (*)
      │       ├── libdd-ffe v2.0.0 (*)
      │       └── libdd-remote-config v8.0.0 (*)
      ├── libdd-common v9.0.0 (*)
      ├── rustls-platform-verifier v0.6.2
      │   └── libdd-common v9.0.0 (*)
      └── tokio-rustls v0.26.0
          ├── hyper-rustls v0.27.7 (*)
          └── (dev) libdd-common v9.0.0 (*)

error[unsound]: `AsyncFd::new` and related constructors violate I/O safety
    ┌─ /home/runner/work/libdatadog/libdatadog/Cargo.lock:222:1
    │
222 │ tokio 1.49.0 registry+https://github.com/rust-lang/crates.io-index
    │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ unsound advisory detected
    │
    ├ ID: RUSTSEC-2026-0386
    ├ Advisory: https://rustsec.org/advisories/RUSTSEC-2026-0386
    ├ `tokio::io::unix::AsyncFd` provides safe constructors (`new`, `with_interest`, `try_new`, and `try_with_interest`) that accept any `T: AsRawFd` rather than requiring proof of ownership or validity of the file descriptor. Because `RawFd` implements `AsRawFd`, safe code can construct an `AsyncFd` from an arbitrary or closed file descriptor, or close/replace the underlying file descriptor while the `AsyncFd` is still alive.
      
      This violates I/O safety in two ways:
      
      1. `AsyncFd<T>` implements `AsFd` for all `T: AsRawFd` by calling `BorrowedFd::borrow_raw(self.as_raw_fd())`, allowing safe code to obtain an arbitrary `BorrowedFd<'_>` without owning the file descriptor.
      2. Tokio's I/O driver stores a pointer to an internal allocation in the `epoll`/`kqueue` user data for registered file descriptors. If the underlying file descriptor is duplicated and then closed while an `AsyncFd` is alive, and the OS reuses that file descriptor number for a another IO resource on the same runtime, dropping the first `AsyncFd` deregisters the reused file descriptor number and frees the first while the original file description remains registered in `epoll`, leading to a use-after-free when a readiness event fires.
      
      Starting in Tokio 1.51.6, 1.53.3, and 1.54.0, `AsyncFd::new`, `AsyncFd::with_interest`, `AsyncFd::try_new`, and `AsyncFd::try_with_interest` are deprecated in favor of the `unsafe` constructors `AsyncFd::register` and `AsyncFd::register_with_interest`. Callers must ensure that the file descriptor remains valid, open, and unchanged for the lifetime of the `AsyncFd`.
    ├ Announcement: https://github.com/tokio-rs/tokio/pull/8587
    ├ Solution: Upgrade to >=1.51.6, <1.52.0 OR >=1.53.3 (try `cargo update -p tokio`)
    ├ tokio v1.49.0
      ├── h2 v0.4.19
      │   └── hyper v1.11.1
      │       ├── httpmock v0.8.0-alpha.1
      │       │   └── (dev) libdd-ffe v2.0.0
      │       ├── hyper-rustls v0.27.7
      │       │   └── libdd-common v9.0.0
      │       │       ├── libdd-capabilities-impl v8.0.0
      │       │       │   ├── (dev) libdd-ffe v2.0.0 (*)
      │       │       │   └── libdd-remote-config v8.0.0
      │       │       │       ├── libdd-ffe v2.0.0 (*)
      │       │       │       └── (dev) libdd-remote-config v8.0.0 (*)
      │       │       ├── libdd-ffe v2.0.0 (*)
      │       │       └── libdd-remote-config v8.0.0 (*)
      │       ├── hyper-util v0.1.17
      │       │   ├── httpmock v0.8.0-alpha.1 (*)
      │       │   ├── hyper-rustls v0.27.7 (*)
      │       │   ├── libdd-common v9.0.0 (*)
      │       │   └── libdd-remote-config v8.0.0 (*)
      │       ├── libdd-common v9.0.0 (*)
      │       └── libdd-remote-config v8.0.0 (*)
      ├── httpmock v0.8.0-alpha.1 (*)
      ├── hyper v1.11.1 (*)
      ├── hyper-rustls v0.27.7 (*)
      ├── hyper-util v0.1.17 (*)
      ├── (dev) libdd-capabilities-impl v8.0.0 (*)
      ├── (dev) libdd-common v9.0.0 (*)
      ├── libdd-ffe v2.0.0 (*)
      ├── libdd-remote-config v8.0.0 (*)
      ├── (dev) libdd-trace-protobuf v5.1.1
      │   ├── libdd-ffe v2.0.0 (*)
      │   └── libdd-remote-config v8.0.0 (*)
      ├── tokio-rustls v0.26.0
      │   ├── hyper-rustls v0.27.7 (*)
      │   └── (dev) libdd-common v9.0.0 (*)
      ├── tokio-util v0.7.12
      │   ├── h2 v0.4.19 (*)
      │   └── libdd-remote-config v8.0.0 (*)
      └── (dev) tools v47.0.0
          └── (dev) libdd-common v9.0.0 (*)

advisories FAILED, bans ok, sources ok

Updated: 2026-11-02 14:24:17 UTC | Commit: 6438bf2 | dependency-check job results

@datadog-datadog-us1-prod

datadog-datadog-us1-prod Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Pipelines

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 983b4b0 | Docs | Give us feedback!

@pr-commenter

pr-commenter Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Benchmarks

Comparison

Benchmark execution time: 2026-11-03 20:11:26

Comparing candidate commit 983b4b0 in PR branch helen/ffe-dependent-flags-evaluator with baseline commit 899c4cc in branch main.

📊 Benchmarking dashboard

Found 0 performance improvements and 6 performance regressions! Performance is the same for 8 metrics, 0 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 ----------------------------------'

scenario:flagevaluation_evp/coalescer/stress/10flags_1000users_250fields

  • 🟥 execution_time [+13.735ms; +14.983ms] or [+7.196%; +7.850%]
  • 🟥 throughput [-382.006op/s; -351.003op/s] or [-7.291%; -6.700%]

scenario:flagevaluation_evp/payloads/stress/10flags_1000users_250fields

  • 🟥 execution_time [+12.374ms; +12.976ms] or [+6.765%; +7.094%]
  • 🟥 throughput [-362.497op/s; -346.116op/s] or [-6.630%; -6.330%]

scenario:sdk_test_data/rules-based

  • 🟥 execution_time [+36.248µs; +41.051µs] or [+13.358%; +15.128%]

scenario:single_flag_killswitch/rules-based

  • 🟥 execution_time [+35.315ns; +36.441ns] or [+21.227%; +21.903%]

Candidate

Candidate benchmark details

Group 1

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz 983b4b0 1793735878 helen/ffe-dependent-flags-evaluator
scenario metric min mean ± sd median ± mad p75 p95 p99 max peak_to_median_ratio skewness kurtosis cv sem runs sample_size
dependent_flags/memoized_16_references execution_time 3.014µs 3.026µs ± 0.004µs 3.025µs ± 0.001µs 3.027µs 3.029µs 3.039µs 3.059µs 1.13% 4.750 38.802 0.12% 0.000µs 1 200
scenario metric 95% CI mean Shapiro-Wilk pvalue Ljung-Box pvalue (lag=1) Dip test pvalue
dependent_flags/memoized_16_references execution_time [3.025µs; 3.026µs] or [-0.017%; +0.017%] None None None

Group 2

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz 983b4b0 1793735878 helen/ffe-dependent-flags-evaluator
scenario metric min mean ± sd median ± mad p75 p95 p99 max peak_to_median_ratio skewness kurtosis cv sem runs sample_size
dependent_flags/legacy execution_time 113.370ns 113.959ns ± 0.192ns 113.965ns ± 0.111ns 114.061ns 114.220ns 114.549ns 114.684ns 0.63% 0.159 2.281 0.17% 0.014ns 1 200
scenario metric 95% CI mean Shapiro-Wilk pvalue Ljung-Box pvalue (lag=1) Dip test pvalue
dependent_flags/legacy execution_time [113.932ns; 113.985ns] or [-0.023%; +0.023%] None None None

Group 3

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz 983b4b0 1793735878 helen/ffe-dependent-flags-evaluator
scenario metric min mean ± sd median ± mad p75 p95 p99 max peak_to_median_ratio skewness kurtosis cv sem runs sample_size
dependent_flags/one_prerequisite execution_time 739.088ns 742.316ns ± 1.820ns 742.146ns ± 0.265ns 742.408ns 743.195ns 745.512ns 763.660ns 2.90% 9.122 98.592 0.24% 0.129ns 1 200
scenario metric 95% CI mean Shapiro-Wilk pvalue Ljung-Box pvalue (lag=1) Dip test pvalue
dependent_flags/one_prerequisite execution_time [742.064ns; 742.568ns] or [-0.034%; +0.034%] None None None

Group 4

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz 983b4b0 1793735878 helen/ffe-dependent-flags-evaluator
scenario metric min mean ± sd median ± mad p75 p95 p99 max peak_to_median_ratio skewness kurtosis cv sem runs sample_size
flagevaluation_evp/coalescer/scale/2500flags_500users_20fields execution_time 32.033ms 32.183ms ± 0.057ms 32.173ms ± 0.031ms 32.205ms 32.282ms 32.335ms 32.369ms 0.61% 0.603 1.610 0.18% 0.008ms 1 50
flagevaluation_evp/coalescer/scale/2500flags_500users_20fields throughput 77235.302op/s 77681.543op/s ± 137.967op/s 77705.473op/s ± 75.272op/s 77751.816op/s 77855.522op/s 77976.831op/s 78043.780op/s 0.44% -0.586 1.591 0.18% 19.512op/s 1 50
flagevaluation_evp/coalescer/stress/10flags_1000users_250fields execution_time 204.312ms 205.233ms ± 1.274ms 204.658ms ± 0.258ms 205.016ms 208.141ms 208.217ms 208.230ms 1.75% 1.745 1.275 0.61% 0.255ms 1 25
flagevaluation_evp/coalescer/stress/10flags_1000users_250fields throughput 4802.385op/s 4872.701op/s ± 29.939op/s 4886.197op/s ± 6.158op/s 4887.542op/s 4893.320op/s 4894.206op/s 4894.473op/s 0.17% -1.741 1.265 0.60% 5.988op/s 1 25
flagevaluation_evp/coalescer/typical/100flags_50users_10fields execution_time 690.698µs 702.403µs ± 5.065µs 702.272µs ± 3.674µs 707.096µs 709.992µs 710.690µs 711.666µs 1.34% -0.237 -0.495 0.72% 0.506µs 1 100
flagevaluation_evp/coalescer/typical/100flags_50users_10fields throughput 140515.295op/s 142375.744op/s ± 1028.497op/s 142394.877op/s ± 744.829op/s 143024.705op/s 144407.169op/s 144761.028op/s 144781.077op/s 1.68% 0.268 -0.458 0.72% 102.850op/s 1 100
scenario metric 95% CI mean Shapiro-Wilk pvalue Ljung-Box pvalue (lag=1) Dip test pvalue
flagevaluation_evp/coalescer/scale/2500flags_500users_20fields execution_time [32.167ms; 32.199ms] or [-0.049%; +0.049%] None None None
flagevaluation_evp/coalescer/scale/2500flags_500users_20fields throughput [77643.301op/s; 77719.785op/s] or [-0.049%; +0.049%] None None None
flagevaluation_evp/coalescer/stress/10flags_1000users_250fields execution_time [204.733ms; 205.732ms] or [-0.243%; +0.243%] None None None
flagevaluation_evp/coalescer/stress/10flags_1000users_250fields throughput [4860.965op/s; 4884.437op/s] or [-0.241%; +0.241%] None None None
flagevaluation_evp/coalescer/typical/100flags_50users_10fields execution_time [701.410µs; 703.396µs] or [-0.141%; +0.141%] None None None
flagevaluation_evp/coalescer/typical/100flags_50users_10fields throughput [142174.162op/s; 142577.326op/s] or [-0.142%; +0.142%] None None None

Group 5

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz 983b4b0 1793735878 helen/ffe-dependent-flags-evaluator
scenario metric min mean ± sd median ± mad p75 p95 p99 max peak_to_median_ratio skewness kurtosis cv sem runs sample_size
single_flag_killswitch/rules-based execution_time 199.281ns 202.248ns ± 1.995ns 201.912ns ± 1.511ns 203.708ns 205.661ns 207.544ns 209.307ns 3.66% 0.604 0.317 0.98% 0.141ns 1 200
scenario metric 95% CI mean Shapiro-Wilk pvalue Ljung-Box pvalue (lag=1) Dip test pvalue
single_flag_killswitch/rules-based execution_time [201.971ns; 202.524ns] or [-0.137%; +0.137%] None None None

Group 6

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz 983b4b0 1793735878 helen/ffe-dependent-flags-evaluator
scenario metric min mean ± sd median ± mad p75 p95 p99 max peak_to_median_ratio skewness kurtosis cv sem runs sample_size
sdk_test_data/rules-based execution_time 303.545µs 310.013µs ± 11.974µs 306.798µs ± 1.566µs 309.356µs 322.770µs 355.692µs 430.385µs 40.28% 6.618 55.386 3.85% 0.847µs 1 200
scenario metric 95% CI mean Shapiro-Wilk pvalue Ljung-Box pvalue (lag=1) Dip test pvalue
sdk_test_data/rules-based execution_time [308.353µs; 311.672µs] or [-0.535%; +0.535%] None None None

Group 7

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz 983b4b0 1793735878 helen/ffe-dependent-flags-evaluator
scenario metric min mean ± sd median ± mad p75 p95 p99 max peak_to_median_ratio skewness kurtosis cv sem runs sample_size
flagevaluation_evp/payloads/scale/2500flags_500users_20fields execution_time 34.941ms 35.145ms ± 0.106ms 35.153ms ± 0.067ms 35.208ms 35.321ms 35.383ms 35.416ms 0.75% 0.178 -0.251 0.30% 0.015ms 1 50
flagevaluation_evp/payloads/scale/2500flags_500users_20fields throughput 70588.585op/s 71134.406op/s ± 213.488op/s 71117.769op/s ± 134.901op/s 71270.103op/s 71459.080op/s 71532.691op/s 71550.134op/s 0.61% -0.163 -0.267 0.30% 30.192op/s 1 50
flagevaluation_evp/payloads/stress/10flags_1000users_250fields execution_time 194.603ms 195.574ms ± 0.650ms 195.386ms ± 0.483ms 195.958ms 196.583ms 197.095ms 197.254ms 0.96% 0.786 -0.033 0.33% 0.130ms 1 25
flagevaluation_evp/payloads/stress/10flags_1000users_250fields throughput 5069.614op/s 5113.220op/s ± 16.957op/s 5118.066op/s ± 12.679op/s 5125.457op/s 5132.272op/s 5137.234op/s 5138.680op/s 0.40% -0.773 -0.063 0.32% 3.391op/s 1 25
flagevaluation_evp/payloads/typical/100flags_50users_10fields execution_time 844.055µs 845.843µs ± 0.649µs 845.946µs ± 0.409µs 846.252µs 846.842µs 847.171µs 847.197µs 0.15% -0.320 -0.141 0.08% 0.065µs 1 100
flagevaluation_evp/payloads/typical/100flags_50users_10fields throughput 118036.342op/s 118225.271op/s ± 90.793op/s 118210.846op/s ± 57.114op/s 118282.645op/s 118395.318op/s 118425.369op/s 118475.692op/s 0.22% 0.324 -0.139 0.08% 9.079op/s 1 100
scenario metric 95% CI mean Shapiro-Wilk pvalue Ljung-Box pvalue (lag=1) Dip test pvalue
flagevaluation_evp/payloads/scale/2500flags_500users_20fields execution_time [35.116ms; 35.174ms] or [-0.083%; +0.083%] None None None
flagevaluation_evp/payloads/scale/2500flags_500users_20fields throughput [71075.232op/s; 71193.581op/s] or [-0.083%; +0.083%] None None None
flagevaluation_evp/payloads/stress/10flags_1000users_250fields execution_time [195.319ms; 195.828ms] or [-0.130%; +0.130%] None None None
flagevaluation_evp/payloads/stress/10flags_1000users_250fields throughput [5106.573op/s; 5119.867op/s] or [-0.130%; +0.130%] None None None
flagevaluation_evp/payloads/typical/100flags_50users_10fields execution_time [845.716µs; 845.971µs] or [-0.015%; +0.015%] None None None
flagevaluation_evp/payloads/typical/100flags_50users_10fields throughput [118207.476op/s; 118243.066op/s] or [-0.015%; +0.015%] None None None

Baseline

Baseline benchmark details

Group 1

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz 899c4cc 1793721195 main
scenario metric min mean ± sd median ± mad p75 p95 p99 max peak_to_median_ratio skewness kurtosis cv sem runs sample_size
flagevaluation_evp/coalescer/scale/2500flags_500users_20fields execution_time 31.414ms 31.560ms ± 0.068ms 31.573ms ± 0.045ms 31.603ms 31.662ms 31.681ms 31.693ms 0.38% -0.300 -0.657 0.21% 0.010ms 1 50
flagevaluation_evp/coalescer/scale/2500flags_500users_20fields throughput 78882.287op/s 79215.152op/s ± 171.549op/s 79182.626op/s ± 113.308op/s 79319.182op/s 79512.470op/s 79564.277op/s 79582.342op/s 0.50% 0.309 -0.654 0.21% 24.261op/s 1 50
flagevaluation_evp/coalescer/stress/10flags_1000users_250fields execution_time 190.137ms 190.873ms ± 0.954ms 190.653ms ± 0.212ms 190.841ms 193.330ms 193.937ms 193.954ms 1.73% 2.688 6.088 0.49% 0.191ms 1 25
flagevaluation_evp/coalescer/stress/10flags_1000users_250fields throughput 5155.856op/s 5239.206op/s ± 25.835op/s 5245.137op/s ± 5.815op/s 5251.338op/s 5258.018op/s 5259.079op/s 5259.378op/s 0.27% -2.675 6.043 0.48% 5.167op/s 1 25
flagevaluation_evp/coalescer/typical/100flags_50users_10fields execution_time 688.541µs 699.785µs ± 5.146µs 697.795µs ± 2.125µs 705.108µs 708.100µs 708.901µs 709.209µs 1.64% 0.198 -0.894 0.73% 0.515µs 1 100
flagevaluation_evp/coalescer/typical/100flags_50users_10fields throughput 141002.118op/s 142908.686op/s ± 1049.433op/s 143308.649op/s ± 436.430op/s 143571.160op/s 144717.503op/s 144875.005op/s 145234.669op/s 1.34% -0.174 -0.877 0.73% 104.943op/s 1 100
scenario metric 95% CI mean Shapiro-Wilk pvalue Ljung-Box pvalue (lag=1) Dip test pvalue
flagevaluation_evp/coalescer/scale/2500flags_500users_20fields execution_time [31.541ms; 31.579ms] or [-0.060%; +0.060%] None None None
flagevaluation_evp/coalescer/scale/2500flags_500users_20fields throughput [79167.601op/s; 79262.702op/s] or [-0.060%; +0.060%] None None None
flagevaluation_evp/coalescer/stress/10flags_1000users_250fields execution_time [190.499ms; 191.247ms] or [-0.196%; +0.196%] None None None
flagevaluation_evp/coalescer/stress/10flags_1000users_250fields throughput [5229.078op/s; 5249.333op/s] or [-0.193%; +0.193%] None None None
flagevaluation_evp/coalescer/typical/100flags_50users_10fields execution_time [698.776µs; 700.793µs] or [-0.144%; +0.144%] None None None
flagevaluation_evp/coalescer/typical/100flags_50users_10fields throughput [142703.001op/s; 143114.371op/s] or [-0.144%; +0.144%] None None None

Group 2

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz 899c4cc 1793721195 main
scenario metric min mean ± sd median ± mad p75 p95 p99 max peak_to_median_ratio skewness kurtosis cv sem runs sample_size
single_flag_killswitch/rules-based execution_time 162.650ns 166.370ns ± 3.538ns 165.588ns ± 2.175ns 168.133ns 172.683ns 177.589ns 186.532ns 12.65% 1.920 5.899 2.12% 0.250ns 1 200
scenario metric 95% CI mean Shapiro-Wilk pvalue Ljung-Box pvalue (lag=1) Dip test pvalue
single_flag_killswitch/rules-based execution_time [165.880ns; 166.860ns] or [-0.295%; +0.295%] None None None

Group 3

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz 899c4cc 1793721195 main
scenario metric min mean ± sd median ± mad p75 p95 p99 max peak_to_median_ratio skewness kurtosis cv sem runs sample_size
sdk_test_data/rules-based execution_time 265.236µs 271.363µs ± 12.525µs 268.439µs ± 1.444µs 270.500µs 283.141µs 319.992µs 400.153µs 49.07% 7.006 60.594 4.60% 0.886µs 1 200
scenario metric 95% CI mean Shapiro-Wilk pvalue Ljung-Box pvalue (lag=1) Dip test pvalue
sdk_test_data/rules-based execution_time [269.627µs; 273.099µs] or [-0.640%; +0.640%] None None None

Group 4

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz 899c4cc 1793721195 main
scenario metric min mean ± sd median ± mad p75 p95 p99 max peak_to_median_ratio skewness kurtosis cv sem runs sample_size
flagevaluation_evp/payloads/scale/2500flags_500users_20fields execution_time 34.609ms 34.823ms ± 0.116ms 34.807ms ± 0.081ms 34.908ms 35.005ms 35.054ms 35.078ms 0.78% 0.196 -0.732 0.33% 0.016ms 1 50
flagevaluation_evp/payloads/scale/2500flags_500users_20fields throughput 71269.738op/s 71793.064op/s ± 238.551op/s 71824.060op/s ± 167.889op/s 71957.727op/s 72151.793op/s 72230.856op/s 72235.091op/s 0.57% -0.184 -0.737 0.33% 33.736op/s 1 50
flagevaluation_evp/payloads/stress/10flags_1000users_250fields execution_time 182.012ms 182.899ms ± 0.408ms 182.993ms ± 0.232ms 183.207ms 183.520ms 183.574ms 183.578ms 0.32% -0.416 -0.394 0.22% 0.082ms 1 25
flagevaluation_evp/payloads/stress/10flags_1000users_250fields throughput 5447.271op/s 5467.527op/s ± 12.208op/s 5464.701op/s ± 6.914op/s 5475.250op/s 5489.115op/s 5493.157op/s 5494.157op/s 0.54% 0.426 -0.387 0.22% 2.442op/s 1 25
flagevaluation_evp/payloads/typical/100flags_50users_10fields execution_time 853.746µs 855.817µs ± 1.029µs 855.811µs ± 0.571µs 856.375µs 858.004µs 858.184µs 858.456µs 0.31% 0.351 0.082 0.12% 0.103µs 1 100
flagevaluation_evp/payloads/typical/100flags_50users_10fields throughput 116488.219op/s 116847.598op/s ± 140.399op/s 116848.271op/s ± 77.984op/s 116927.796op/s 117075.384op/s 117126.823op/s 117130.826op/s 0.24% -0.344 0.076 0.12% 14.040op/s 1 100
scenario metric 95% CI mean Shapiro-Wilk pvalue Ljung-Box pvalue (lag=1) Dip test pvalue
flagevaluation_evp/payloads/scale/2500flags_500users_20fields execution_time [34.791ms; 34.855ms] or [-0.092%; +0.092%] None None None
flagevaluation_evp/payloads/scale/2500flags_500users_20fields throughput [71726.943op/s; 71859.186op/s] or [-0.092%; +0.092%] None None None
flagevaluation_evp/payloads/stress/10flags_1000users_250fields execution_time [182.739ms; 183.059ms] or [-0.087%; +0.087%] None None None
flagevaluation_evp/payloads/stress/10flags_1000users_250fields throughput [5462.741op/s; 5472.312op/s] or [-0.088%; +0.088%] None None None
flagevaluation_evp/payloads/typical/100flags_50users_10fields execution_time [855.615µs; 856.018µs] or [-0.024%; +0.024%] None None None
flagevaluation_evp/payloads/typical/100flags_50users_10fields throughput [116820.080op/s; 116875.116op/s] or [-0.024%; +0.024%] None None None

Route flags without prerequisite conditions through the allocation-only evaluator, sharing validation and assignment construction with dependent evaluation.

Environment: Datadog workspace

Co-Authored-By: Codex GPT-6 <noreply@localhost>
Base automatically changed from helen/rc-dependent-flags-capability to main October 30, 2026 18:28
hhan2024 and others added 3 commits November 2, 2026 09:19
Apply main’s removal of the obsolete Clippy allowance before merging main, preserving the dependent-flags benchmark.

Environment: Datadog workspace

Co-Authored-By: Codex GPT-6 <noreply@localhost>
Apply main’s removal of the obsolete Clippy allowance before merging main, preserving the dependent-flags benchmark.

Environment: Datadog workspace

Co-Authored-By: Codex GPT-6 <noreply@localhost>
@hhan2024
hhan2024 marked this pull request as ready for review November 2, 2026 15:22
@hhan2024
hhan2024 requested review from a team as code owners November 2, 2026 15:22
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-02T15:26:46.344168Z 1cf7834 Draft marked ready
🔒 Security Review ✅ Completed 2026-11-02T15:33:38.128927Z 1cf7834 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1cf7834903

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@leoromanovsky

leoromanovsky commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

FFE reviewer selection

Reviewers: @typotter, @sameerank

Why: Dependent Flags — The shared Rust FFE engine now evaluates prerequisite (flagEvaluation) conditions. Reviewers come from the matched pod.

🤖 Automated selection.

Selection details

Initiative: project / dependent-flags

Why this match: The shared Rust FFE engine now evaluates prerequisite (flagEvaluation) conditions. It compares prerequisite variant keys with ONE_OF/NOT_ONE_OF and short-circuits. It handles missing, disabled, cyclic and over-depth prerequisites, enforces maxDependencyDepth with memoized dependency height, and parses prerequisite conditions in the UFC models and compiled config. This is the dependent-flags behavior that SDKs consume, and benchmarks and tests for dependent flags are included.

  • @typotter — primary (dependent-flags): Matched pod member. Covers: openfeature-architecture. Existing review coverage preserved.
  • @sameerank — floating (dependent-flags): Coverage and knowledge sharing. Existing review coverage preserved.

Additional review: openfeature-architecture — The change adds to the public evaluation contract that SDK providers consume. It exports a new eval_flag_with_trace API that returns EvaluationResult/EvaluationRecord with per-flag results and an exposure_eligible marker. It also adds public EvaluationError variants (DependencyEvaluationFailed, DependencyCycle, DependencyDepthExceeded) that providers must map to OpenFeature error and reason metadata. Coverage: @typotter. Evidence: libdd-ffe/src/rules_based/eval/eval_assignment.rs, libdd-ffe/src/rules_based/error.rs, libdd-ffe/src/rules_based/eval/mod.rs, libdd-ffe/src/rules_based/mod.rs

The PR author was excluded. Existing reviewers and teams were preserved.

Debug or fix a problem

Workflow run · Bits Code session

  • Wrong project or pod? Clarify this PR’s purpose, or correct the reviewer registry and publish the change. Sheet edits take effect after publication.
  • Need another reviewer now? Request them in GitHub. This workflow only adds requests and keeps existing reviewers.
  • Stuck beyond five minutes or failed? Inspect the run’s failed step and available Bits session. After fixing the cause, rerun the workflow with this PR URL and dry_run=false. Unchanged completed runs are skipped. If the receipt says needs_reconciliation or a write outcome is uncertain, ask the workflow owner to reconcile GitHub and the receipt before retrying; do not delete the lock blindly.
  • Report a problem: Comment on the reviewer-routing skill PR with this PR URL, the workflow run link, and the expected initiative or reviewer correction.
Evidence and run details

✅ Complete — reviewer coverage verified. The workflow checked pending requests or submitted reviews on the evaluated head.

Generated asynchronously by the FFE reviewer-routing workflow.

Changed paths: libdd-ffe/src/rules_based/eval/eval_rules.rs, libdd-ffe/src/rules_based/eval/eval_assignment.rs, libdd-ffe/src/rules_based/ufc/models.rs, libdd-ffe/src/rules_based/ufc/compiled_flag_config.rs, libdd-ffe/src/rules_based/error.rs, libdd-ffe/src/rules_based/eval/dependent_flags_tests.rs, libdd-ffe-test-suite/benches/dependent_flags.rs

Evaluated head: 1cf7834903025181d51480bc6a5a6a174e4e7b47

Registry revision: revision:4442c1de-20f2-4e1b-b952-2b64bbb31794

Pinned skill source: 7e3283c602f6b18a7e3e623eecb52d3a9c091a2e

Instructions: workflow-injected snapshot (no plugin loaded)

Instruction snapshot SHA-256: 52ed8cb68675dde1a47addaefe4a9500891c24014abf7dade6dfe38cf836c1d2

@typotter typotter 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.

🤖 Review from Codex working with Tyler Potter:

Requesting changes for the inline finding.

Comment thread libdd-ffe/src/rules_based/ufc/models.rs Resolved
Validate dependency limits only when an authored condition reaches a prerequisite.
Preserve independent evaluations and skipped branches, align condition validation
and empty-list membership with JS, and record prerequisites rejected by depth.
Cover lazy validation, null limits, trace ordering, and per-flag isolation.

Environment: Datadog workspace

Co-Authored-By: Codex GPT-6 <noreply@localhost>
@aarsilv aarsilv added the crucible: reviewing A Crucible review has started; this PR has reviewer attention. label Nov 4, 2026
Some(value) => {
let depth = value
.as_f64()
.filter(|depth| depth.fract() == 0.0 && (0.0..=255.0).contains(depth))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Comment from Claude working with Aaron Silverman:

[P2] A 255-edge dependency chain overflows small thread stacks. Please lower the accepted maximum, for example to 32.

The evaluation recurses on the caller's stack, several frames per dependency edge. I evaluated a 255-edge chain with maxDependencyDepth: 255 on threads with fixed stack sizes:

Build 128 KiB 256 KiB 512 KiB 1 MiB
release overflow overflow pass pass
debug overflow overflow overflow overflow

Each "overflow" run reported has overflowed its stack; Rust aborts the process on a stack overflow. Host threads with small stacks exist, so a valid configuration should not be able to crash the process.

Suggested change: accept 0..=32 here (and in the README). That cuts the maximum recursion depth by about eight times; I did not measure a 32-edge chain. Make the same change in the other evaluators, the fixtures and the design document, so all SDKs accept the same range.


/// One requested evaluation and the prerequisites it actually evaluated.
#[derive(Debug)]
pub struct EvaluationResult {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Comment from Claude working with Aaron Silverman:

[P3] Two small API points for the new trace types.

  • EvaluationResult and EvaluationRecord are public structs with public fields and no #[non_exhaustive]. Adding a field later is a semver-major change for libdd-ffe. feat(ffe): support dependent flag evaluations dd-trace-php#4421 already builds EvaluationResult with a struct literal. Suggest #[non_exhaustive] on both, plus a constructor such as EvaluationResult::from_error(error).
  • eval_flag_with_trace does not emit the log::trace! records, so its direct callers (the C FFI in feat(ffe): expose prerequisite evaluations through C FFI #2791, and the Python and PHP bindings) lose those diagnostics. eval_flag_with_options already calls eval_flag_with_trace and logs afterwards, so suggest moving the logging into eval_flag_with_trace (covering its early returns) and removing it from eval_flag_with_options, so each evaluation logs once.

/// Records exclude the requested root flag and occur once per evaluated prerequisite.
/// An unsuccessful root has no eligible prerequisite exposures. SDKs must additionally
/// honor each assignment's `do_log`, consent, and exposure deduplication policy.
pub fn eval_flag_with_trace(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Comment from Claude working with Aaron Silverman:

Heads up: This PR conflicts with main, and the recorded checks at 983b4b0 contain no GitHub Actions test results. Several consumers also pin an older revision of this branch.

Suggested order: rebase this PR and #2791 on main, merge both, and release. Then update the Python and PHP native pins to that release, and update Ruby to the published gem that contains both PRs.

.any(|value| value.as_ref() == dependency.assignment.variation_key.as_str())
== *expected_membership;
if matched {
exposures.insert(key.clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Comment from Claude working with Aaron Silverman:

[P1] Proposed exposure rule change, to follow the worked example in the dependent-flags design: a prerequisite whose own comparison rejects the rule should remain eligible for an exposure.

This block adds a prerequisite to exposures only when its condition matches. The design's example uses a rule that requires both a user attribute (country) and a prerequisite flag's value. For a user who matches the attribute but whose prerequisite evaluates to the other variant, the root falls back, and the example lists both the root and the prerequisite for exposure handling (subject to the usual logging rules and deduplication). The design's numbered rule about rejected rules can be read more broadly, so the design is ambiguous; we would like to follow the example.

Suggested evaluator change: when a FlagEvaluation condition is the condition that rejects the rule, keep that key (and dependency.exposures) in a flag-level "influencing" set, and mark those records exposure_eligible when the root succeeds. Rules rejected by another condition, and root failures, keep today's behavior.

The shared fixture cases dependent-one-of-miss and dependent-not-one-of-hit in ffe-system-test-data need the same change, and so do the other evaluators.

// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

use super::eval_assignment::*;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Comment from Claude working with Aaron Silverman:

[P2] This PR does not run the shared dependent-flags cases from ffe-system-test-data, so no checked-in shared case checks this implementation's dependency results or telemetry.

Suggested change: bump the fixtures to b57eac6 and add a runner for evaluation-suites/dependent-flags (or, more generally, every suite directory). Assert result and, where present, expectations.exposures and expectations.evaluationEvents (including _count and noUnmatchedEvents). Assert the case count (14 today), so the runner cannot pass with zero cases. I wrote a runner for that one suite and ran it: with the fixtures at b57eac6, all 14 cases pass at 983b4b0, and the core canonical test still passes. If every record is marked exposure_eligible, the runner fails.

Runner for the dependent-flags suite (tested)
diff --git a/libdd-ffe-test-suite/tests/canonical_fixtures.rs b/libdd-ffe-test-suite/tests/canonical_fixtures.rs
index 8d7c8b69d..d15044704 100644
--- a/libdd-ffe-test-suite/tests/canonical_fixtures.rs
+++ b/libdd-ffe-test-suite/tests/canonical_fixtures.rs
@@ -244,3 +244,150 @@ fn error_code_from_error(err: &EvaluationError) -> Option<&'static str> {
         _ => Some("GENERAL"),
     }
 }
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct SuiteCase {
+    flag: String,
+    variation_type: FlagType,
+    default_value: serde_json::Value,
+    targeting_key: Option<Str>,
+    #[serde(default)]
+    attributes: HashMap<Str, serde_json::Value>,
+    result: TestResult,
+    expectations: Option<serde_json::Value>,
+}
+
+/// Removes one actual event that matches every field the matcher states.
+fn take_match(actual: &mut Vec<serde_json::Value>, matcher: &serde_json::Value) -> bool {
+    let fields: Vec<_> = matcher
+        .as_object()
+        .unwrap()
+        .iter()
+        .filter(|(key, _)| !key.starts_with('_'))
+        .collect();
+    let Some(index) = actual.iter().position(|event| {
+        fields
+            .iter()
+            .all(|(key, value)| event.get(key.as_str()) == Some(value))
+    }) else {
+        return false;
+    };
+    actual.remove(index);
+    true
+}
+
+fn assert_events(
+    kind: &str,
+    flag: &str,
+    mut actual: Vec<serde_json::Value>,
+    expectations: &serde_json::Value,
+) {
+    for matcher in expectations[kind].as_array().cloned().unwrap_or_default() {
+        let count = matcher
+            .get("_count")
+            .and_then(|count| count.as_u64())
+            .unwrap_or(1);
+        for _ in 0..count {
+            assert!(
+                take_match(&mut actual, &matcher),
+                "{flag}: missing {kind} {matcher}; left {actual:?}"
+            );
+        }
+    }
+    if expectations["noUnmatchedEvents"] == serde_json::Value::Bool(true) {
+        assert!(actual.is_empty(), "{flag}: unexpected {kind} {actual:?}");
+    }
+}
+
+#[test]
+#[cfg_attr(miri, ignore)]
+fn evaluates_dependent_flags_suite() {
+    let suite = fixture_root().join("evaluation-suites/dependent-flags");
+    let config = Configuration::from_server_response(
+        UniversalFlagConfig::from_json(fs::read(suite.join("ufc-config.json")).unwrap()).unwrap(),
+    );
+    let cases: Vec<SuiteCase> =
+        serde_json::from_slice(&fs::read(suite.join("cases/dependent-flags.json")).unwrap())
+            .unwrap();
+    assert_eq!(cases.len(), 14);
+    for case in cases {
+        let attributes = case
+            .attributes
+            .into_iter()
+            .map(|(key, value)| (key, serde_json::from_value::<Attribute>(value).unwrap()))
+            .collect();
+        let subject = EvaluationContext::new(case.targeting_key, Arc::new(attributes));
+        let trace = config.eval_flag_with_trace(
+            &case.flag,
+            &subject,
+            case.variation_type.into(),
+            Utc::now(),
+            EvaluationOptions::default(),
+        );
+        let (value, reason, code) = match &trace.result {
+            Ok(assignment) => (
+                assignment.value.variation_value(),
+                reason_from_assignment(assignment.reason),
+                None,
+            ),
+            Err(err) => (
+                case.default_value.clone(),
+                reason_from_error(err),
+                error_code_from_error(err),
+            ),
+        };
+        assert_eq!(value, case.result.value, "{}", case.flag);
+        if let Some(expected) = case.result.reason.as_deref() {
+            assert_eq!(reason, expected, "{}", case.flag);
+        }
+        if let Some(expected) = case.result.error_code.as_deref() {
+            assert_eq!(code, Some(expected), "{}", case.flag);
+        }
+        let Some(expectations) = case.expectations else {
+            continue;
+        };
+        // This runner reports an unassigned or disabled prerequisite record as GENERAL, as the no-assignment case expects.
+        let mut events: Vec<serde_json::Value> = trace
+            .evaluations
+            .iter()
+            .map(|record| {
+                let code = match &record.result {
+                    Ok(_) => None,
+                    Err(EvaluationError::FlagDisabled | EvaluationError::DefaultAllocationNull) => {
+                        Some("GENERAL")
+                    }
+                    Err(err) => error_code_from_error(err),
+                };
+                serde_json::json!({"flag": record.flag_key.as_str(), "errorCode": code})
+            })
+            .collect();
+        events.push(serde_json::json!({"flag": case.flag, "errorCode": code}));
+        assert_events("evaluationEvents", &case.flag, events, &expectations);
+
+        let mut exposures: Vec<serde_json::Value> = trace
+            .evaluations
+            .iter()
+            .filter_map(|record| match &record.result {
+                Ok(assignment) if record.exposure_eligible && assignment.do_log => {
+                    Some(serde_json::json!({
+                        "flag": record.flag_key.as_str(),
+                        "allocation": assignment.allocation_key.as_str(),
+                        "variant": assignment.variation_key.as_str(),
+                    }))
+                }
+                _ => None,
+            })
+            .collect();
+        if let Ok(assignment) = &trace.result {
+            if assignment.do_log {
+                exposures.push(serde_json::json!({
+                    "flag": case.flag,
+                    "allocation": assignment.allocation_key.as_str(),
+                    "variant": assignment.variation_key.as_str(),
+                }));
+            }
+        }
+        assert_events("exposures", &case.flag, exposures, &expectations);
+    }
+}

if depth + result.height > limit {
return Err(EvaluationError::DependencyDepthExceeded);
}
return Ok(result.clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Comment from Claude working with Aaron Silverman:

[P2] No test reuses a memoized prerequisite that has its own successful prerequisites, so a regression in this line passes all existing dependent-flag tests.

I changed this line to drop the nested exposures on a memo hit: return Ok(EvaluatedFlag { exposures: HashSet::new(), ..result.clone() });. All 27 dependent-flag tests still passed. The existing successful memo-reuse tests reuse leaf prerequisites, and two shared fixture cases (reuse across allocations, and a transitive success followed by a root rejection) have no exact Rust equivalent.

Suggested tests (tested)

All three pass on 983b4b0. memoized_transitive_prerequisite_keeps_nested_exposures_when_reused fails with the mutation above.

diff --git a/libdd-ffe/src/rules_based/eval/dependent_flags_tests.rs b/libdd-ffe/src/rules_based/eval/dependent_flags_tests.rs
index 23fc808ad..bebd00390 100644
--- a/libdd-ffe/src/rules_based/eval/dependent_flags_tests.rs
+++ b/libdd-ffe/src/rules_based/eval/dependent_flags_tests.rs
@@ -687,3 +687,52 @@ fn independent_flags_ignore_unrelated_dependency_graphs() {
     assert_eq!(result.result.unwrap().variation_key.as_str(), "on");
     assert!(result.evaluations.is_empty());
 }
+
+#[test]
+fn memoized_transitive_prerequisite_keeps_nested_exposures_when_reused() {
+    let mut root = flag(vec![]);
+    root["allocations"][0]["rules"] = json!([
+        {"conditions": [dependency("parent"), fails()]},
+        {"conditions": [dependency("parent")]}
+    ]);
+    let r = eval(wire(
+        json!({"root": root, "parent": flag(vec![dependency("leaf")]), "leaf": flag(vec![])}),
+    ));
+    assert_eq!(r.result.unwrap().variation_key.as_str(), "on");
+    assert_eq!(
+        r.evaluations
+            .iter()
+            .map(|e| e.flag_key.as_str())
+            .collect::<Vec<_>>(),
+        ["leaf", "parent"]
+    );
+    assert!(r.evaluations.iter().all(|e| e.exposure_eligible));
+}
+
+#[test]
+fn transitive_success_then_root_rule_rejection_exposes_nothing() {
+    let r = eval(wire(
+        json!({"root": flag(vec![dependency("parent"), fails()]),
+        "parent": flag(vec![dependency("leaf")]), "leaf": flag(vec![])}),
+    ));
+    assert_eq!(r.result.unwrap().variation_key.as_str(), "off");
+    assert_eq!(r.evaluations.len(), 2);
+    assert!(
+        r.evaluations
+            .iter()
+            .all(|e| e.result.is_ok() && !e.exposure_eligible)
+    );
+}
+
+#[test]
+fn prerequisite_first_reached_in_rejected_allocation_is_exposed_once_by_later_allocation() {
+    let mut root = flag(vec![dependency("parent"), fails()]);
+    root["allocations"]
+        .as_array_mut()
+        .unwrap()
+        .insert(1, allocation(vec![dependency("parent")], "on"));
+    let r = eval(wire(json!({"root": root, "parent": flag(vec![])})));
+    assert_eq!(r.result.unwrap().variation_key.as_str(), "on");
+    assert_eq!(r.evaluations.len(), 1);
+    assert!(r.evaluations[0].exposure_eligible);
+}

Comment thread libdd-ffe/README.md
marked `exposure_eligible` belong to the root's successful rule/split path; a failed root has
no eligible prerequisite exposures. SDKs must
also apply each assignment's `do_log`, consent, and ordinary exposure deduplication policy.
The trace does not emit telemetry itself. SDKs should advertise dependent-flag support only

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Comment from Claude working with Aaron Silverman:

Reminder: release order for the dependent-flags stack.

  1. Rebase this PR and feat(ffe): expose prerequisite evaluations through C FFI #2791 on main, merge, and release libdatadog.
  2. Update the native pins in feat(openfeature): support dependent flags on Python delivery paths dd-trace-py#21346 and feat(ffe): support dependent flag evaluations dd-trace-php#4421 to that release, and update feat(open_feature): support dependent flag evaluations dd-trace-rb#6613 to the published libdatadog gem that contains both PRs.
  3. Server-side capability gating must be live before any SDK advertises dependent-flag-variant-key-v1.


#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct FlagEvaluationWire {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Comment from Claude working with Aaron Silverman:

[P1] An unknown key inside flagEvaluation makes the flag that contains it fail. #[serde(deny_unknown_fields)] on FlagEvaluationWire rejects any key other than key, so a future field such as {"key": "beta", "property": "variant"} makes the flag fail and return the caller default. dd-trace-py and dd-trace-rb inherit this through libdd-ffe. Other flags still work, unless their evaluation reaches the failing flag as a prerequisite.

We want UFC parsing to tolerate newly added fields. We made the same request for the edge evaluator in ddoghq/dd-source#155394. The dependent-flags design leaves evaluating other prerequisite properties, such as enablement state and error codes, open as future extensions.

Bench result (local merged SDK builds, with the ffe-dogfooding staging UFC plus synthetic flags): the scenario added unknown keys inside each flagEvaluation, on each dependent condition, and in evaluatorParams. Go, Node.js, Java, .NET, Python and Ruby returned errors for every tested dependent flag, because each one rejects the extra flagEvaluation key. PHP, whose libdatadog branch has no deny_unknown_fields on FlagEvaluationWire, returned the expected values.

Suggested change: read key, ignore other keys, and keep rejecting a missing or empty key (remove deny_unknown_fields from FlagEvaluationWire, and change the extra-key case in invalid_dependency_conditions_are_isolated_to_the_containing_flag to expect acceptance). If strict parsing is intended instead, the server must keep new fields away from older clients, for example through a new capability version. Either way, please add a shared case with an extra flagEvaluation key to the ffe-system-test-data dependent-flags suite, so that all SDKs agree.

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.

going to make this change. system test to ensure this behavior added in ffe-system-test-data #71: and system-tests #8378

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

crucible: reviewing A Crucible review has started; this PR has reviewer attention.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants