Skip to content

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

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

hhan2024 wants to merge 8 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?

Evaluates flagEvaluation conditions using prerequisite variant keys with ONE_OF and NOT_ONE_OF, preserving authored order, short-circuiting, and per-root memoization. A mismatch rejects the rule; a reached missing, disabled, malformed, unassigned, cyclic, or over-depth prerequisite aborts the root and preserves the failure cause.

Configuration::eval_flag_with_trace returns each reached prerequisite once, including errors and nodes rejected by the depth guard. Evaluation records survive rejected paths. Only prerequisites contributing to the selected rule and split are marked exposure_eligible; a failed root has no eligible prerequisite exposures. The selected assignment retains its UFC do_log bit, including fallback allocations.

SDK consumers must additionally honor do_log, privacy, final OpenFeature hook success, and ordinary exposure deduplication. Exposure publication and cache admission belong after successful finalization; evaluation records remain reportable on error. The engine does not publish telemetry itself. Python consumes this API in dd-trace-py #21346.

Dependency depth counts edges, defaults to 10, and accepts explicit limits from 0 through 255. Limit validation is deferred until a dependency condition executes, so invalid/null settings do not break independent or short-circuited evaluations. Memoized results retain the deepest visited branch, including rejected paths, to prevent reuse from bypassing the limit. Dependency-free flags retain their allocation-only fast path.

Shared contract coverage

Updates the canonical fixture submodule to merged revision 9afc3f75c3fef9643a9c1b5e2b1e5a9adac48177 and activates all 14 dependent-flag cases in libdd-ffe-test-suite. The runner asserts results, specified metadata, prerequisite/root evaluation records, and selected-path exposure eligibility, including exact-event expectations and platform selection controls. It runs in the existing crate test CI without requiring a separate activation flag.

Validation

  • Evaluator: 137 unit tests and 10 documentation tests passed, with Python, exposure, EVP, and evaluation-metrics features enabled.
  • Shared fixture crate: canonical core fixtures, all 14 dependent-flag cases, and the existing experimentation-opt-out tests passed.
  • Pinned nightly formatting, clippy for the fixture crate with all targets and warnings denied, structural lint, and whitespace checks passed.
  • The Python consumer's complete OpenFeature suites passed on 0.8 and 0.10, including the depth-rejected prerequisite error event and transactional exposure rollback. Tyler Potter's shared dependency telemetry system test also passed against that consumer.

Integration and release

Stacked on #2745 for the native RC capability enum. This PR does not automatically advertise capabilities. Consumers must integrate and test both evaluation and exposure reporting before enabling dependent-flag delivery; a native library release remains separate.

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: ad8cc4c | Docs | Give us feedback!

@pr-commenter

pr-commenter Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Benchmarks

Comparison

Benchmark execution time: 2026-11-06 19:11:59

Comparing candidate commit ad8cc4c in PR branch helen/ffe-dependent-flags-evaluator with baseline commit ac6320b 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 [+11.149ms; +12.200ms] or [+5.810%; +6.358%]
  • 🟥 throughput [-312.036op/s; -285.653op/s] or [-5.988%; -5.481%]

scenario:flagevaluation_evp/payloads/stress/10flags_1000users_250fields

  • 🟥 execution_time [+11.652ms; +12.076ms] or [+6.345%; +6.576%]
  • 🟥 throughput [-336.478op/s; -324.398op/s] or [-6.179%; -5.958%]

scenario:sdk_test_data/rules-based

  • 🟥 execution_time [+35.582µs; +40.216µs] or [+13.051%; +14.750%]

