Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions libdd-ffe-test-suite/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ publish = false
[lib]
bench = false

[[bench]]
name = "dependent-flags"
harness = false
path = "benches/dependent_flags.rs"

[dev-dependencies]
libdd-ffe = { path = "../libdd-ffe", features = ["flagevaluation-evp"] }
chrono = { workspace = true, features = ["now", "serde"] }
Expand Down
53 changes: 53 additions & 0 deletions libdd-ffe-test-suite/benches/dependent_flags.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

use criterion::{Criterion, criterion_group, criterion_main};
use libdd_ffe::rules_based::{
Configuration, EvaluationContext, ExpectedFlagType, UniversalFlagConfig,
};
use serde_json::json;
use std::hint::black_box;

fn bench_dependencies(c: &mut Criterion) {
let parent = json!({"key":"parent","enabled":true,"variationType":"BOOLEAN",
"variations":{"on":{"key":"on","value":true}},
"allocations":[{"key":"default","splits":[{"shards":[],"variationKey":"on"}]}]});
for (name, repeated) in [
("legacy", 0),
("one_prerequisite", 1),
("memoized_16_references", 16),
] {
let mut root = parent.clone();
root["key"] = json!("root");
if repeated > 0 {
root["allocations"][0]["rules"] = json!([{"conditions": (0..repeated).map(|_|
json!({"flagEvaluation":{"key":"parent"},"operator":"ONE_OF","value":["on"]})
).collect::<Vec<_>>()}]);
}
let config = Configuration::from_server_response(
UniversalFlagConfig::from_json(
serde_json::to_vec(
&json!({"createdAt":"2026-10-28T00:00:00Z","environment":{"name":"test"},
"flags":{"root":root,"parent":parent}}),
)
.unwrap(),
)
.unwrap(),
);
let context = EvaluationContext::new(Some("subject".into()), Default::default());
let now = chrono::Utc::now();
c.bench_function(&format!("dependent_flags/{name}"), |b| {
b.iter(|| {
black_box(config.eval_flag(
black_box("root"),
black_box(&context),
ExpectedFlagType::Boolean,
now,
))
.unwrap()
})
});
}
}
criterion_group!(benches, bench_dependencies);
criterion_main!(benches);
27 changes: 27 additions & 0 deletions libdd-ffe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,30 @@ This library change does not enable PHP EVP emission or change flag evaluation
results. PHP capture/admission/lifecycle integration and its end-to-end release
validation are separate work. Native matched-build IPC/HTTP tests are not a
substitute for PHP upgrade tests or Windows CI.

## Dependent flags

Rules may mix ordinary attribute conditions with `flagEvaluation` references. A reference
compares the prerequisite's selected **variant key**, using `ONE_OF` or `NOT_ONE_OF` with a
string array, including an empty array. References contain only a nonempty `key` and cannot
also specify an `attribute`. Conditions run in authored order and short-circuit. A mismatch
rejects the rule; a reached missing, disabled, malformed, or unassigned prerequisite fails
the root with `DependencyEvaluationFailed`, preserving its cause.

Each root evaluation uses one configuration, context, timestamp, and experimentation opt-out
policy. Completed prerequisites are memoized only within that invocation. Cycles fail;
`evaluatorParams.maxDependencyDepth` limits dependency edges (default 10, accepted range
0–255). The limit is validated only when a dependency condition executes. Invalid values,
including explicit `null`, fail that evaluation with `ConfigurationParseError` without
preventing configuration loading, independent evaluations, or skipped dependency conditions.
Cached results retain the deepest dependency visited, including rejected rules, so reuse
from a deeper path cannot bypass the limit.

`Configuration::eval_flag_with_trace` additionally returns unique prerequisite evaluation
records, including prerequisites rejected by the depth limit and excluding the requested root.
SDKs can log these evaluations even if a later condition rejects their rule. Only records
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.

after integrating and testing both evaluation and exposure reporting.
16 changes: 16 additions & 0 deletions libdd-ffe/src/rules_based/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ use crate::rules_based::{ExpectedFlagType, FlagType, Str};
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum EvaluationError {
/// A prerequisite failed. Its cause is preserved, while SDKs can distinguish this from
/// a directly requested flag's normal disabled/default result.
#[error("prerequisite {flag_key} failed: {source}")]
DependencyEvaluationFailed {
/// Referenced prerequisite key.
flag_key: Str,
/// Original prerequisite failure.
source: Box<EvaluationError>,
},
/// A reached prerequisite references a flag already being evaluated.
#[error("flag dependency cycle")]
DependencyCycle,

/// A reached prerequisite exceeds the configuration's dependency depth limit.
#[error("flag dependency depth exceeded")]
DependencyDepthExceeded,
/// Requested flag has unexpected type.
#[error("invalid flag type (expected: {expected:?}, found: {found:?})")]
TypeMismatch {
Expand Down
Loading