Skip to content
Closed
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 @@ -23,3 +23,8 @@ serde_json = { workspace = true, features = ["std", "raw_value"] }
name = "ffe-eval"
harness = false
path = "benches/eval.rs"

[[bench]]
name = "dependent-flags"
harness = false
path = "benches/dependent_flags.rs"
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);
22 changes: 22 additions & 0 deletions libdd-ffe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,25 @@ 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. 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). 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, 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
after integrating and testing both evaluation and exposure reporting.
22 changes: 22 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 All @@ -27,6 +43,12 @@ pub enum EvaluationError {
#[error("targeting key is missing")]
TargetingKeyMissing,

/// The enabled experimentation opt-out parameter is not an RFC3339 string.
#[error(
"dd_experimentation_opt_out_at must be an RFC3339 timestamp string; supply a valid timestamp to opt out, or omit the attribute to permit experimentation"
)]
InvalidContext,

/// Failed to parse configuration. This should normally never happen and is likely a signal
/// that you should update SDK.
#[error("failed to parse configuration")]
Expand Down
Loading
Loading