scenario:single_flag_killswitch/rules-based

  • 🟥 execution_time [+34.250ns; +35.296ns] or [+20.454%; +21.079%]

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 ad8cc4c 1793991688 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 31.679ms 31.815ms ± 0.055ms 31.813ms ± 0.033ms 31.841ms 31.893ms 31.975ms 32.025ms 0.67% 0.944 3.283 0.17% 0.008ms 1 50
flagevaluation_evp/coalescer/scale/2500flags_500users_20fields throughput 78063.532op/s 78579.686op/s ± 134.721op/s 78583.922op/s ± 82.589op/s 78676.577op/s 78759.475op/s 78846.615op/s 78917.295op/s 0.42% -0.921 3.220 0.17% 19.052op/s 1 50
flagevaluation_evp/coalescer/stress/10flags_1000users_250fields execution_time 202.657ms 203.564ms ± 1.042ms 203.250ms ± 0.151ms 203.398ms 206.276ms 206.378ms 206.394ms 1.55% 2.143 3.016 0.50% 0.208ms 1 25
flagevaluation_evp/coalescer/stress/10flags_1000users_250fields throughput 4845.103op/s 4912.585op/s ± 24.889op/s 4920.039op/s ± 3.653op/s 4923.692op/s 4931.212op/s 4933.909op/s 4934.451op/s 0.29% -2.136 2.999 0.50% 4.978op/s 1 25
flagevaluation_evp/coalescer/typical/100flags_50users_10fields execution_time 687.295µs 697.997µs ± 4.215µs 697.440µs ± 2.136µs 700.636µs 705.605µs 705.975µs 706.473µs 1.30% -0.161 0.261 0.60% 0.422µs 1 100
flagevaluation_evp/coalescer/typical/100flags_50users_10fields throughput 141548.252op/s 143272.296op/s ± 866.182op/s 143381.421op/s ± 440.510op/s 143741.124op/s 145067.300op/s 145411.452op/s 145497.895op/s 1.48% 0.201 0.298 0.60% 86.618op/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.800ms; 31.830ms] or [-0.048%; +0.048%] None None None
flagevaluation_evp/coalescer/scale/2500flags_500users_20fields throughput [78542.344op/s; 78617.028op/s] or [-0.048%; +0.048%] None None None
flagevaluation_evp/coalescer/stress/10flags_1000users_250fields execution_time [203.155ms; 203.972ms] or [-0.201%; +0.201%] None None None
flagevaluation_evp/coalescer/stress/10flags_1000users_250fields throughput [4902.829op/s; 4922.342op/s] or [-0.199%; +0.199%] None None None
flagevaluation_evp/coalescer/typical/100flags_50users_10fields execution_time [697.171µs; 698.823µs] or [-0.118%; +0.118%] None None None
flagevaluation_evp/coalescer/typical/100flags_50users_10fields throughput [143102.528op/s; 143442.065op/s] or [-0.118%; +0.118%] None None None

Group 2

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz ad8cc4c 1793991688 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 723.014ns 726.495ns ± 1.151ns 726.302ns ± 0.259ns 726.574ns 728.640ns 731.151ns 735.215ns 1.23% 3.466 20.143 0.16% 0.081ns 1 200
scenario metric 95% CI mean Shapiro-Wilk pvalue Ljung-Box pvalue (lag=1) Dip test pvalue
dependent_flags/one_prerequisite execution_time [726.335ns; 726.654ns] or [-0.022%; +0.022%] None None None

Group 3

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz ad8cc4c 1793991688 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.011µs 3.025µs ± 0.004µs 3.025µs ± 0.001µs 3.026µs 3.029µs 3.037µs 3.069µs 1.45% 6.088 61.785 0.14% 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.019%; +0.019%] None None None

Group 4

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz ad8cc4c 1793991688 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.832ms 35.007ms ± 0.078ms 35.004ms ± 0.057ms 35.064ms 35.129ms 35.166ms 35.171ms 0.48% 0.086 -0.498 0.22% 0.011ms 1 50
flagevaluation_evp/payloads/scale/2500flags_500users_20fields throughput 71081.307op/s 71413.860op/s ± 158.411op/s 71421.022op/s ± 117.306op/s 71529.751op/s 71659.481op/s 71723.778op/s 71773.270op/s 0.49% -0.077 -0.497 0.22% 22.403op/s 1 50
flagevaluation_evp/payloads/stress/10flags_1000users_250fields execution_time 194.877ms 195.515ms ± 0.318ms 195.591ms ± 0.186ms 195.674ms 195.990ms 196.064ms 196.082ms 0.25% -0.233 -0.591 0.16% 0.064ms 1 25
flagevaluation_evp/payloads/stress/10flags_1000users_250fields throughput 5099.896op/s 5114.720op/s ± 8.318op/s 5112.716op/s ± 4.877op/s 5119.974op/s 5128.530op/s 5130.880op/s 5131.435op/s 0.37% 0.240 -0.589 0.16% 1.664op/s 1 25
flagevaluation_evp/payloads/typical/100flags_50users_10fields execution_time 842.821µs 844.850µs ± 1.289µs 844.481µs ± 0.929µs 846.026µs 846.942µs 847.274µs 847.282µs 0.33% 0.354 -1.169 0.15% 0.129µs 1 100
flagevaluation_evp/payloads/typical/100flags_50users_10fields throughput 118024.396op/s 118364.461op/s ± 180.465op/s 118415.932op/s ± 130.246op/s 118504.529op/s 118620.668op/s 118648.818op/s 118649.097op/s 0.20% -0.351 -1.170 0.15% 18.047op/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.986ms; 35.029ms] or [-0.061%; +0.061%] None None None
flagevaluation_evp/payloads/scale/2500flags_500users_20fields throughput [71369.952op/s; 71457.769op/s] or [-0.061%; +0.061%] None None None
flagevaluation_evp/payloads/stress/10flags_1000users_250fields execution_time [195.390ms; 195.639ms] or [-0.064%; +0.064%] None None None
flagevaluation_evp/payloads/stress/10flags_1000users_250fields throughput [5111.459op/s; 5117.981op/s] or [-0.064%; +0.064%] None None None
flagevaluation_evp/payloads/typical/100flags_50users_10fields execution_time [844.598µs; 845.103µs] or [-0.030%; +0.030%] None None None
flagevaluation_evp/payloads/typical/100flags_50users_10fields throughput [118329.090op/s; 118399.831op/s] or [-0.030%; +0.030%] None None None

