Skip to content
Merged
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
3 changes: 3 additions & 0 deletions examples/ffi/ffe.c
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ int main(int argc, char* argv[]) {
ddog_ffe_Handle_Configuration config = config_result.ok;
printf(" Configuration loaded successfully\n");

bool observe_full_evaluation_data = ddog_ffe_configuration_get_observe_full_evaluation_data(config);
printf(" observeFullEvaluationData: %s\n", observe_full_evaluation_data ? "true" : "false");

// Step 2: Create evaluation context with targeting key and attributes
printf("\nStep 2: Creating evaluation context...\n");

Expand Down
91 changes: 91 additions & 0 deletions libdd-ffe-ffi/src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,94 @@ pub unsafe extern "C" fn ddog_ffe_configuration_drop(config: *mut Handle<Configu
// SAFETY: the caller must ensure that config is a valid handle
unsafe { Handle::free(config) };
}

/// Get the config-level `observeFullEvaluationData` flag.
///
/// Opt-in boolean for emitting full flag evaluation data from SDKs to Datadog. Flag evaluation data
/// may contain PII; defaults to `false` for privacy.
///
/// # Safety
///
/// `config` must be a valid `Configuration` handle created by `ddog_ffe_configuration_new`.
#[no_mangle]
pub unsafe extern "C" fn ddog_ffe_configuration_get_observe_full_evaluation_data(
config: Handle<Configuration>,
) -> bool {
// SAFETY: the caller must ensure that config is a valid handle.
unsafe { config.as_ref() }.observe_full_evaluation_data()
Comment thread
vjfridge marked this conversation as resolved.
}

#[cfg(test)]
mod tests {
use super::*;

/// Builds a configuration handle through the real FFI entry point, reads the flag back through
/// the FFI getter, then releases the handle.
fn observe_full_evaluation_data_via_ffi(json: &str) -> bool {
// `json` outlives this call, so the borrowed pointer stays valid.
let borrowed = BorrowedStr {
ptr: json.as_ptr(),
len: json.len(),
};

// SAFETY: `borrowed` points to valid memory.
let result = unsafe { ddog_ffe_configuration_new(borrowed) };
let mut handle = match result {
Result::Ok(handle) => handle,
Result::Err(err) => panic!("configuration must parse: {err:?}"),
};

// The getter takes the handle by value, which C callers do without giving up ownership.
// `Handle` is `#[repr(transparent)]` and has no `Drop`, so a bitwise copy is sound here and
// lets us still drop the original exactly once below.
// SAFETY: `handle` was just created by `ddog_ffe_configuration_new`.
let handle_copy = unsafe { std::ptr::read(&handle) };

// SAFETY: `handle_copy` refers to the live configuration created above.
let observe =
unsafe { ddog_ffe_configuration_get_observe_full_evaluation_data(handle_copy) };

// SAFETY: `handle` is still valid and is freed exactly once.
unsafe { ddog_ffe_configuration_drop(&mut handle) };

observe
}

fn config_json(observe_field: &str) -> String {
format!(
r#"
{{
"createdAt": "2024-07-18T00:00:00Z",
"environment": {{ "name": "test" }}
{observe_field},
"flags": {{}}
}}
"#
)
}

#[test]
fn ffi_getter_returns_true_when_field_is_true() {
let json = config_json(r#", "observeFullEvaluationData": true"#);
assert!(observe_full_evaluation_data_via_ffi(&json));
}

#[test]
fn ffi_getter_returns_false_when_field_is_false() {
let json = config_json(r#", "observeFullEvaluationData": false"#);
assert!(!observe_full_evaluation_data_via_ffi(&json));
}

#[test]
fn ffi_getter_returns_false_when_field_is_absent() {
let json = config_json("");
assert!(!observe_full_evaluation_data_via_ffi(&json));
}

/// A malformed value must not fail configuration creation through the FFI boundary.
#[test]
fn ffi_getter_returns_false_when_field_is_malformed() {
let json = config_json(r#", "observeFullEvaluationData": null"#);
assert!(!observe_full_evaluation_data_via_ffi(&json));
}
}
40 changes: 40 additions & 0 deletions libdd-ffe/src/rules_based/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ impl Configuration {
&self.flags.compiled.environment.name
}

pub fn observe_full_evaluation_data(&self) -> bool {
self.flags.compiled.observe_full_evaluation_data
}

/// Returns an iterator over all flag keys. Note that this may return both disabled flags and
/// flags with bad configuration. Mostly useful for debugging.
pub fn flag_keys(&self) -> impl Iterator<Item = &Str> {
Expand All @@ -51,3 +55,39 @@ impl Configuration {
Some(Cow::Borrowed(self.flags.to_json()))
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn observe_full_evaluation_data_is_exposed_from_server_response() {
let json = br#"
{
"createdAt": "2024-07-18T00:00:00Z",
"environment": { "name": "test" },
"observeFullEvaluationData": true,
"flags": {}
}
"#;
let config = Configuration::from_server_response(
UniversalFlagConfig::from_json(json.to_vec()).unwrap(),
);
assert!(config.observe_full_evaluation_data());
}

#[test]
fn observe_full_evaluation_data_defaults_to_false_from_server_response() {
let json = br#"
{
"createdAt": "2024-07-18T00:00:00Z",
"environment": { "name": "test" },
"flags": {}
}
"#;
let config = Configuration::from_server_response(
UniversalFlagConfig::from_json(json.to_vec()).unwrap(),
);
assert!(!config.observe_full_evaluation_data());
}
}
4 changes: 4 additions & 0 deletions libdd-ffe/src/rules_based/ufc/compiled_flag_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ pub(crate) struct CompiledFlagsConfig {
pub created_at: Timestamp,
/// Environment this configuration belongs to.
pub environment: Environment,
/// Opt-in boolean for emitting full flag evaluation data from SDKs to Datadog. Flag evaluation
/// data may contain PII; defaults to `false` for privacy.
pub observe_full_evaluation_data: bool,
/// Flags configuration.
///
/// For flags that failed to parse or are disabled, we store the evaluation failure directly.
Expand Down Expand Up @@ -96,6 +99,7 @@ impl From<UniversalFlagConfigWire> for CompiledFlagsConfig {
CompiledFlagsConfig {
created_at: config.created_at,
environment: config.environment,
observe_full_evaluation_data: config.observe_full_evaluation_data,
flags,
}
}
Expand Down
86 changes: 86 additions & 0 deletions libdd-ffe/src/rules_based/ufc/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ pub(crate) struct UniversalFlagConfigWire {
pub created_at: Timestamp,
/// Environment this configuration belongs to.
pub environment: Environment,
/// Opt-in boolean for emitting full flag evaluation data from SDKs to Datadog. Flag evaluation
/// data may contain PII; defaults to `false` for privacy.
///
/// Deserialized leniently: an absent, `null`, or non-boolean value falls back to `false`
/// instead of failing the parse of the whole configuration.
#[serde(default, deserialize_with = "deserialize_lenient_bool")]
pub observe_full_evaluation_data: bool,
/// Flags configuration.
///
/// Value is wrapped in `TryParse` so that if we fail to parse one flag (e.g., new server
Expand All @@ -30,6 +37,20 @@ pub struct Environment {
pub name: Str,
}

/// Deserializes a boolean, falling back to `false` for any value that is not a JSON boolean.
///
/// Server-controlled optional booleans must never fail the parse of the whole configuration. A
/// `null` or mistyped value would otherwise discard every flag and force SDKs onto their defaults.
fn deserialize_lenient_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: serde::Deserializer<'de>,
{
// Deserialize to a generic `Value` first so that any JSON shape is consumed without error,
// then keep only a real boolean. Anything else becomes `false`.
let value = serde_json::Value::deserialize(deserializer)?;
Ok(value.as_bool().unwrap_or(false))
}

/// `TryParse` allows the subfield to fail parsing without failing the parsing of the whole
/// structure.
///
Expand Down Expand Up @@ -558,6 +579,71 @@ impl ShardRange {
mod tests {
use super::{TryParse, UniversalFlagConfigWire};

#[test]
fn observe_full_evaluation_data_defaults_to_false_when_absent() {
let ufc: UniversalFlagConfigWire = serde_json::from_str(
r#"
{
"createdAt": "2024-07-18T00:00:00Z",
"environment": { "name": "test" },
"flags": {}
}
"#,
)
.unwrap();
assert!(!ufc.observe_full_evaluation_data);
}

#[test]
fn observe_full_evaluation_data_parses_when_present() {
let ufc: UniversalFlagConfigWire = serde_json::from_str(
r#"
{
"createdAt": "2024-07-18T00:00:00Z",
"environment": { "name": "test" },
"observeFullEvaluationData": true,
"flags": {}
}
"#,
)
.unwrap();
assert!(ufc.observe_full_evaluation_data);
}

/// A malformed value for this field must never discard the rest of the configuration. Each
/// case must parse, fall back to `false`, and keep the sibling flag usable.
#[test]
fn observe_full_evaluation_data_falls_back_to_false_when_malformed() {
for malformed in [r#"null"#, r#""true""#, r#"1"#, r#"{}"#, r#"[]"#] {
let json = format!(
r#"
{{
"createdAt": "2024-07-18T00:00:00Z",
"environment": {{ "name": "test" }},
"observeFullEvaluationData": {malformed},
"flags": {{ "my_flag": {{
"key": "my_flag",
"enabled": true,
"variationType": "BOOLEAN",
"variations": {{}},
"allocations": []
}} }}
}}
"#
);
let ufc: UniversalFlagConfigWire = serde_json::from_str(&json)
.unwrap_or_else(|e| panic!("value {malformed} must not fail the parse: {e}"));
assert!(
!ufc.observe_full_evaluation_data,
"value {malformed} must fall back to false"
);
assert!(
ufc.flags.contains_key("my_flag"),
"value {malformed} must not discard sibling flags"
);
}
}

#[test]
fn parse_partially_if_unexpected() {
let ufc: UniversalFlagConfigWire = serde_json::from_str(
Expand Down
Loading