Group 5

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz ad8cc4c 1793991688 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 304.961µs 310.539µs ± 11.664µs 307.568µs ± 1.131µs 309.601µs 321.662µs 358.835µs 426.880µs 38.79% 6.731 55.540 3.75% 0.825µ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.922µs; 312.155µs] or [-0.521%; +0.521%] None None None

Group 6

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz ad8cc4c 1793991688 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 110.371ns 110.947ns ± 1.254ns 110.851ns ± 0.049ns 110.913ns 110.997ns 111.088ns 128.536ns 15.95% 13.903 192.531 1.13% 0.089ns 1 200
scenario metric 95% CI mean Shapiro-Wilk pvalue Ljung-Box pvalue (lag=1) Dip test pvalue
dependent_flags/legacy execution_time [110.774ns; 111.121ns] or [-0.157%; +0.157%] None None None

Group 7

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz ad8cc4c 1793991688 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.023ns 202.221ns ± 1.994ns 201.771ns ± 1.317ns 203.559ns 205.823ns 206.979ns 208.765ns 3.47% 0.606 0.159 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.944ns; 202.497ns] or [-0.137%; +0.137%] 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 ac6320b 1793981605 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.675ms 31.740ms ± 0.040ms 31.734ms ± 0.027ms 31.766ms 31.810ms 31.829ms 31.841ms 0.34% 0.539 -0.529 0.13% 0.006ms 1 50
flagevaluation_evp/coalescer/scale/2500flags_500users_20fields throughput 78514.296op/s 78765.216op/s ± 100.106op/s 78778.840op/s ± 68.290op/s 78838.459op/s 78902.724op/s 78915.841op/s 78925.543op/s 0.19% -0.535 -0.536 0.13% 14.157op/s 1 50
flagevaluation_evp/coalescer/stress/10flags_1000users_250fields execution_time 191.220ms 191.889ms ± 0.843ms 191.706ms ± 0.189ms 191.873ms 193.923ms 194.657ms 194.763ms 1.59% 2.611 5.923 0.43% 0.169ms 1 25
flagevaluation_evp/coalescer/stress/10flags_1000users_250fields throughput 5134.435op/s 5211.430op/s ± 22.650op/s 5216.325op/s ± 5.150op/s 5222.921op/s 5228.255op/s 5229.285op/s 5229.569op/s 0.25% -2.596 5.868 0.43% 4.530op/s 1 25
flagevaluation_evp/coalescer/typical/100flags_50users_10fields execution_time 690.677µs 704.882µs ± 5.124µs 707.322µs ± 1.576µs 708.457µs 709.715µs 710.024µs 710.231µs 0.41% -1.140 0.008 0.72% 0.512µs 1 100
flagevaluation_evp/coalescer/typical/100flags_50users_10fields throughput 140799.292op/s 141875.155op/s ± 1040.006op/s 141378.262op/s ± 314.265op/s 142528.415op/s 143900.063op/s 144450.972op/s 144785.479op/s 2.41% 1.155 0.052 0.73% 104.001op/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.729ms; 31.751ms] or [-0.035%; +0.035%] None None None
flagevaluation_evp/coalescer/scale/2500flags_500users_20fields throughput [78737.469op/s; 78792.964op/s] or [-0.035%; +0.035%] None None None
flagevaluation_evp/coalescer/stress/10flags_1000users_250fields execution_time [191.559ms; 192.220ms] or [-0.172%; +0.172%] None None None
flagevaluation_evp/coalescer/stress/10flags_1000users_250fields throughput [5202.551op/s; 5220.309op/s] or [-0.170%; +0.170%] None None None
flagevaluation_evp/coalescer/typical/100flags_50users_10fields execution_time [703.878µs; 705.887µs] or [-0.142%; +0.142%] None None None
flagevaluation_evp/coalescer/typical/100flags_50users_10fields throughput [141671.317op/s; 142078.992op/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 ac6320b 1793981605 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 35.166ms 35.453ms ± 0.106ms 35.463ms ± 0.066ms 35.509ms 35.599ms 35.656ms 35.709ms 0.69% -0.299 0.305 0.30% 0.015ms 1 50
flagevaluation_evp/payloads/scale/2500flags_500users_20fields throughput 70010.509op/s 70517.244op/s ± 210.479op/s 70496.784op/s ± 130.706op/s 70658.310op/s 70870.420op/s 71039.983op/s 71090.705op/s 0.84% 0.318 0.321 0.30% 29.766op/s 1 50
flagevaluation_evp/payloads/stress/10flags_1000users_250fields execution_time 182.947ms 183.650ms ± 0.438ms 183.607ms ± 0.240ms 183.851ms 184.420ms 184.760ms 184.839ms 0.67% 0.688 0.909 0.23% 0.088ms 1 25
flagevaluation_evp/payloads/stress/10flags_1000users_250fields throughput 5410.122op/s 5445.158op/s ± 12.972op/s 5446.420op/s ± 7.100op/s 5450.901op/s 5465.280op/s 5465.868op/s 5466.053op/s 0.36% -0.671 0.880 0.23% 2.594op/s 1 25
flagevaluation_evp/payloads/typical/100flags_50users_10fields execution_time 857.989µs 860.137µs ± 1.292µs 860.022µs ± 1.026µs 861.064µs 862.072µs 863.200µs 863.779µs 0.44% 0.323 -0.540 0.15% 0.129µs 1 100
flagevaluation_evp/payloads/typical/100flags_50users_10fields throughput 115770.382op/s 116260.804op/s ± 174.516op/s 116276.045op/s ± 138.566op/s 116406.920op/s 116512.959op/s 116549.810op/s 116551.599op/s 0.24% -0.317 -0.550 0.15% 17.452op/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.423ms; 35.482ms] or [-0.083%; +0.083%] None None None
flagevaluation_evp/payloads/scale/2500flags_500users_20fields throughput [70458.903op/s; 70575.585op/s] or [-0.083%; +0.083%] None None None
flagevaluation_evp/payloads/stress/10flags_1000users_250fields execution_time [183.479ms; 183.822ms] or [-0.094%; +0.094%] None None None
flagevaluation_evp/payloads/stress/10flags_1000users_250fields throughput [5440.073op/s; 5450.243op/s] or [-0.093%; +0.093%] None None None
flagevaluation_evp/payloads/typical/100flags_50users_10fields execution_time [859.884µs; 860.390µs] or [-0.029%; +0.029%] None None None
flagevaluation_evp/payloads/typical/100flags_50users_10fields throughput [116226.599op/s; 116295.008op/s] or [-0.029%; +0.029%] None None None

Group 3

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz ac6320b 1793981605 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 266.806µs 272.640µs ± 11.977µs 269.655µs ± 1.335µs 271.713µs 286.294µs 320.071µs 392.301µs 45.48% 6.684 55.115 4.38% 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 [270.980µs; 274.300µs] or [-0.609%; +0.609%] None None None

Group 4

cpu_model git_commit_sha git_commit_date git_branch
Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz ac6320b 1793981605 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 163.309ns 167.448ns ± 3.208ns 166.681ns ± 2.162ns 168.935ns 174.292ns 175.512ns 180.208ns 8.12% 1.183 1.505 1.91% 0.227ns 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 [167.003ns; 167.892ns] or [-0.265%; +0.265%] 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 Outdated
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

Run the merged canonical dependent-flag suite through the trace API and verify selected-path exposure eligibility alongside evaluation records and errors. Document SDK finalization requirements.

Environment: Datadog workspace

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

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