From 2b3f9e5cad2be4456376126ef0dad26dbfed4418 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 21 May 2026 21:58:02 -0400 Subject: [PATCH 01/14] feat(ffe): wire runtime-backed feature flags --- .github/CODEOWNERS | 7 + Cargo.lock | 16 + components-rs/Cargo.toml | 4 +- components-rs/common.h | 23 + components-rs/ddtrace.h | 46 +- components-rs/ffe.rs | 557 ++++++++++++++++++ components-rs/lib.rs | 1 + components-rs/remote_config.rs | 39 +- components-rs/sidecar.h | 5 + ext/configuration.h | 1 + ext/ddtrace.c | 197 +++++++ ext/ddtrace.stub.php | 74 +++ ext/ddtrace_arginfo.h | 52 ++ ext/sidecar.c | 35 +- libdatadog | 2 +- metadata/supported-configurations.json | 7 + src/api/FeatureFlags/Client.php | 247 ++++++++ src/api/FeatureFlags/EvaluationDetails.php | 111 ++++ src/api/FeatureFlags/EvaluationErrorCode.php | 29 + src/api/FeatureFlags/EvaluationReason.php | 31 + src/api/FeatureFlags/EvaluationType.php | 54 ++ src/api/FeatureFlags/Evaluator.php | 16 + src/api/FeatureFlags/ExposureWriter.php | 17 + src/api/FeatureFlags/MetricsRecorder.php | 12 + src/api/FeatureFlags/NativeEvaluator.php | 132 +++++ src/api/FeatureFlags/NativeExposureWriter.php | 126 ++++ src/api/FeatureFlags/NoopExposureWriter.php | 14 + src/api/FeatureFlags/NoopMetricsRecorder.php | 10 + src/api/FeatureFlags/RemoteConfigClient.php | 65 ++ src/api/FeatureFlags/ResultMapper.php | 278 +++++++++ .../TriggerErrorWarningEmitter.php | 11 + src/api/FeatureFlags/UnavailableEvaluator.php | 28 + src/api/FeatureFlags/WarningEmitter.php | 12 + tests/api/Unit/FeatureFlags/ClientTest.php | 245 ++++++++ .../Unit/FeatureFlags/ExposureWriterTest.php | 146 +++++ .../FeatureFlags/RemoteConfigClientTest.php | 55 ++ .../Unit/FeatureFlags/ResultMapperTest.php | 162 +++++ tests/ext/ffe/flush_drains_buffer.phpt | 49 ++ tests/ext/ffe/fork_resets_dedup.phpt | 52 ++ tests/ext/ffe/native_bridge_evaluate.phpt | 67 +++ tests/ext/ffe/remote_config_lifecycle.phpt | 90 +++ tests/internal-api-stress-test.php | 7 +- 42 files changed, 3119 insertions(+), 13 deletions(-) create mode 100644 components-rs/ffe.rs create mode 100644 src/api/FeatureFlags/Client.php create mode 100644 src/api/FeatureFlags/EvaluationDetails.php create mode 100644 src/api/FeatureFlags/EvaluationErrorCode.php create mode 100644 src/api/FeatureFlags/EvaluationReason.php create mode 100644 src/api/FeatureFlags/EvaluationType.php create mode 100644 src/api/FeatureFlags/Evaluator.php create mode 100644 src/api/FeatureFlags/ExposureWriter.php create mode 100644 src/api/FeatureFlags/MetricsRecorder.php create mode 100644 src/api/FeatureFlags/NativeEvaluator.php create mode 100644 src/api/FeatureFlags/NativeExposureWriter.php create mode 100644 src/api/FeatureFlags/NoopExposureWriter.php create mode 100644 src/api/FeatureFlags/NoopMetricsRecorder.php create mode 100644 src/api/FeatureFlags/RemoteConfigClient.php create mode 100644 src/api/FeatureFlags/ResultMapper.php create mode 100644 src/api/FeatureFlags/TriggerErrorWarningEmitter.php create mode 100644 src/api/FeatureFlags/UnavailableEvaluator.php create mode 100644 src/api/FeatureFlags/WarningEmitter.php create mode 100644 tests/api/Unit/FeatureFlags/ClientTest.php create mode 100644 tests/api/Unit/FeatureFlags/ExposureWriterTest.php create mode 100644 tests/api/Unit/FeatureFlags/RemoteConfigClientTest.php create mode 100644 tests/api/Unit/FeatureFlags/ResultMapperTest.php create mode 100644 tests/ext/ffe/flush_drains_buffer.phpt create mode 100644 tests/ext/ffe/fork_resets_dedup.phpt create mode 100644 tests/ext/ffe/native_bridge_evaluate.phpt create mode 100644 tests/ext/ffe/remote_config_lifecycle.phpt diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 90b79c23e7a..bd705e8c881 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,6 +14,13 @@ compile_rust.sh @Datadog/libdatadog-apm # APM IDM Team /src/ @DataDog/apm-idm-php +# FFE (Feature Flagging & Experimentation) SDK Team +/components-rs/ffe.rs @Datadog/libdatadog-apm @DataDog/feature-flagging-and-experimentation-sdk +/libdatadog @Datadog/libdatadog-apm @DataDog/feature-flagging-and-experimentation-sdk +/src/api/FeatureFlags/ @DataDog/feature-flagging-and-experimentation-sdk +/tests/api/Unit/FeatureFlags/ @DataDog/feature-flagging-and-experimentation-sdk +/tests/ext/ffe/ @DataDog/feature-flagging-and-experimentation-sdk + # Release files Cargo.lock @DataDog/apm-php @DataDog/profiling-php @Datadog/libdatadog-apm package.xml @DataDog/apm-php @DataDog/profiling-php @Datadog/asm-php diff --git a/Cargo.lock b/Cargo.lock index db76b71b6bc..ce7aba7a65f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1451,6 +1451,7 @@ dependencies = [ "bincode", "cbindgen 0.27.0", "const-str", + "datadog-ffe", "datadog-ipc", "datadog-live-debugger", "datadog-live-debugger-ffi", @@ -1474,6 +1475,7 @@ dependencies = [ "libdd-trace-stats", "libdd-trace-utils", "log", + "lru", "paste", "regex", "regex-automata", @@ -2081,6 +2083,11 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "hdrhistogram" @@ -3286,6 +3293,15 @@ dependencies = [ "value-bag", ] +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "mach2" version = "0.5.0" diff --git a/components-rs/Cargo.toml b/components-rs/Cargo.toml index a6103bcde96..264348ab8a6 100644 --- a/components-rs/Cargo.toml +++ b/components-rs/Cargo.toml @@ -15,7 +15,8 @@ libdd-telemetry-ffi = { path = "../libdatadog/libdd-telemetry-ffi", default-feat datadog-live-debugger = { path = "../libdatadog/datadog-live-debugger" } datadog-live-debugger-ffi = { path = "../libdatadog/datadog-live-debugger-ffi", default-features = false } datadog-ipc = { path = "../libdatadog/datadog-ipc" } -datadog-remote-config = { path = "../libdatadog/datadog-remote-config" } +datadog-ffe = { path = "../libdatadog/datadog-ffe" } +datadog-remote-config = { path = "../libdatadog/datadog-remote-config", features = ["ffe"] } datadog-sidecar = { path = "../libdatadog/datadog-sidecar" } datadog-sidecar-ffi = { path = "../libdatadog/datadog-sidecar-ffi" } libdd-data-pipeline = { path = "../libdatadog/libdd-data-pipeline" } @@ -32,6 +33,7 @@ serde = "1.0.196" simd-json = "0.14.1" serde_with = "3.6.0" lazy_static = "1.4" +lru = "0.16.4" log = "0.4.20" env_logger = "0.10.1" zwohash = "0.1.2" diff --git a/components-rs/common.h b/components-rs/common.h index f5a6d674c0a..adf46fad3ec 100644 --- a/components-rs/common.h +++ b/components-rs/common.h @@ -439,6 +439,8 @@ typedef struct ddog_DebuggerPayload ddog_DebuggerPayload; typedef struct ddog_DslString ddog_DslString; +typedef struct ddog_FfeResult ddog_FfeResult; + typedef struct ddog_HashMap_ShmCacheKey__ShmCache ddog_HashMap_ShmCacheKey__ShmCache; /** @@ -478,6 +480,19 @@ typedef struct ddog_SidecarTransport ddog_SidecarTransport; */ typedef struct ddog_SpanConcentrator ddog_SpanConcentrator; +/** + * Flags selecting which Remote Config products/capabilities to subscribe to. + * + * Passed as a single C-ABI struct so call sites can use designated initializers + * and name the flags, instead of a positional sequence of bool args. + */ +typedef struct ddog_DdogRemoteConfigFlags { + bool live_debugging_enabled; + bool appsec_activation; + bool appsec_config; + bool ffe_enabled; +} ddog_DdogRemoteConfigFlags; + /** * Holds the raw parts of a Rust Vec; it should only be created from Rust, * never from C. @@ -679,6 +694,14 @@ typedef struct ddog_Vec_DebuggerPayload { */ typedef uint64_t ddog_QueueId; +typedef struct ddog_FfeAttribute { + const char *key; + int32_t value_type; + const char *string_value; + double number_value; + bool bool_value; +} ddog_FfeAttribute; + /** * A (key, value) pair for peer-service tags, borrowed from PHP/concentrator memory. */ diff --git a/components-rs/ddtrace.h b/components-rs/ddtrace.h index 7c575e34319..a4410f50bac 100644 --- a/components-rs/ddtrace.h +++ b/components-rs/ddtrace.h @@ -61,6 +61,48 @@ int posix_spawn_file_actions_addchdir_np(void *file_actions, const char *path); uint64_t dd_fnv1a_64(const uint8_t *data, uintptr_t len); +bool ddog_ffe_load_config(const char *json); + +bool ddog_ffe_has_config(void); + +uint64_t ddog_ffe_config_version(void); + +struct ddog_FfeResult *ddog_ffe_evaluate(const char *flag_key, + int32_t expected_type, + const char *targeting_key, + const struct ddog_FfeAttribute *attributes, + uintptr_t attributes_count); + +const char *ddog_ffe_result_value(const struct ddog_FfeResult *result); + +const char *ddog_ffe_result_variant(const struct ddog_FfeResult *result); + +const char *ddog_ffe_result_allocation_key(const struct ddog_FfeResult *result); + +int32_t ddog_ffe_result_reason(const struct ddog_FfeResult *result); + +int32_t ddog_ffe_result_error_code(const struct ddog_FfeResult *result); + +bool ddog_ffe_result_do_log(const struct ddog_FfeResult *result); + +void ddog_ffe_free_result(struct ddog_FfeResult *result); + +void ddog_ffe_set_service_context(const char *service, + const char *env, + const char *version); + +bool ddog_ffe_enqueue_exposure(const char *event_json, + const char *flag_key, + const char *allocation_key, + const char *targeting_key, + const char *variant_key); + +ddog_CharSlice ddog_ffe_flush_exposures(void); + +void ddog_ffe_free_flush_result(ddog_CharSlice slice); + +void ddog_ffe_reset_exposure_state(void); + const char *ddog_normalize_process_tag_value(ddog_CharSlice tag_value); void ddog_free_normalized_tag_value(const char *ptr); @@ -118,9 +160,7 @@ void ddog_reset_logger(void); uint32_t ddog_get_logs_count(ddog_CharSlice level); -void ddog_init_remote_config(bool live_debugging_enabled, - bool appsec_activation, - bool appsec_config); +void ddog_init_remote_config(struct ddog_DdogRemoteConfigFlags flags); struct ddog_RemoteConfigState *ddog_init_remote_config_state(const struct ddog_Endpoint *endpoint, bool di_enabled); diff --git a/components-rs/ffe.rs b/components-rs/ffe.rs new file mode 100644 index 00000000000..9fee4a176f2 --- /dev/null +++ b/components-rs/ffe.rs @@ -0,0 +1,557 @@ +use datadog_ffe::rules_based::{ + self as ffe, AssignmentReason, AssignmentValue, Attribute, Configuration, EvaluationContext, + EvaluationError, ExpectedFlagType, Str, UniversalFlagConfig, +}; +use libdd_common_ffi::CharSlice; +use lru::LruCache; +use std::collections::HashMap; +use std::ffi::{c_char, CStr, CString}; +use std::num::NonZeroUsize; +use std::sync::{Arc, Mutex}; + +struct FfeState { + config: Option, + version: u64, +} + +lazy_static::lazy_static! { + static ref FFE_STATE: Mutex = Mutex::new(FfeState { + config: None, + version: 0, + }); +} + +pub fn store_config(config: Configuration) { + if let Ok(mut state) = FFE_STATE.lock() { + state.config = Some(config); + state.version = state.version.wrapping_add(1); + } +} + +pub fn clear_config() { + if let Ok(mut state) = FFE_STATE.lock() { + state.config = None; + state.version = state.version.wrapping_add(1); + } +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_load_config(json: *const c_char) -> bool { + if json.is_null() { + return false; + } + + let json = match unsafe { CStr::from_ptr(json) }.to_str() { + Ok(json) => json, + Err(_) => return false, + }; + + match UniversalFlagConfig::from_json(json.as_bytes().to_vec()) { + Ok(ufc) => { + store_config(Configuration::from_server_response(ufc)); + true + } + Err(_) => false, + } +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_has_config() -> bool { + FFE_STATE + .lock() + .map(|state| state.config.is_some()) + .unwrap_or(false) +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_config_version() -> u64 { + FFE_STATE.lock().map(|state| state.version).unwrap_or(0) +} + +const REASON_STATIC: i32 = 0; +const REASON_DEFAULT: i32 = 1; +const REASON_TARGETING_MATCH: i32 = 2; +const REASON_SPLIT: i32 = 3; +const REASON_DISABLED: i32 = 4; +const REASON_ERROR: i32 = 5; + +const ERROR_NONE: i32 = 0; +const ERROR_TYPE_MISMATCH: i32 = 1; +const ERROR_CONFIG_PARSE: i32 = 2; +const ERROR_FLAG_UNRECOGNIZED: i32 = 3; +const ERROR_CONFIG_MISSING: i32 = 6; +const ERROR_GENERAL: i32 = 7; + +const ATTR_TYPE_STRING: i32 = 0; +const ATTR_TYPE_NUMBER: i32 = 1; +const ATTR_TYPE_BOOL: i32 = 2; + +const TYPE_STRING: i32 = 0; +const TYPE_INTEGER: i32 = 1; +const TYPE_FLOAT: i32 = 2; +const TYPE_BOOLEAN: i32 = 3; +const TYPE_OBJECT: i32 = 4; + +pub struct FfeResult { + pub value_json: CString, + pub variant: Option, + pub allocation_key: Option, + pub reason: i32, + pub error_code: i32, + pub do_log: bool, +} + +#[repr(C)] +pub struct FfeAttribute { + pub key: *const c_char, + pub value_type: i32, + pub string_value: *const c_char, + pub number_value: f64, + pub bool_value: bool, +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_evaluate( + flag_key: *const c_char, + expected_type: i32, + targeting_key: *const c_char, + attributes: *const FfeAttribute, + attributes_count: usize, +) -> *mut FfeResult { + if flag_key.is_null() { + return std::ptr::null_mut(); + } + + let flag_key = match unsafe { CStr::from_ptr(flag_key) }.to_str() { + Ok(flag_key) => flag_key, + Err(_) => return std::ptr::null_mut(), + }; + + let expected_type = match expected_type { + TYPE_STRING => ExpectedFlagType::String, + TYPE_INTEGER => ExpectedFlagType::Integer, + TYPE_FLOAT => ExpectedFlagType::Float, + TYPE_BOOLEAN => ExpectedFlagType::Boolean, + TYPE_OBJECT => ExpectedFlagType::Object, + _ => return std::ptr::null_mut(), + }; + + let targeting_key = if targeting_key.is_null() { + None + } else { + match unsafe { CStr::from_ptr(targeting_key) }.to_str() { + Ok(targeting_key) if !targeting_key.is_empty() => Some(Str::from(targeting_key)), + _ => None, + } + }; + + let attributes = parse_attributes(attributes, attributes_count); + let context = EvaluationContext::new(targeting_key, Arc::new(attributes)); + + let state = match FFE_STATE.lock() { + Ok(state) => state, + Err(_) => return std::ptr::null_mut(), + }; + + let assignment = ffe::get_assignment( + state.config.as_ref(), + flag_key, + &context, + expected_type, + ffe::now(), + ); + + Box::into_raw(Box::new(result_from_assignment(assignment))) +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_result_value(result: *const FfeResult) -> *const c_char { + if result.is_null() { + return std::ptr::null(); + } + + unsafe { &*result }.value_json.as_ptr() +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_result_variant(result: *const FfeResult) -> *const c_char { + if result.is_null() { + return std::ptr::null(); + } + + unsafe { &*result } + .variant + .as_ref() + .map(|value| value.as_ptr()) + .unwrap_or(std::ptr::null()) +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_result_allocation_key(result: *const FfeResult) -> *const c_char { + if result.is_null() { + return std::ptr::null(); + } + + unsafe { &*result } + .allocation_key + .as_ref() + .map(|value| value.as_ptr()) + .unwrap_or(std::ptr::null()) +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_result_reason(result: *const FfeResult) -> i32 { + if result.is_null() { + return REASON_ERROR; + } + + unsafe { &*result }.reason +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_result_error_code(result: *const FfeResult) -> i32 { + if result.is_null() { + return ERROR_GENERAL; + } + + unsafe { &*result }.error_code +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_result_do_log(result: *const FfeResult) -> bool { + if result.is_null() { + return false; + } + + unsafe { &*result }.do_log +} + +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_free_result(result: *mut FfeResult) { + if !result.is_null() { + drop(Box::from_raw(result)); + } +} + +fn parse_attributes( + attributes: *const FfeAttribute, + attributes_count: usize, +) -> HashMap { + let mut parsed = HashMap::new(); + + if attributes.is_null() || attributes_count == 0 { + return parsed; + } + + let attributes = unsafe { std::slice::from_raw_parts(attributes, attributes_count) }; + for attribute in attributes { + if attribute.key.is_null() { + continue; + } + + let key = match unsafe { CStr::from_ptr(attribute.key) }.to_str() { + Ok(key) => key, + Err(_) => continue, + }; + + let value = match attribute.value_type { + ATTR_TYPE_STRING => { + if attribute.string_value.is_null() { + continue; + } + + match unsafe { CStr::from_ptr(attribute.string_value) }.to_str() { + Ok(value) => Attribute::from(value), + Err(_) => continue, + } + } + ATTR_TYPE_NUMBER => Attribute::from(attribute.number_value), + ATTR_TYPE_BOOL => Attribute::from(attribute.bool_value), + _ => continue, + }; + + parsed.insert(Str::from(key), value); + } + + parsed +} + +fn result_from_assignment(assignment: Result) -> FfeResult { + match assignment { + Ok(assignment) => FfeResult { + value_json: string_to_cstring(assignment_value_to_json(&assignment.value)), + variant: Some(string_to_cstring( + assignment.variation_key.as_str().to_string(), + )), + allocation_key: Some(string_to_cstring( + assignment.allocation_key.as_str().to_string(), + )), + reason: match assignment.reason { + AssignmentReason::Static => REASON_STATIC, + AssignmentReason::TargetingMatch => REASON_TARGETING_MATCH, + AssignmentReason::Split => REASON_SPLIT, + }, + error_code: ERROR_NONE, + do_log: assignment.do_log, + }, + Err(error) => { + let (error_code, reason) = match &error { + EvaluationError::TypeMismatch { .. } => (ERROR_TYPE_MISMATCH, REASON_ERROR), + EvaluationError::ConfigurationParseError => (ERROR_CONFIG_PARSE, REASON_ERROR), + EvaluationError::ConfigurationMissing => (ERROR_CONFIG_MISSING, REASON_ERROR), + EvaluationError::FlagUnrecognizedOrDisabled => { + (ERROR_FLAG_UNRECOGNIZED, REASON_DEFAULT) + } + EvaluationError::FlagDisabled => (ERROR_NONE, REASON_DISABLED), + EvaluationError::DefaultAllocationNull => (ERROR_NONE, REASON_DEFAULT), + _ => (ERROR_GENERAL, REASON_ERROR), + }; + + FfeResult { + value_json: string_to_cstring("null".to_string()), + variant: None, + allocation_key: None, + reason, + error_code, + do_log: false, + } + } + } +} + +fn assignment_value_to_json(value: &AssignmentValue) -> String { + match value { + AssignmentValue::String(value) => serde_json::to_string(value.as_str()).unwrap_or_default(), + AssignmentValue::Integer(value) => value.to_string(), + AssignmentValue::Float(value) => serde_json::Number::from_f64(*value) + .map(|value| value.to_string()) + .unwrap_or_else(|| value.to_string()), + AssignmentValue::Boolean(value) => value.to_string(), + AssignmentValue::Json { raw, .. } => raw.get().to_string(), + } +} + +fn string_to_cstring(value: String) -> CString { + CString::new(value).unwrap_or_default() +} + +struct ServiceContext { + service: String, + env: String, + version: String, +} + +struct ExposureState { + dedup_cache: LruCache<(String, String), (String, String)>, + batch_buffer: Vec, + service_context: Option, +} + +const EXPOSURE_DEDUP_LIMIT: usize = 65_536; +const EXPOSURE_BATCH_LIMIT: usize = 1_000; + +lazy_static::lazy_static! { + static ref EXPOSURE_STATE: Mutex = Mutex::new(ExposureState { + dedup_cache: LruCache::new(NonZeroUsize::new(EXPOSURE_DEDUP_LIMIT).unwrap()), + batch_buffer: Vec::new(), + service_context: None, + }); +} + +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_set_service_context( + service: *const c_char, + env: *const c_char, + version: *const c_char, +) { + if let Ok(mut state) = EXPOSURE_STATE.lock() { + state.service_context = Some(ServiceContext { + service: optional_cstr_to_string(service), + env: optional_cstr_to_string(env), + version: optional_cstr_to_string(version), + }); + } +} + +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_enqueue_exposure( + event_json: *const c_char, + flag_key: *const c_char, + allocation_key: *const c_char, + targeting_key: *const c_char, + variant_key: *const c_char, +) -> bool { + if event_json.is_null() || flag_key.is_null() || variant_key.is_null() { + return false; + } + + let event = match required_cstr_to_string(event_json) { + Some(event) => event, + None => return false, + }; + let flag = match required_cstr_to_string(flag_key) { + Some(flag) => flag, + None => return false, + }; + let variant = match required_cstr_to_string(variant_key) { + Some(variant) => variant, + None => return false, + }; + let allocation = optional_cstr_to_string(allocation_key); + let targeting = optional_cstr_to_string(targeting_key); + + let dedup_key = (flag, targeting); + let dedup_value = (allocation, variant); + + if let Ok(mut state) = EXPOSURE_STATE.lock() { + if let Some(cached) = state.dedup_cache.get(&dedup_key) { + if *cached == dedup_value { + return false; + } + } + + state.dedup_cache.put(dedup_key, dedup_value); + if state.batch_buffer.len() >= EXPOSURE_BATCH_LIMIT { + return false; + } + + state.batch_buffer.push(event); + return true; + } + + false +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_flush_exposures() -> CharSlice<'static> { + if let Ok(mut state) = EXPOSURE_STATE.lock() { + if state.batch_buffer.is_empty() { + return CharSlice::default(); + } + + let events = state.batch_buffer.drain(..).collect::>(); + let context = match &state.service_context { + Some(context) => serde_json::json!({ + "service": context.service.as_str(), + "env": context.env.as_str(), + "version": context.version.as_str(), + }), + None => serde_json::json!({ + "service": "", + "env": "", + "version": "", + }), + }; + + let payload = format!( + r#"{{"context":{},"exposures":[{}]}}"#, + context, + events.join(",") + ); + let mut bytes = payload.into_bytes().into_boxed_slice(); + let ptr = bytes.as_mut_ptr(); + let len = bytes.len(); + std::mem::forget(bytes); + + return unsafe { CharSlice::from_raw_parts(ptr as *const c_char, len) }; + } + + CharSlice::default() +} + +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_free_flush_result(slice: CharSlice<'static>) { + use libdd_common_ffi::slice::AsBytes; + + let bytes = slice.as_bytes(); + let len = bytes.len(); + let ptr = bytes.as_ptr() as *mut u8; + if !ptr.is_null() && len > 0 { + let _ = Box::from_raw(std::slice::from_raw_parts_mut(ptr, len) as *mut [u8]); + } +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_reset_exposure_state() { + if let Ok(mut state) = EXPOSURE_STATE.lock() { + state.dedup_cache.clear(); + state.batch_buffer.clear(); + state.service_context = None; + } +} + +unsafe fn required_cstr_to_string(value: *const c_char) -> Option { + CStr::from_ptr(value) + .to_str() + .ok() + .map(|value| value.to_string()) +} + +unsafe fn optional_cstr_to_string(value: *const c_char) -> String { + if value.is_null() { + return String::new(); + } + + required_cstr_to_string(value).unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + use libdd_common_ffi::slice::AsBytes; + + #[test] + fn exposure_flush_drains_buffer_and_keeps_context() { + ddog_ffe_reset_exposure_state(); + + let service = CString::new("svc-flush").unwrap(); + let env = CString::new("test").unwrap(); + let version = CString::new("1.2.3").unwrap(); + let event = CString::new( + r#"{"timestamp":1,"flag":{"key":"demo"},"allocation":{"key":"alloc-a"},"variant":{"key":"on"},"subject":{"id":"user-1","attributes":{}}}"#, + ) + .unwrap(); + let flag = CString::new("demo").unwrap(); + let allocation = CString::new("alloc-a").unwrap(); + let targeting = CString::new("user-1").unwrap(); + let on = CString::new("on").unwrap(); + let off = CString::new("off").unwrap(); + + unsafe { + ddog_ffe_set_service_context(service.as_ptr(), env.as_ptr(), version.as_ptr()); + assert!(ddog_ffe_enqueue_exposure( + event.as_ptr(), + flag.as_ptr(), + allocation.as_ptr(), + targeting.as_ptr(), + on.as_ptr(), + )); + assert!(!ddog_ffe_enqueue_exposure( + event.as_ptr(), + flag.as_ptr(), + allocation.as_ptr(), + targeting.as_ptr(), + on.as_ptr(), + )); + assert!(ddog_ffe_enqueue_exposure( + event.as_ptr(), + flag.as_ptr(), + allocation.as_ptr(), + targeting.as_ptr(), + off.as_ptr(), + )); + } + + let payload = ddog_ffe_flush_exposures(); + assert!(!payload.as_bytes().is_empty()); + let decoded: serde_json::Value = serde_json::from_slice(payload.as_bytes()).unwrap(); + assert_eq!(decoded["context"]["service"], "svc-flush"); + assert_eq!(decoded["context"]["env"], "test"); + assert_eq!(decoded["context"]["version"], "1.2.3"); + assert_eq!(decoded["exposures"].as_array().unwrap().len(), 2); + unsafe { ddog_ffe_free_flush_result(payload) }; + + let empty = ddog_ffe_flush_exposures(); + assert!(empty.as_bytes().is_empty()); + } +} diff --git a/components-rs/lib.rs b/components-rs/lib.rs index bf9a2675ff2..560715ff05c 100644 --- a/components-rs/lib.rs +++ b/components-rs/lib.rs @@ -4,6 +4,7 @@ #![allow(static_mut_refs)] // remove with move to Rust 2024 edition pub mod agent_info; +pub mod ffe; pub mod log; pub mod remote_config; pub mod sidecar; diff --git a/components-rs/remote_config.rs b/components-rs/remote_config.rs index 515aede6f36..fc0ec34fb7e 100644 --- a/components-rs/remote_config.rs +++ b/components-rs/remote_config.rs @@ -1,4 +1,5 @@ use crate::sidecar::MaybeShmLimiter; +use datadog_ffe::rules_based::Configuration; use datadog_live_debugger::debugger_defs::{DebuggerData, DebuggerPayload}; use datadog_live_debugger::{FilterList, LiveDebuggingData, ServiceConfiguration}; use datadog_live_debugger_ffi::data::Probe; @@ -116,13 +117,28 @@ pub struct LiveDebuggerState { pub di_enabled: bool, } +/// Flags selecting which Remote Config products/capabilities to subscribe to. +/// +/// Passed as a single C-ABI struct so call sites can use designated initializers +/// and name the flags, instead of a positional sequence of bool args. +#[repr(C)] +pub struct DdogRemoteConfigFlags { + pub live_debugging_enabled: bool, + pub appsec_activation: bool, + pub appsec_config: bool, + pub ffe_enabled: bool, +} + #[no_mangle] #[allow(static_mut_refs)] -pub unsafe extern "C" fn ddog_init_remote_config( - live_debugging_enabled: bool, - appsec_activation: bool, - appsec_config: bool, -) { +pub unsafe extern "C" fn ddog_init_remote_config(flags: DdogRemoteConfigFlags) { + let DdogRemoteConfigFlags { + live_debugging_enabled, + appsec_activation, + appsec_config, + ffe_enabled, + } = flags; + DDTRACE_REMOTE_CONFIG_PRODUCTS.push(RemoteConfigProduct::ApmTracing); DDTRACE_REMOTE_CONFIG_CAPABILITIES.push(RemoteConfigCapabilities::ApmTracingCustomTags); DDTRACE_REMOTE_CONFIG_CAPABILITIES.push(RemoteConfigCapabilities::ApmTracingEnabled); @@ -139,6 +155,11 @@ pub unsafe extern "C" fn ddog_init_remote_config( DDTRACE_REMOTE_CONFIG_CAPABILITIES.push(RemoteConfigCapabilities::AsmActivation); } + if ffe_enabled { + DDTRACE_REMOTE_CONFIG_PRODUCTS.push(RemoteConfigProduct::FfeFlags); + DDTRACE_REMOTE_CONFIG_CAPABILITIES.push(RemoteConfigCapabilities::FfeFlagConfigurationRules); + } + if live_debugging_enabled { DDTRACE_REMOTE_CONFIG_PRODUCTS.push(RemoteConfigProduct::LiveDebugger) } @@ -377,6 +398,10 @@ pub extern "C" fn ddog_process_remote_configs(remote_config: &mut RemoteConfigSt ); } } + RemoteConfigData::FfeFlags(ufc) => { + debug!("Received FFE flags configuration"); + crate::ffe::store_config(Configuration::from_server_response(ufc)); + } RemoteConfigData::Ignored(_) => (), RemoteConfigData::TracerFlareConfig(_) => {} RemoteConfigData::TracerFlareTask(_) => {} @@ -402,6 +427,10 @@ pub extern "C" fn ddog_process_remote_configs(remote_config: &mut RemoteConfigSt } } } + RemoteConfigProduct::FfeFlags => { + debug!("FFE flags configuration removed"); + crate::ffe::clear_config(); + } _ => (), }, } diff --git a/components-rs/sidecar.h b/components-rs/sidecar.h index 4a3aa617416..45fb1f8e846 100644 --- a/components-rs/sidecar.h +++ b/components-rs/sidecar.h @@ -298,6 +298,11 @@ ddog_MaybeError ddog_sidecar_send_debugger_datum(struct ddog_SidecarTransport ** ddog_QueueId queue_id, struct ddog_DebuggerPayload *payload); +ddog_MaybeError ddog_sidecar_send_ffe_exposures(struct ddog_SidecarTransport **transport, + const struct ddog_InstanceId *instance_id, + const ddog_QueueId *queue_id, + ddog_CharSlice payload); + ddog_MaybeError ddog_sidecar_send_debugger_diagnostics(struct ddog_SidecarTransport **transport, const struct ddog_InstanceId *instance_id, ddog_QueueId queue_id, diff --git a/ext/configuration.h b/ext/configuration.h index 1c95a47abda..0fafcc56b1a 100644 --- a/ext/configuration.h +++ b/ext/configuration.h @@ -278,6 +278,7 @@ enum ddtrace_sidecar_connection_mode { CONFIG(INT, DD_CODE_ORIGIN_MAX_USER_FRAMES, "8") \ CONFIG(BOOL, DD_TRACE_RESOURCE_RENAMING_ENABLED, "false") \ CONFIG(BOOL, DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, "false") \ + CONFIG(BOOL, DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED, "false") \ CONFIG(BOOL, DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED, "true") \ CONFIG(BOOL, DD_TRACE_STATS_COMPUTATION_ENABLED, "false") \ DD_INTEGRATIONS diff --git a/ext/ddtrace.c b/ext/ddtrace.c index 625c9cfa03b..3362c0e003f 100644 --- a/ext/ddtrace.c +++ b/ext/ddtrace.c @@ -2951,6 +2951,203 @@ PHP_FUNCTION(DDTrace_flush_endpoints) { ddog_sidecar_telemetry_filter_flush(&DDTRACE_G(sidecar), ddtrace_sidecar_instance_id, &DDTRACE_G(sidecar_queue_id), ddtrace_telemetry_buffer(), ddtrace_telemetry_cache(), service_name, env_name)); } +PHP_FUNCTION(DDTrace_ffe_has_config) { + ZEND_PARSE_PARAMETERS_NONE(); + + RETURN_BOOL(ddog_ffe_has_config()); +} + +PHP_FUNCTION(DDTrace_ffe_config_version) { + ZEND_PARSE_PARAMETERS_NONE(); + + RETURN_LONG((zend_long) ddog_ffe_config_version()); +} + +PHP_FUNCTION(DDTrace_ffe_load_config) { + char *json; + size_t json_len; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STRING(json, json_len) + ZEND_PARSE_PARAMETERS_END(); + + UNUSED(json_len); + RETURN_BOOL(ddog_ffe_load_config(json)); +} + +PHP_FUNCTION(DDTrace_ffe_evaluate) { + char *flag_key; + size_t flag_key_len; + zend_long type_id_zl; + char *targeting_key = NULL; + size_t targeting_key_len = 0; + zval *attrs_zv; + int32_t type_id; + struct ddog_FfeAttribute *c_attrs = NULL; + size_t attrs_count = 0; + const char *tk = NULL; + HashTable *attributes; + size_t idx = 0; + zend_string *key; + zval *value; + struct ddog_FfeResult *result; + const char *value_json; + const char *variant; + const char *allocation_key; + + ZEND_PARSE_PARAMETERS_START(4, 4) + Z_PARAM_STRING(flag_key, flag_key_len) + Z_PARAM_LONG(type_id_zl) + Z_PARAM_STRING_OR_NULL(targeting_key, targeting_key_len) + Z_PARAM_ARRAY(attrs_zv) + ZEND_PARSE_PARAMETERS_END(); + + UNUSED(flag_key_len); + + type_id = (int32_t) type_id_zl; + tk = targeting_key_len > 0 ? targeting_key : NULL; + attributes = Z_ARRVAL_P(attrs_zv); + attrs_count = zend_hash_num_elements(attributes); + + if (attrs_count > 0) { + c_attrs = ecalloc(attrs_count, sizeof(struct ddog_FfeAttribute)); + ZEND_HASH_FOREACH_STR_KEY_VAL(attributes, key, value) { + if (!key || idx >= attrs_count) { + continue; + } + + c_attrs[idx].key = ZSTR_VAL(key); + switch (Z_TYPE_P(value)) { + case IS_STRING: + c_attrs[idx].value_type = 0; + c_attrs[idx].string_value = Z_STRVAL_P(value); + break; + case IS_LONG: + c_attrs[idx].value_type = 1; + c_attrs[idx].number_value = (double) Z_LVAL_P(value); + break; + case IS_DOUBLE: + c_attrs[idx].value_type = 1; + c_attrs[idx].number_value = Z_DVAL_P(value); + break; + case IS_TRUE: + c_attrs[idx].value_type = 2; + c_attrs[idx].bool_value = true; + break; + case IS_FALSE: + c_attrs[idx].value_type = 2; + c_attrs[idx].bool_value = false; + break; + default: + continue; + } + + idx++; + } ZEND_HASH_FOREACH_END(); + attrs_count = idx; + } + + result = ddog_ffe_evaluate(flag_key, type_id, tk, c_attrs, attrs_count); + if (c_attrs) { + efree(c_attrs); + } + + if (!result) { + RETURN_NULL(); + } + + value_json = ddog_ffe_result_value(result); + variant = ddog_ffe_result_variant(result); + allocation_key = ddog_ffe_result_allocation_key(result); + + array_init(return_value); + if (value_json) { + add_assoc_string(return_value, "value_json", (char *) value_json); + } else { + add_assoc_null(return_value, "value_json"); + } + if (variant) { + add_assoc_string(return_value, "variant", (char *) variant); + } else { + add_assoc_null(return_value, "variant"); + } + if (allocation_key) { + add_assoc_string(return_value, "allocation_key", (char *) allocation_key); + } else { + add_assoc_null(return_value, "allocation_key"); + } + add_assoc_long(return_value, "reason", ddog_ffe_result_reason(result)); + add_assoc_long(return_value, "error_code", ddog_ffe_result_error_code(result)); + add_assoc_bool(return_value, "do_log", ddog_ffe_result_do_log(result)); + + ddog_ffe_free_result(result); +} + +PHP_FUNCTION(DDTrace_ffe_send_exposure) { + char *event_json, *flag_key, *variant_key; + size_t event_json_len, flag_key_len, variant_key_len; + char *allocation_key = NULL; + size_t allocation_key_len = 0; + char *targeting_key = NULL; + size_t targeting_key_len = 0; + + ZEND_PARSE_PARAMETERS_START(5, 5) + Z_PARAM_STRING(event_json, event_json_len) + Z_PARAM_STRING(flag_key, flag_key_len) + Z_PARAM_STRING_OR_NULL(allocation_key, allocation_key_len) + Z_PARAM_STRING_OR_NULL(targeting_key, targeting_key_len) + Z_PARAM_STRING(variant_key, variant_key_len) + ZEND_PARSE_PARAMETERS_END(); + + UNUSED(event_json_len); + UNUSED(flag_key_len); + UNUSED(variant_key_len); + + RETURN_BOOL(ddog_ffe_enqueue_exposure( + event_json, + flag_key, + allocation_key_len > 0 ? allocation_key : NULL, + targeting_key_len > 0 ? targeting_key : NULL, + variant_key)); +} + +PHP_FUNCTION(DDTrace_ffe_flush_exposures) { + ddog_CharSlice payload; + + ZEND_PARSE_PARAMETERS_NONE(); + + payload = ddog_ffe_flush_exposures(); + if (payload.ptr == NULL || payload.len == 0) { + RETURN_NULL(); + } + + RETVAL_STRINGL(payload.ptr, payload.len); + ddog_ffe_free_flush_result(payload); +} + +PHP_FUNCTION(DDTrace_ffe_set_service_context) { + char *service, *env, *version; + size_t service_len, env_len, version_len; + + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_STRING(service, service_len) + Z_PARAM_STRING(env, env_len) + Z_PARAM_STRING(version, version_len) + ZEND_PARSE_PARAMETERS_END(); + + UNUSED(service_len); + UNUSED(env_len); + UNUSED(version_len); + + ddog_ffe_set_service_context(service, env, version); +} + +PHP_FUNCTION(DDTrace_ffe_reset_exposure_state) { + ZEND_PARSE_PARAMETERS_NONE(); + + ddog_ffe_reset_exposure_state(); +} + PHP_FUNCTION(dd_trace_send_traces_via_thread) { char *payload = NULL; ddtrace_zpplong_t num_traces = 0; diff --git a/ext/ddtrace.stub.php b/ext/ddtrace.stub.php index e4a388e157d..c1da1c442ed 100644 --- a/ext/ddtrace.stub.php +++ b/ext/ddtrace.stub.php @@ -845,6 +845,79 @@ function add_endpoint(string $path, string $operation_name, string $resource_nam * Call this once after batching all add_endpoint() calls. */ function flush_endpoints(): void {} + + /** + * Evaluate a feature flag using the stored UFC configuration. + * + * @param string $flagKey The flag key to evaluate. + * @param int $expectedType The expected flag type (0=string, 1=int, 2=float, 3=bool, 4=object). + * @param string|null $targetingKey The targeting key for evaluation context. + * @param array $attributes Flat key-value map of evaluation context attributes (string keys, primitive values). + * @return array|null Associative array with keys: value_json, variant, allocation_key, reason, error_code, do_log. Null only if evaluation engine is unavailable. + * + * @internal Used by the Datadog feature flag client. + */ + function ffe_evaluate(string $flagKey, int $expectedType, ?string $targetingKey, array $attributes): ?array {} + + /** + * Check if FFE (Feature Flag Evaluation) configuration is loaded. + * + * @return bool True if a flag configuration has been loaded. + * + * @internal Used by the Datadog feature flag client. + */ + function ffe_has_config(): bool {} + + /** + * Return the current FFE configuration version counter. + * + * @return int Monotonically-increasing version counter. + * + * @internal Used by the Datadog feature flag client. + */ + function ffe_config_version(): int {} + + /** + * Load a UFC JSON configuration string into the FFE engine. + * Used for testing without Remote Config. + * + * @param string $json UFC JSON configuration string. + * @return bool True if the configuration was parsed and loaded successfully. + * + * @internal Used by tests. + */ + function ffe_load_config(string $json): bool {} + + /** + * Enqueue a serialized FFE exposure event for native deduplication and batched delivery. + * + * @internal Used by the Datadog feature flag client. + */ + function ffe_send_exposure(string $eventJson, string $flagKey, ?string $allocationKey, ?string $targetingKey, string $variantKey): bool {} + + /** + * Drain buffered FFE exposure events and return the native batch payload. + * Used by tests; production delivery is owned by request/module shutdown. + * + * @return string|null Serialized batch payload, or null when the buffer is empty. + * + * @internal Used by tests. + */ + function ffe_flush_exposures(): ?string {} + + /** + * Set service/env/version context for native FFE exposure batch payloads. + * + * @internal Used by the Datadog feature flag client. + */ + function ffe_set_service_context(string $service, string $env, string $version): void {} + + /** + * Reset native FFE exposure state. + * + * @internal Used by tests and fork handling. + */ + function ffe_reset_exposure_state(): void {} } namespace DDTrace\System { @@ -975,6 +1048,7 @@ function add_span_flag(\DDTrace\SpanData $span, int $flag): void {} * @internal */ function handle_fork(): void {} + } namespace datadog\appsec\v2 { diff --git a/ext/ddtrace_arginfo.h b/ext/ddtrace_arginfo.h index a6e618143b5..1424feeb913 100644 --- a/ext/ddtrace_arginfo.h +++ b/ext/ddtrace_arginfo.h @@ -176,6 +176,42 @@ ZEND_END_ARG_INFO() #define arginfo_DDTrace_flush_endpoints arginfo_DDTrace_flush +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_ffe_evaluate, 0, 4, IS_ARRAY, 1) + ZEND_ARG_TYPE_INFO(0, flagKey, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, expectedType, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, targetingKey, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, attributes, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_ffe_has_config, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_ffe_config_version, 0, 0, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_ffe_load_config, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, json, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_ffe_send_exposure, 0, 5, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, eventJson, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, flagKey, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, allocationKey, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, targetingKey, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, variantKey, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_ffe_flush_exposures, 0, 0, IS_STRING, 1) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_ffe_set_service_context, 0, 3, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, service, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, env, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, version, IS_STRING, 0) +ZEND_END_ARG_INFO() + +#define arginfo_DDTrace_ffe_reset_exposure_state arginfo_DDTrace_flush + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_System_container_id, 0, 0, IS_STRING, 1) ZEND_END_ARG_INFO() @@ -394,6 +430,14 @@ ZEND_FUNCTION(DDTrace_resource_weak_get); ZEND_FUNCTION(DDTrace_are_endpoints_collected); ZEND_FUNCTION(DDTrace_add_endpoint); ZEND_FUNCTION(DDTrace_flush_endpoints); +ZEND_FUNCTION(DDTrace_ffe_evaluate); +ZEND_FUNCTION(DDTrace_ffe_has_config); +ZEND_FUNCTION(DDTrace_ffe_config_version); +ZEND_FUNCTION(DDTrace_ffe_load_config); +ZEND_FUNCTION(DDTrace_ffe_send_exposure); +ZEND_FUNCTION(DDTrace_ffe_flush_exposures); +ZEND_FUNCTION(DDTrace_ffe_set_service_context); +ZEND_FUNCTION(DDTrace_ffe_reset_exposure_state); ZEND_FUNCTION(DDTrace_System_container_id); ZEND_FUNCTION(DDTrace_System_process_tags_base_hash); ZEND_FUNCTION(DDTrace_Config_integration_analytics_enabled); @@ -489,6 +533,14 @@ static const zend_function_entry ext_functions[] = { ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "are_endpoints_collected"), zif_DDTrace_are_endpoints_collected, arginfo_DDTrace_are_endpoints_collected, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "add_endpoint"), zif_DDTrace_add_endpoint, arginfo_DDTrace_add_endpoint, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "flush_endpoints"), zif_DDTrace_flush_endpoints, arginfo_DDTrace_flush_endpoints, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_evaluate"), zif_DDTrace_ffe_evaluate, arginfo_DDTrace_ffe_evaluate, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_has_config"), zif_DDTrace_ffe_has_config, arginfo_DDTrace_ffe_has_config, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_config_version"), zif_DDTrace_ffe_config_version, arginfo_DDTrace_ffe_config_version, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_load_config"), zif_DDTrace_ffe_load_config, arginfo_DDTrace_ffe_load_config, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_send_exposure"), zif_DDTrace_ffe_send_exposure, arginfo_DDTrace_ffe_send_exposure, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_flush_exposures"), zif_DDTrace_ffe_flush_exposures, arginfo_DDTrace_ffe_flush_exposures, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_set_service_context"), zif_DDTrace_ffe_set_service_context, arginfo_DDTrace_ffe_set_service_context, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_reset_exposure_state"), zif_DDTrace_ffe_reset_exposure_state, arginfo_DDTrace_ffe_reset_exposure_state, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\System", "container_id"), zif_DDTrace_System_container_id, arginfo_DDTrace_System_container_id, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\System", "process_tags_base_hash"), zif_DDTrace_System_process_tags_base_hash, arginfo_DDTrace_System_process_tags_base_hash, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Config", "integration_analytics_enabled"), zif_DDTrace_Config_integration_analytics_enabled, arginfo_DDTrace_Config_integration_analytics_enabled, 0, NULL, NULL) diff --git a/ext/sidecar.c b/ext/sidecar.c index 9ea81ab2220..99e03846a62 100644 --- a/ext/sidecar.c +++ b/ext/sidecar.c @@ -381,7 +381,8 @@ bool ddtrace_sidecar_should_enable(bool *appsec_activation, bool *appsec_config) bool enable_sidecar = ddtrace_sidecar_maybe_enable_appsec(appsec_activation, appsec_config); if (!enable_sidecar) { enable_sidecar = get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED() || - get_global_DD_TRACE_SIDECAR_TRACE_SENDER(); + get_global_DD_TRACE_SIDECAR_TRACE_SENDER() || + get_global_DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED(); } return enable_sidecar; } @@ -390,7 +391,12 @@ void ddtrace_sidecar_setup(bool appsec_activation, bool appsec_config) { ddtrace_set_non_resettable_sidecar_globals(); ddtrace_set_resettable_sidecar_globals(); - ddog_init_remote_config(get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED(), appsec_activation, appsec_config); + ddog_init_remote_config((struct ddog_DdogRemoteConfigFlags){ + .live_debugging_enabled = get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED(), + .appsec_activation = appsec_activation, + .appsec_config = appsec_config, + .ffe_enabled = get_global_DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED(), + }); zend_long mode = get_global_DD_TRACE_SIDECAR_CONNECTION_MODE(); @@ -438,6 +444,8 @@ void ddtrace_sidecar_handle_fork(void) { bool appsec_config = false; bool enable_sidecar = ddtrace_sidecar_should_enable(&appsec_activation, &appsec_config); + ddog_ffe_reset_exposure_state(); + if (!enable_sidecar) { return; } @@ -529,7 +537,11 @@ void ddtrace_sidecar_finalize(bool clear_id) { } } +static void dd_flush_ffe_exposures(void); + void ddtrace_sidecar_shutdown(void) { + dd_flush_ffe_exposures(); + ddtrace_sidecar_for_signal = NULL; // In thread mode, drop the main thread's connection before shutting down the @@ -872,9 +884,28 @@ void ddtrace_sidecar_rinit(void) { } void ddtrace_sidecar_rshutdown(void) { + dd_flush_ffe_exposures(); ddog_Vec_Tag_drop(DDTRACE_G(active_global_tags)); } +static void dd_flush_ffe_exposures(void) { + if (!DDTRACE_G(sidecar) || !ddtrace_sidecar_instance_id) { + return; + } + + ddog_CharSlice payload = ddog_ffe_flush_exposures(); + if (payload.ptr == NULL || payload.len == 0) { + return; + } + + ddtrace_ffi_try("Failed forwarding FFE exposures to sidecar", + ddog_sidecar_send_ffe_exposures(&DDTRACE_G(sidecar), + ddtrace_sidecar_instance_id, + &DDTRACE_G(sidecar_queue_id), + payload)); + ddog_ffe_free_flush_result(payload); +} + void ddtrace_sidecar_gshutdown(zend_ddtrace_globals *ddtrace_globals) { // NOTE: do not use DDTRACE_G() in this function; it may be called from the // main thread via ts_free_id() diff --git a/libdatadog b/libdatadog index cea1e44eddd..a649b7f658d 160000 --- a/libdatadog +++ b/libdatadog @@ -1 +1 @@ -Subproject commit cea1e44edddd9124f75d5095f31026904a1f58d8 +Subproject commit a649b7f658d7df1449333929f42224bfbbc72f8d diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index e65fd5c4c98..b146be36e05 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -382,6 +382,13 @@ "default": "false" } ], + "DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED": [ + { + "implementation": "A", + "type": "boolean", + "default": "false" + } + ], "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": [ { "implementation": "B", diff --git a/src/api/FeatureFlags/Client.php b/src/api/FeatureFlags/Client.php new file mode 100644 index 00000000000..b248371e568 --- /dev/null +++ b/src/api/FeatureFlags/Client.php @@ -0,0 +1,247 @@ +evaluator = $evaluator; + $this->warningEmitter = $warningEmitter; + $this->exposureWriter = $exposureWriter ?: new NoopExposureWriter(); + $this->metricsRecorder = $metricsRecorder ?: new NoopMetricsRecorder(); + } + + public static function create( + $evaluator = null, + $warningEmitter = null, + $exposureWriter = null, + $metricsRecorder = null + ) { + if ($evaluator !== null && !$evaluator instanceof Evaluator) { + throw new \InvalidArgumentException('Expected an Evaluator instance'); + } + + if ($warningEmitter !== null && !$warningEmitter instanceof WarningEmitter) { + throw new \InvalidArgumentException('Expected a WarningEmitter instance'); + } + + return new self( + $evaluator ?: NativeEvaluator::createOrUnavailable(), + $warningEmitter ?: new TriggerErrorWarningEmitter(), + $exposureWriter ?: NativeExposureWriter::createOrNoop(), + $metricsRecorder + ); + } + + public function getBooleanValue($flagKey, $defaultValue, array $context = array()) + { + return $this->getBooleanDetails($flagKey, $defaultValue, $context)->getValue(); + } + + public function getStringValue($flagKey, $defaultValue, array $context = array()) + { + return $this->getStringDetails($flagKey, $defaultValue, $context)->getValue(); + } + + public function getIntegerValue($flagKey, $defaultValue, array $context = array()) + { + return $this->getIntegerDetails($flagKey, $defaultValue, $context)->getValue(); + } + + public function getFloatValue($flagKey, $defaultValue, array $context = array()) + { + return $this->getFloatDetails($flagKey, $defaultValue, $context)->getValue(); + } + + public function getObjectValue($flagKey, array $defaultValue, array $context = array()) + { + return $this->getObjectDetails($flagKey, $defaultValue, $context)->getValue(); + } + + public function getBooleanDetails($flagKey, $defaultValue, array $context = array()) + { + return $this->evaluate($flagKey, EvaluationType::BOOLEAN, $this->expectBoolean($defaultValue), $context); + } + + public function getStringDetails($flagKey, $defaultValue, array $context = array()) + { + return $this->evaluate($flagKey, EvaluationType::STRING, $this->expectString($defaultValue), $context); + } + + public function getIntegerDetails($flagKey, $defaultValue, array $context = array()) + { + return $this->evaluate($flagKey, EvaluationType::INTEGER, $this->expectInteger($defaultValue), $context); + } + + public function getFloatDetails($flagKey, $defaultValue, array $context = array()) + { + return $this->evaluate($flagKey, EvaluationType::FLOAT, $this->expectFloat($defaultValue), $context); + } + + public function getObjectDetails($flagKey, array $defaultValue, array $context = array()) + { + return $this->evaluate($flagKey, EvaluationType::OBJECT, $defaultValue, $context); + } + + private function evaluate($flagKey, $expectedType, $defaultValue, array $context) + { + $flagKey = $this->expectFlagKey($flagKey); + list($targetingKey, $attributes) = $this->normalizeContext($context); + + $details = $this->evaluator->evaluate( + $flagKey, + $expectedType, + $defaultValue, + $targetingKey, + $attributes + ); + + $this->warnIfNonProductionRuntime($details); + $this->metricsRecorder->recordEvaluation( + $flagKey, + $details->getValueType(), + $details->getReason(), + $details->getErrorCode() + ); + $this->writeExposure($flagKey, $targetingKey, $attributes, $details); + + return $details; + } + + private function writeExposure($flagKey, $targetingKey, array $attributes, EvaluationDetails $details) + { + if ($details->isError()) { + return; + } + + $exposureData = $details->getExposureData(); + if (!$exposureData || (array_key_exists('doLog', $exposureData) && $exposureData['doLog'] === false)) { + return; + } + + $event = array( + 'flagKey' => $flagKey, + 'targetingKey' => $targetingKey, + 'attributes' => $attributes, + 'value' => $details->getValue(), + 'valueType' => $details->getValueType(), + 'reason' => $details->getReason(), + 'variant' => $details->getVariant(), + 'flagMetadata' => $details->getFlagMetadata(), + 'exposureData' => $exposureData, + ); + + if (array_key_exists('allocationKey', $exposureData)) { + $event['allocationKey'] = $exposureData['allocationKey']; + } + + if (array_key_exists('doLog', $exposureData)) { + $event['doLog'] = $exposureData['doLog']; + } + + $this->exposureWriter->write($event); + } + + private function normalizeContext(array $context) + { + $targetingKey = null; + if (array_key_exists('targetingKey', $context) && $context['targetingKey'] !== null) { + $targetingKey = (string) $context['targetingKey']; + } + + $attributes = array(); + if (isset($context['attributes']) && is_array($context['attributes'])) { + foreach ($context['attributes'] as $key => $value) { + if (is_bool($value) || is_int($value) || is_float($value) || is_string($value)) { + $attributes[(string) $key] = $value; + } + } + } + + return array($targetingKey, $attributes); + } + + private function warnIfNonProductionRuntime(EvaluationDetails $details) + { + if ($this->warnedAboutNonProductionRuntime) { + return; + } + + $providerState = $details->getProviderState(); + if (!array_key_exists('productionRuntime', $providerState) || $providerState['productionRuntime'] !== false) { + return; + } + + $message = $details->getErrorMessage(); + if (!is_string($message) || $message === '') { + $message = 'Datadog-backed PHP feature flag evaluation is not fully enabled yet.'; + } + + $this->warningEmitter->warning($message); + $this->warnedAboutNonProductionRuntime = true; + } + + private function expectFlagKey($flagKey) + { + if (!is_string($flagKey) || $flagKey === '') { + throw new \InvalidArgumentException('Feature flag key must be a non-empty string'); + } + + return $flagKey; + } + + private function expectBoolean($value) + { + if (!is_bool($value)) { + throw new \InvalidArgumentException('Boolean flag default value must be a bool'); + } + + return $value; + } + + private function expectString($value) + { + if (!is_string($value)) { + throw new \InvalidArgumentException('String flag default value must be a string'); + } + + return $value; + } + + private function expectInteger($value) + { + if (!is_int($value)) { + throw new \InvalidArgumentException('Integer flag default value must be an int'); + } + + return $value; + } + + private function expectFloat($value) + { + if (!is_int($value) && !is_float($value)) { + throw new \InvalidArgumentException('Float flag default value must be a number'); + } + + return (float) $value; + } +} diff --git a/src/api/FeatureFlags/EvaluationDetails.php b/src/api/FeatureFlags/EvaluationDetails.php new file mode 100644 index 00000000000..cc788a1a1ee --- /dev/null +++ b/src/api/FeatureFlags/EvaluationDetails.php @@ -0,0 +1,111 @@ + $flagMetadata + * @param array $exposureData + * @param array $providerState + */ + public function __construct( + $value, + $valueType, + $reason, + $variant = null, + $errorCode = null, + $errorMessage = null, + array $flagMetadata = array(), + array $exposureData = array(), + array $providerState = array() + ) { + if (!EvaluationType::isValid($valueType)) { + throw new \InvalidArgumentException('Unknown feature flag value type: ' . (string) $valueType); + } + + if (!EvaluationReason::isValid($reason)) { + throw new \InvalidArgumentException('Unknown feature flag evaluation reason: ' . (string) $reason); + } + + if (!EvaluationErrorCode::isValid($errorCode)) { + throw new \InvalidArgumentException('Unknown feature flag evaluation error code: ' . (string) $errorCode); + } + + $this->value = $value; + $this->valueType = $valueType; + $this->reason = $reason; + $this->variant = $variant; + $this->errorCode = $errorCode; + $this->errorMessage = $errorMessage; + $this->flagMetadata = $flagMetadata; + $this->exposureData = $exposureData; + $this->providerState = $providerState; + } + + public function getValue() + { + return $this->value; + } + + public function getValueType() + { + return $this->valueType; + } + + public function getReason() + { + return $this->reason; + } + + public function getVariant() + { + return $this->variant; + } + + public function getErrorCode() + { + return $this->errorCode; + } + + public function getErrorMessage() + { + return $this->errorMessage; + } + + public function getFlagMetadata() + { + return $this->flagMetadata; + } + + public function getExposureData() + { + return $this->exposureData; + } + + public function getProviderState() + { + return $this->providerState; + } + + public function isError() + { + return $this->errorCode !== null; + } +} diff --git a/src/api/FeatureFlags/EvaluationErrorCode.php b/src/api/FeatureFlags/EvaluationErrorCode.php new file mode 100644 index 00000000000..a8f9a722a22 --- /dev/null +++ b/src/api/FeatureFlags/EvaluationErrorCode.php @@ -0,0 +1,29 @@ + true, + self::PARSE_ERROR => true, + self::TYPE_MISMATCH => true, + self::GENERAL => true, + self::PROVIDER_NOT_READY => true, + ); + + private function __construct() + { + } + + public static function isValid($errorCode) + { + return $errorCode === null || isset(self::$valid[$errorCode]); + } +} diff --git a/src/api/FeatureFlags/EvaluationReason.php b/src/api/FeatureFlags/EvaluationReason.php new file mode 100644 index 00000000000..7983797b9bf --- /dev/null +++ b/src/api/FeatureFlags/EvaluationReason.php @@ -0,0 +1,31 @@ + true, + self::DEFAULT_REASON => true, + self::TARGETING_MATCH => true, + self::SPLIT => true, + self::DISABLED => true, + self::ERROR => true, + ); + + private function __construct() + { + } + + public static function isValid($reason) + { + return isset(self::$valid[$reason]); + } +} diff --git a/src/api/FeatureFlags/EvaluationType.php b/src/api/FeatureFlags/EvaluationType.php new file mode 100644 index 00000000000..fa20632137c --- /dev/null +++ b/src/api/FeatureFlags/EvaluationType.php @@ -0,0 +1,54 @@ + true, + self::STRING => true, + self::INTEGER => true, + self::FLOAT => true, + self::OBJECT => true, + ); + + private function __construct() + { + } + + public static function isValid($valueType) + { + return isset(self::$valid[$valueType]); + } + + public static function fromDefaultValue($defaultValue) + { + if (is_bool($defaultValue)) { + return self::BOOLEAN; + } + + if (is_string($defaultValue)) { + return self::STRING; + } + + if (is_int($defaultValue)) { + return self::INTEGER; + } + + if (is_float($defaultValue)) { + return self::FLOAT; + } + + if (is_array($defaultValue)) { + return self::OBJECT; + } + + throw new \InvalidArgumentException('Unsupported feature flag default value type'); + } +} diff --git a/src/api/FeatureFlags/Evaluator.php b/src/api/FeatureFlags/Evaluator.php new file mode 100644 index 00000000000..51af0f32d65 --- /dev/null +++ b/src/api/FeatureFlags/Evaluator.php @@ -0,0 +1,16 @@ + $attributes + * @return EvaluationDetails + */ + public function evaluate($flagKey, $expectedType, $defaultValue, $targetingKey = null, array $attributes = array()); +} diff --git a/src/api/FeatureFlags/ExposureWriter.php b/src/api/FeatureFlags/ExposureWriter.php new file mode 100644 index 00000000000..5a35d3ebe20 --- /dev/null +++ b/src/api/FeatureFlags/ExposureWriter.php @@ -0,0 +1,17 @@ + $event + * @return void + */ + public function write(array $event); + + /** + * @return void + */ + public function flush(); +} diff --git a/src/api/FeatureFlags/MetricsRecorder.php b/src/api/FeatureFlags/MetricsRecorder.php new file mode 100644 index 00000000000..609135d0ad7 --- /dev/null +++ b/src/api/FeatureFlags/MetricsRecorder.php @@ -0,0 +1,12 @@ +mapper = $mapper ?: new ResultMapper(); + $this->unavailableEvaluator = $unavailableEvaluator ?: new UnavailableEvaluator(); + $this->remoteConfig = $remoteConfig ?: new RemoteConfigClient(); + } + + public static function isAvailable() + { + return function_exists('DDTrace\\ffe_evaluate') + && RemoteConfigClient::isAvailable(); + } + + public static function createOrUnavailable() + { + return self::isAvailable() ? new self() : new UnavailableEvaluator(); + } + + public function evaluate( + $flagKey, + $expectedType, + $defaultValue, + $targetingKey = null, + array $attributes = array() + ) { + if (!self::isAvailable()) { + return $this->unavailableEvaluator->evaluate( + $flagKey, + $expectedType, + $defaultValue, + $targetingKey, + $attributes + ); + } + + $rawResult = \DDTrace\ffe_evaluate( + $flagKey, + $this->typeId($expectedType), + $targetingKey, + $this->normalizeAttributes($attributes) + ); + + if (is_array($rawResult)) { + $rawResult = $this->withProviderState($rawResult); + } + + return $this->mapper->map($rawResult, $expectedType, $defaultValue); + } + + private function typeId($expectedType) + { + switch ($expectedType) { + case EvaluationType::STRING: + return 0; + case EvaluationType::INTEGER: + return 1; + case EvaluationType::FLOAT: + return 2; + case EvaluationType::BOOLEAN: + return 3; + case EvaluationType::OBJECT: + return 4; + } + + throw new \InvalidArgumentException('Unknown feature flag value type: ' . (string) $expectedType); + } + + private function normalizeAttributes(array $attributes) + { + $normalized = array(); + foreach ($attributes as $key => $value) { + if (is_bool($value) || is_int($value) || is_float($value) || is_string($value)) { + $normalized[(string) $key] = $value; + } + } + + return $normalized; + } + + private function withProviderState(array $rawResult) + { + $hasConfig = $this->remoteConfig->hasConfig(); + $configVersion = $this->remoteConfig->configVersion(); + + $providerState = array( + 'ready' => $hasConfig, + 'hasConfig' => $hasConfig, + 'configVersion' => $configVersion, + 'productionRuntime' => false, + 'mode' => 'native_remote_config', + 'reason' => $hasConfig ? 'metrics_delivery_pending' : 'configuration_missing', + ); + + if (isset($rawResult['provider_state']) && is_array($rawResult['provider_state'])) { + $providerState = array_merge($providerState, $rawResult['provider_state']); + } + + if (!$hasConfig) { + $rawResult['error_message'] = self::WARNING_MESSAGE; + } + + $rawResult['provider_state'] = $providerState; + $rawResult['has_config'] = $hasConfig; + $rawResult['config_version'] = $configVersion; + + return $rawResult; + } +} diff --git a/src/api/FeatureFlags/NativeExposureWriter.php b/src/api/FeatureFlags/NativeExposureWriter.php new file mode 100644 index 00000000000..9cf19308229 --- /dev/null +++ b/src/api/FeatureFlags/NativeExposureWriter.php @@ -0,0 +1,126 @@ +setServiceContext(); + } + + public static function isAvailable() + { + return function_exists('DDTrace\\ffe_send_exposure') + && function_exists('DDTrace\\ffe_set_service_context'); + } + + public static function createOrNoop() + { + return self::isAvailable() ? new self() : new NoopExposureWriter(); + } + + public function write(array $event) + { + if (!self::isAvailable()) { + return; + } + + if (array_key_exists('doLog', $event) && $event['doLog'] === false) { + return; + } + + $flagKey = $this->stringValue($event, 'flagKey'); + if ($flagKey === '') { + return; + } + + $variantKey = $this->stringValue($event, 'variant'); + $allocationKey = $this->nullableStringValue($event, 'allocationKey'); + $targetingKey = $this->nullableStringValue($event, 'targetingKey'); + $eventJson = $this->eventJson($event, $flagKey, $allocationKey, $targetingKey, $variantKey); + if ($eventJson === null) { + return; + } + + \DDTrace\ffe_send_exposure($eventJson, $flagKey, $allocationKey, $targetingKey, $variantKey); + } + + public function flush() + { + // Native exposure delivery is owned by the extension lifecycle. Calling + // DDTrace\ffe_flush_exposures() here would drain the buffer without + // forwarding it to the sidecar. + } + + private function setServiceContext() + { + \DDTrace\ffe_set_service_context( + $this->configValue('DD_SERVICE', 'datadog.service'), + $this->configValue('DD_ENV', 'datadog.env'), + $this->configValue('DD_VERSION', 'datadog.version') + ); + } + + private function eventJson(array $event, $flagKey, $allocationKey, $targetingKey, $variantKey) + { + $attributes = array(); + if (array_key_exists('attributes', $event) && is_array($event['attributes'])) { + foreach ($event['attributes'] as $key => $value) { + if (is_bool($value) || is_int($value) || is_float($value) || is_string($value)) { + $attributes[(string) $key] = $value; + } + } + } + + $json = json_encode(array( + 'timestamp' => (int) floor(microtime(true) * 1000), + 'flag' => array('key' => $flagKey), + 'allocation' => array('key' => $allocationKey === null ? '' : $allocationKey), + 'variant' => array('key' => $variantKey), + 'subject' => array( + 'id' => $targetingKey === null ? '' : $targetingKey, + 'attributes' => empty($attributes) ? new \stdClass() : (object) $attributes, + ), + ), JSON_UNESCAPED_SLASHES); + + return is_string($json) ? $json : null; + } + + private function configValue($envName, $iniName) + { + if (function_exists('dd_trace_env_config')) { + $value = \dd_trace_env_config($envName); + if (is_scalar($value)) { + return (string) $value; + } + } + + $value = ini_get($iniName); + return is_string($value) ? $value : ''; + } + + private function stringValue(array $event, $key) + { + $value = $this->nullableStringValue($event, $key); + return $value === null ? '' : $value; + } + + private function nullableStringValue(array $event, $key) + { + if (!array_key_exists($key, $event) || $event[$key] === null) { + return null; + } + + if (is_scalar($event[$key])) { + return (string) $event[$key]; + } + + $json = json_encode($event[$key]); + return is_string($json) ? $json : null; + } +} diff --git a/src/api/FeatureFlags/NoopExposureWriter.php b/src/api/FeatureFlags/NoopExposureWriter.php new file mode 100644 index 00000000000..60fe0b5bc02 --- /dev/null +++ b/src/api/FeatureFlags/NoopExposureWriter.php @@ -0,0 +1,14 @@ +hasConfig = $hasConfig ?: function () { + return function_exists('DDTrace\\ffe_has_config') && \DDTrace\ffe_has_config(); + }; + $this->configVersion = $configVersion ?: function () { + return function_exists('DDTrace\\ffe_config_version') ? \DDTrace\ffe_config_version() : 0; + }; + } + + public static function isAvailable() + { + return function_exists('DDTrace\\ffe_has_config') + && function_exists('DDTrace\\ffe_config_version'); + } + + public function hasConfig() + { + return (bool) call_user_func($this->hasConfig); + } + + public function configVersion() + { + $version = call_user_func($this->configVersion); + return is_int($version) ? $version : (int) $version; + } + + public function waitUntilReady($timeoutSeconds = 5.0, $pollIntervalMicroseconds = 10000) + { + if ($this->hasConfig()) { + return true; + } + + if ($timeoutSeconds <= 0) { + return false; + } + + $deadline = microtime(true) + (float) $timeoutSeconds; + while (microtime(true) < $deadline) { + usleep((int) $pollIntervalMicroseconds); + if ($this->hasConfig()) { + return true; + } + } + + return $this->hasConfig(); + } +} diff --git a/src/api/FeatureFlags/ResultMapper.php b/src/api/FeatureFlags/ResultMapper.php new file mode 100644 index 00000000000..0967ab16d39 --- /dev/null +++ b/src/api/FeatureFlags/ResultMapper.php @@ -0,0 +1,278 @@ +|EvaluationDetails|null $rawResult + * @param string $expectedType One of EvaluationType::*. + * @param mixed $defaultValue + * @return EvaluationDetails + */ + public function map($rawResult, $expectedType, $defaultValue) + { + if (!EvaluationType::isValid($expectedType)) { + throw new \InvalidArgumentException('Unknown feature flag value type: ' . (string) $expectedType); + } + + if ($rawResult instanceof EvaluationDetails) { + return $rawResult; + } + + if ($rawResult === null) { + return $this->errorDetails( + $defaultValue, + $expectedType, + EvaluationErrorCode::PROVIDER_NOT_READY, + 'FFE evaluator is not ready', + array('ready' => false) + ); + } + + if (!is_array($rawResult)) { + return $this->errorDetails( + $defaultValue, + $expectedType, + EvaluationErrorCode::GENERAL, + 'FFE evaluator returned an invalid result' + ); + } + + $errorCode = $this->mapErrorCode( + $this->read($rawResult, array('error_code', 'errorCode'), self::BRIDGE_ERROR_GENERAL) + ); + if ($errorCode !== null) { + return $this->errorDetails( + $defaultValue, + $expectedType, + $errorCode, + $this->read($rawResult, array('error_message', 'errorMessage'), null), + $this->readArray($rawResult, array('provider_state', 'providerState')) + ); + } + + $decoded = null; + $decodeError = $this->decodeValue($rawResult, $expectedType, $decoded); + if ($decodeError !== null) { + return $this->errorDetails( + $defaultValue, + $expectedType, + $decodeError, + $decodeError === EvaluationErrorCode::PARSE_ERROR + ? 'FFE evaluator returned invalid JSON' + : 'FFE evaluator returned a value with the wrong type', + $this->readArray($rawResult, array('provider_state', 'providerState')) + ); + } + + $reason = $this->mapReason($this->read($rawResult, array('reason'), self::BRIDGE_REASON_DEFAULT)); + + return new EvaluationDetails( + $decoded, + $expectedType, + $reason, + $this->read($rawResult, array('variant'), null), + null, + null, + $this->readArray($rawResult, array('flag_metadata', 'flagMetadata', 'metadata')), + $this->exposureData($rawResult), + $this->providerState($rawResult) + ); + } + + private function errorDetails( + $defaultValue, + $expectedType, + $errorCode, + $errorMessage = null, + array $providerState = array() + ) { + return new EvaluationDetails( + $defaultValue, + $expectedType, + EvaluationReason::ERROR, + null, + $errorCode, + $errorMessage, + array(), + array(), + $providerState + ); + } + + private function decodeValue(array $rawResult, $expectedType, &$decoded) + { + if (array_key_exists('value', $rawResult)) { + $value = $rawResult['value']; + } else { + $valueJson = $this->read($rawResult, array('value_json', 'valueJson'), null); + if (!is_string($valueJson) || $valueJson === '') { + return EvaluationErrorCode::PARSE_ERROR; + } + + $value = json_decode($valueJson, true); + if (json_last_error() !== JSON_ERROR_NONE) { + return EvaluationErrorCode::PARSE_ERROR; + } + } + + if (!$this->coerceValue($value, $expectedType, $decoded)) { + return EvaluationErrorCode::TYPE_MISMATCH; + } + + return null; + } + + private function coerceValue($value, $expectedType, &$coerced) + { + switch ($expectedType) { + case EvaluationType::BOOLEAN: + if (is_bool($value)) { + $coerced = $value; + return true; + } + return false; + + case EvaluationType::STRING: + if (is_string($value)) { + $coerced = $value; + return true; + } + return false; + + case EvaluationType::INTEGER: + if (is_int($value)) { + $coerced = $value; + return true; + } + return false; + + case EvaluationType::FLOAT: + if (is_int($value) || is_float($value)) { + $coerced = (float) $value; + return true; + } + return false; + + case EvaluationType::OBJECT: + if (is_array($value)) { + $coerced = $value; + return true; + } + return false; + } + + return false; + } + + private function mapErrorCode($errorCode) + { + if ($errorCode === null || $errorCode === self::BRIDGE_ERROR_NONE || $errorCode === '0') { + return null; + } + + if (is_string($errorCode) && EvaluationErrorCode::isValid($errorCode)) { + return $errorCode; + } + + switch ((int) $errorCode) { + case self::BRIDGE_ERROR_TYPE_MISMATCH: + return EvaluationErrorCode::TYPE_MISMATCH; + case self::BRIDGE_ERROR_CONFIG_PARSE: + return EvaluationErrorCode::PARSE_ERROR; + case self::BRIDGE_ERROR_FLAG_UNRECOGNIZED: + return EvaluationErrorCode::FLAG_NOT_FOUND; + case self::BRIDGE_ERROR_CONFIG_MISSING: + return EvaluationErrorCode::PROVIDER_NOT_READY; + case self::BRIDGE_ERROR_GENERAL: + default: + return EvaluationErrorCode::GENERAL; + } + } + + private function mapReason($reason) + { + if (is_string($reason) && EvaluationReason::isValid($reason)) { + return $reason; + } + + switch ((int) $reason) { + case self::BRIDGE_REASON_STATIC: + return EvaluationReason::STATIC_REASON; + case self::BRIDGE_REASON_TARGETING_MATCH: + return EvaluationReason::TARGETING_MATCH; + case self::BRIDGE_REASON_SPLIT: + return EvaluationReason::SPLIT; + case self::BRIDGE_REASON_DISABLED: + return EvaluationReason::DISABLED; + case self::BRIDGE_REASON_ERROR: + return EvaluationReason::ERROR; + case self::BRIDGE_REASON_DEFAULT: + default: + return EvaluationReason::DEFAULT_REASON; + } + } + + private function exposureData(array $rawResult) + { + $exposureData = $this->readArray($rawResult, array('exposure_data', 'exposureData')); + + if (array_key_exists('allocation_key', $rawResult)) { + $exposureData['allocationKey'] = $rawResult['allocation_key']; + } + + if (array_key_exists('do_log', $rawResult)) { + $exposureData['doLog'] = (bool) $rawResult['do_log']; + } + + return $exposureData; + } + + private function providerState(array $rawResult) + { + $providerState = $this->readArray($rawResult, array('provider_state', 'providerState')); + + if (array_key_exists('has_config', $rawResult)) { + $providerState['hasConfig'] = (bool) $rawResult['has_config']; + } + + if (array_key_exists('config_version', $rawResult)) { + $providerState['configVersion'] = $rawResult['config_version']; + } + + return $providerState; + } + + private function readArray(array $rawResult, array $keys) + { + $value = $this->read($rawResult, $keys, array()); + + return is_array($value) ? $value : array(); + } + + private function read(array $rawResult, array $keys, $default) + { + foreach ($keys as $key) { + if (array_key_exists($key, $rawResult)) { + return $rawResult[$key]; + } + } + + return $default; + } +} diff --git a/src/api/FeatureFlags/TriggerErrorWarningEmitter.php b/src/api/FeatureFlags/TriggerErrorWarningEmitter.php new file mode 100644 index 00000000000..5e57592ca9d --- /dev/null +++ b/src/api/FeatureFlags/TriggerErrorWarningEmitter.php @@ -0,0 +1,11 @@ + false, + 'productionRuntime' => false, + 'reason' => 'runtime_unavailable', + ) + ); + } +} diff --git a/src/api/FeatureFlags/WarningEmitter.php b/src/api/FeatureFlags/WarningEmitter.php new file mode 100644 index 00000000000..792b8a77be7 --- /dev/null +++ b/src/api/FeatureFlags/WarningEmitter.php @@ -0,0 +1,12 @@ +setSuccess('bool.flag', true) + ->setSuccess('string.flag', 'blue') + ->setSuccess('integer.flag', 42) + ->setSuccess('float.flag', 3.5) + ->setSuccess('object.flag', array('enabled' => true)); + + $client = Client::create($evaluator, new RecordingWarningEmitter()); + + $this->assertTrue($client->getBooleanValue('bool.flag', false)); + $this->assertSame('blue', $client->getStringValue('string.flag', 'red')); + $this->assertSame(42, $client->getIntegerValue('integer.flag', 0)); + $this->assertSame(3.5, $client->getFloatValue('float.flag', 0.0)); + $this->assertSame(array('enabled' => true), $client->getObjectValue('object.flag', array())); + } + + public function testDetailsMethodsExposeEvaluationDetails() + { + $evaluator = new ClientTestEvaluator(); + $evaluator->setSuccess( + 'checkout-redesign', + true, + EvaluationReason::SPLIT, + 'treatment', + array('owner' => 'ffe'), + array('allocationKey' => 'alloc-1'), + array('runtime' => 'test', 'hasConfig' => true) + ); + + $client = Client::create($evaluator, new RecordingWarningEmitter()); + + $details = $client->getBooleanDetails('checkout-redesign', false); + + $this->assertTrue($details->getValue()); + $this->assertSame(EvaluationType::BOOLEAN, $details->getValueType()); + $this->assertSame(EvaluationReason::SPLIT, $details->getReason()); + $this->assertSame('treatment', $details->getVariant()); + $this->assertSame(array('owner' => 'ffe'), $details->getFlagMetadata()); + $this->assertSame(array('allocationKey' => 'alloc-1'), $details->getExposureData()); + $this->assertSame(array('runtime' => 'test', 'hasConfig' => true), $details->getProviderState()); + } + + public function testContextNormalizesTargetingKeyAndPrimitiveAttributes() + { + $evaluator = new ClientTestEvaluator(); + $evaluator->setSuccess('flag.context', 'on'); + + $client = Client::create($evaluator, new RecordingWarningEmitter()); + $client->getStringValue('flag.context', 'off', array( + 'targetingKey' => 123, + 'attributes' => array( + 'plan' => 'pro', + 'age' => 41, + 'rate' => 1.5, + 'beta' => true, + 'nested' => array('drop'), + 'null' => null, + 'object' => new \stdClass(), + ), + )); + + $calls = $evaluator->getCalls(); + $this->assertCount(1, $calls); + $this->assertSame('123', $calls[0]['targetingKey']); + $this->assertSame(array( + 'plan' => 'pro', + 'age' => 41, + 'rate' => 1.5, + 'beta' => true, + ), $calls[0]['attributes']); + } + + public function testUnavailableRuntimeReturnsDefaultWithProviderNotReadyDetailsAndWarning() + { + $warnings = new RecordingWarningEmitter(); + $client = Client::create(null, $warnings); + + $value = $client->getBooleanValue('checkout-redesign', true); + $details = $client->getStringDetails('checkout-copy', 'fallback'); + + $this->assertTrue($value); + $this->assertSame('fallback', $details->getValue()); + $this->assertSame(EvaluationReason::ERROR, $details->getReason()); + $this->assertSame(EvaluationErrorCode::PROVIDER_NOT_READY, $details->getErrorCode()); + $this->assertContains($details->getErrorMessage(), array( + NativeEvaluator::WARNING_MESSAGE, + UnavailableEvaluator::WARNING_MESSAGE, + )); + + $providerState = $details->getProviderState(); + $this->assertSame(false, $providerState['ready']); + $this->assertSame(false, $providerState['productionRuntime']); + $this->assertTrue(in_array($providerState['reason'], array( + 'configuration_missing', + 'runtime_unavailable', + ), true)); + $this->assertSame(array($details->getErrorMessage()), $warnings->warnings()); + } + + public function testWarningIsEmittedOncePerClientNotOncePerEvaluation() + { + $warnings = new RecordingWarningEmitter(); + $client = Client::create(null, $warnings); + + $client->getBooleanValue('flag-1', false); + $client->getBooleanValue('flag-2', false); + $client->getStringDetails('flag-3', 'fallback'); + + $this->assertCount(1, $warnings->warnings()); + } + + /** + * @dataProvider invalidDefaultProvider + */ + public function testTypedMethodsRejectInvalidDefaults($method, $defaultValue) + { + $client = Client::create(new ClientTestEvaluator(), new RecordingWarningEmitter()); + + $this->expectException(\InvalidArgumentException::class); + + $client->$method('flag.invalid', $defaultValue); + } + + public function invalidDefaultProvider() + { + return array( + 'boolean' => array('getBooleanDetails', 'false'), + 'string' => array('getStringDetails', false), + 'integer' => array('getIntegerDetails', 1.2), + 'float' => array('getFloatDetails', '1.2'), + ); + } +} + +final class ClientTestEvaluator implements Evaluator +{ + private $details = array(); + private $calls = array(); + + public function setSuccess( + $flagKey, + $value, + $reason = EvaluationReason::STATIC_REASON, + $variant = null, + array $metadata = array(), + array $exposureData = array(), + array $providerState = array() + ) { + $this->details[$flagKey] = new EvaluationDetails( + $value, + $this->typeForValue($value), + $reason, + $variant, + null, + null, + $metadata, + $exposureData, + $providerState + ); + + return $this; + } + + public function evaluate($flagKey, $expectedType, $defaultValue, $targetingKey = null, array $attributes = array()) + { + $this->calls[] = array( + 'flagKey' => $flagKey, + 'targetingKey' => $targetingKey, + 'attributes' => $attributes, + ); + + if (array_key_exists($flagKey, $this->details)) { + return $this->details[$flagKey]; + } + + return new EvaluationDetails( + $defaultValue, + $expectedType, + EvaluationReason::ERROR, + null, + EvaluationErrorCode::PROVIDER_NOT_READY, + UnavailableEvaluator::WARNING_MESSAGE, + array(), + array(), + array('ready' => false, 'productionRuntime' => false, 'reason' => 'test_missing_result') + ); + } + + public function getCalls() + { + return $this->calls; + } + + private function typeForValue($value) + { + if (is_bool($value)) { + return EvaluationType::BOOLEAN; + } + if (is_int($value)) { + return EvaluationType::INTEGER; + } + if (is_float($value)) { + return EvaluationType::FLOAT; + } + if (is_array($value)) { + return EvaluationType::OBJECT; + } + return EvaluationType::STRING; + } +} + +final class RecordingWarningEmitter implements WarningEmitter +{ + private $warnings = array(); + + public function warning($message) + { + $this->warnings[] = $message; + } + + public function warnings() + { + return $this->warnings; + } +} diff --git a/tests/api/Unit/FeatureFlags/ExposureWriterTest.php b/tests/api/Unit/FeatureFlags/ExposureWriterTest.php new file mode 100644 index 00000000000..305e92e3cea --- /dev/null +++ b/tests/api/Unit/FeatureFlags/ExposureWriterTest.php @@ -0,0 +1,146 @@ +setDetails('checkout-redesign', new EvaluationDetails( + true, + EvaluationType::BOOLEAN, + EvaluationReason::SPLIT, + 'treatment', + null, + null, + array('owner' => 'ffe'), + array('allocationKey' => 'alloc-1', 'doLog' => true) + )); + + $client = Client::create( + $evaluator, + new ExposureTestWarningEmitter(), + $writer, + new NoopMetricsRecorder() + ); + + $client->getBooleanValue('checkout-redesign', false, array( + 'targetingKey' => 'user-123', + 'attributes' => array('plan' => 'pro'), + )); + + $this->assertSame(array(array( + 'flagKey' => 'checkout-redesign', + 'targetingKey' => 'user-123', + 'attributes' => array('plan' => 'pro'), + 'value' => true, + 'valueType' => 'boolean', + 'reason' => EvaluationReason::SPLIT, + 'variant' => 'treatment', + 'flagMetadata' => array('owner' => 'ffe'), + 'exposureData' => array('allocationKey' => 'alloc-1', 'doLog' => true), + 'allocationKey' => 'alloc-1', + 'doLog' => true, + )), $writer->events()); + } + + public function testClientSkipsExposureWhenDoLogFalseOrEvaluationErrors() + { + $writer = new RecordingExposureWriter(); + + $evaluator = new ExposureTestEvaluator(); + $evaluator + ->setDetails('do-not-log', new EvaluationDetails( + true, + EvaluationType::BOOLEAN, + EvaluationReason::SPLIT, + 'control', + null, + null, + array(), + array('allocationKey' => 'alloc-1', 'doLog' => false) + )) + ->setDetails('provider-not-ready', new EvaluationDetails( + false, + EvaluationType::BOOLEAN, + EvaluationReason::ERROR, + null, + EvaluationErrorCode::PROVIDER_NOT_READY, + UnavailableEvaluator::WARNING_MESSAGE, + array(), + array(), + array('ready' => false, 'productionRuntime' => false) + )); + + $client = Client::create( + $evaluator, + new ExposureTestWarningEmitter(), + $writer, + new NoopMetricsRecorder() + ); + + $client->getBooleanValue('do-not-log', false); + $client->getBooleanValue('provider-not-ready', false); + + $this->assertSame(array(), $writer->events()); + } +} + +final class RecordingExposureWriter implements ExposureWriter +{ + private $events = array(); + + public function write(array $event) + { + $this->events[] = $event; + } + + public function flush() + { + } + + public function events() + { + return $this->events; + } +} + +final class ExposureTestEvaluator implements Evaluator +{ + private $details = array(); + + public function setDetails($flagKey, EvaluationDetails $details) + { + $this->details[$flagKey] = $details; + return $this; + } + + public function evaluate($flagKey, $expectedType, $defaultValue, $targetingKey = null, array $attributes = array()) + { + return array_key_exists($flagKey, $this->details) + ? $this->details[$flagKey] + : new EvaluationDetails($defaultValue, $expectedType, EvaluationReason::ERROR); + } +} + +final class ExposureTestWarningEmitter implements WarningEmitter +{ + public function warning($message) + { + } +} diff --git a/tests/api/Unit/FeatureFlags/RemoteConfigClientTest.php b/tests/api/Unit/FeatureFlags/RemoteConfigClientTest.php new file mode 100644 index 00000000000..bd0e75cab75 --- /dev/null +++ b/tests/api/Unit/FeatureFlags/RemoteConfigClientTest.php @@ -0,0 +1,55 @@ +assertTrue($client->hasConfig()); + $this->assertSame(42, $client->configVersion()); + } + + public function testWaitUntilReadyPollsUntilConfigArrives() + { + $attempts = 0; + $client = new RemoteConfigClient( + function () use (&$attempts) { + ++$attempts; + return $attempts >= 2; + }, + function () { + return 0; + } + ); + + $this->assertTrue($client->waitUntilReady(0.05, 1000)); + $this->assertGreaterThanOrEqual(2, $attempts); + } + + public function testZeroTimeoutDoesNotBlock() + { + $client = new RemoteConfigClient( + function () { + return false; + }, + function () { + return 0; + } + ); + + $this->assertFalse($client->waitUntilReady(0)); + } +} diff --git a/tests/api/Unit/FeatureFlags/ResultMapperTest.php b/tests/api/Unit/FeatureFlags/ResultMapperTest.php new file mode 100644 index 00000000000..f4a0d7ccd59 --- /dev/null +++ b/tests/api/Unit/FeatureFlags/ResultMapperTest.php @@ -0,0 +1,162 @@ +map(array( + 'value_json' => '"blue"', + 'variant' => 'variant-a', + 'allocation_key' => 'alloc-1', + 'reason' => ResultMapper::BRIDGE_REASON_TARGETING_MATCH, + 'error_code' => ResultMapper::BRIDGE_ERROR_NONE, + 'do_log' => true, + 'flag_metadata' => array('owner' => 'ffe'), + 'provider_state' => array('ready' => true), + 'has_config' => true, + 'config_version' => 42, + ), EvaluationType::STRING, 'red'); + + $this->assertSame('blue', $details->getValue()); + $this->assertSame(EvaluationType::STRING, $details->getValueType()); + $this->assertSame(EvaluationReason::TARGETING_MATCH, $details->getReason()); + $this->assertSame('variant-a', $details->getVariant()); + $this->assertNull($details->getErrorCode()); + $this->assertFalse($details->isError()); + $this->assertSame(array('owner' => 'ffe'), $details->getFlagMetadata()); + $this->assertSame(array('allocationKey' => 'alloc-1', 'doLog' => true), $details->getExposureData()); + $this->assertSame( + array('ready' => true, 'hasConfig' => true, 'configVersion' => 42), + $details->getProviderState() + ); + } + + public function testNonZeroErrorReturnsDefaultAndForcesErrorReason() + { + $details = (new ResultMapper())->map(array( + 'value_json' => '"ignored"', + 'variant' => 'ignored-variant', + 'reason' => ResultMapper::BRIDGE_REASON_TARGETING_MATCH, + 'error_code' => ResultMapper::BRIDGE_ERROR_FLAG_UNRECOGNIZED, + 'error_message' => 'Unknown flag', + ), EvaluationType::STRING, 'fallback'); + + $this->assertSame('fallback', $details->getValue()); + $this->assertSame(EvaluationReason::ERROR, $details->getReason()); + $this->assertNull($details->getVariant()); + $this->assertSame(EvaluationErrorCode::FLAG_NOT_FOUND, $details->getErrorCode()); + $this->assertSame('Unknown flag', $details->getErrorMessage()); + $this->assertTrue($details->isError()); + } + + public function testNullResultMapsToProviderNotReady() + { + $details = (new ResultMapper())->map(null, EvaluationType::BOOLEAN, true); + + $this->assertTrue($details->getValue()); + $this->assertSame(EvaluationReason::ERROR, $details->getReason()); + $this->assertSame(EvaluationErrorCode::PROVIDER_NOT_READY, $details->getErrorCode()); + $this->assertSame('FFE evaluator is not ready', $details->getErrorMessage()); + $this->assertSame(array('ready' => false), $details->getProviderState()); + } + + public function testConfigMissingErrorMapsToProviderNotReady() + { + $details = (new ResultMapper())->map(array( + 'value_json' => 'null', + 'reason' => ResultMapper::BRIDGE_REASON_ERROR, + 'error_code' => ResultMapper::BRIDGE_ERROR_CONFIG_MISSING, + 'provider_state' => array('hasConfig' => false), + ), EvaluationType::BOOLEAN, false); + + $this->assertFalse($details->getValue()); + $this->assertSame(EvaluationErrorCode::PROVIDER_NOT_READY, $details->getErrorCode()); + $this->assertSame(array('hasConfig' => false), $details->getProviderState()); + } + + public function testInvalidJsonMapsToParseError() + { + $details = (new ResultMapper())->map(array( + 'value_json' => '{bad-json', + 'reason' => ResultMapper::BRIDGE_REASON_TARGETING_MATCH, + 'error_code' => ResultMapper::BRIDGE_ERROR_NONE, + ), EvaluationType::OBJECT, array('fallback' => true)); + + $this->assertSame(array('fallback' => true), $details->getValue()); + $this->assertSame(EvaluationReason::ERROR, $details->getReason()); + $this->assertSame(EvaluationErrorCode::PARSE_ERROR, $details->getErrorCode()); + } + + public function testDecodedTypeMismatchMapsToTypeMismatch() + { + $details = (new ResultMapper())->map(array( + 'value_json' => '"not-a-bool"', + 'reason' => ResultMapper::BRIDGE_REASON_TARGETING_MATCH, + 'error_code' => ResultMapper::BRIDGE_ERROR_NONE, + ), EvaluationType::BOOLEAN, false); + + $this->assertFalse($details->getValue()); + $this->assertSame(EvaluationReason::ERROR, $details->getReason()); + $this->assertSame(EvaluationErrorCode::TYPE_MISMATCH, $details->getErrorCode()); + } + + public function testIntegerJsonCanMapToFloat() + { + $details = (new ResultMapper())->map(array( + 'value_json' => '10', + 'reason' => ResultMapper::BRIDGE_REASON_SPLIT, + 'error_code' => ResultMapper::BRIDGE_ERROR_NONE, + ), EvaluationType::FLOAT, 0.0); + + $this->assertSame(10.0, $details->getValue()); + $this->assertSame(EvaluationReason::SPLIT, $details->getReason()); + } + + /** + * @dataProvider reasonProvider + */ + public function testReasonMapping($bridgeReason, $expectedReason) + { + $details = (new ResultMapper())->map(array( + 'value_json' => 'true', + 'reason' => $bridgeReason, + 'error_code' => ResultMapper::BRIDGE_ERROR_NONE, + ), EvaluationType::BOOLEAN, false); + + $this->assertSame($expectedReason, $details->getReason()); + } + + public function reasonProvider() + { + return array( + 'static' => array(ResultMapper::BRIDGE_REASON_STATIC, EvaluationReason::STATIC_REASON), + 'default' => array(ResultMapper::BRIDGE_REASON_DEFAULT, EvaluationReason::DEFAULT_REASON), + 'targeting match' => array(ResultMapper::BRIDGE_REASON_TARGETING_MATCH, EvaluationReason::TARGETING_MATCH), + 'split' => array(ResultMapper::BRIDGE_REASON_SPLIT, EvaluationReason::SPLIT), + 'disabled' => array(ResultMapper::BRIDGE_REASON_DISABLED, EvaluationReason::DISABLED), + 'error' => array(ResultMapper::BRIDGE_REASON_ERROR, EvaluationReason::ERROR), + ); + } + + public function testExistingEvaluationDetailsPassThrough() + { + $existing = new EvaluationDetails( + 'kept', + EvaluationType::STRING, + EvaluationReason::DEFAULT_REASON + ); + + $details = (new ResultMapper())->map($existing, EvaluationType::STRING, 'fallback'); + + $this->assertSame($existing, $details); + } +} diff --git a/tests/ext/ffe/flush_drains_buffer.phpt b/tests/ext/ffe/flush_drains_buffer.phpt new file mode 100644 index 00000000000..ef5d306c88e --- /dev/null +++ b/tests/ext/ffe/flush_drains_buffer.phpt @@ -0,0 +1,49 @@ +--TEST-- +FFE: native exposure flush drains the batch buffer +--SKIPIF-- + +--ENV-- +DD_TRACE_ENABLED=0 +--INI-- +datadog.trace.generate_root_span=0 +--FILE-- + 1713382853716, + 'flag' => ['key' => 'demo-flag'], + 'allocation' => ['key' => 'alloc-a'], + 'variant' => ['key' => 'on'], + 'subject' => ['id' => 'user-1', 'attributes' => new stdClass()], +]); + +var_dump(DDTrace\ffe_send_exposure($event, 'demo-flag', 'alloc-a', 'user-1', 'on')); +var_dump(DDTrace\ffe_send_exposure($event, 'demo-flag', 'alloc-a', 'user-1', 'on')); +var_dump(DDTrace\ffe_send_exposure($event, 'demo-flag', 'alloc-a', 'user-1', 'off')); + +$payload = DDTrace\ffe_flush_exposures(); +var_dump(is_string($payload) && strlen($payload) > 0); + +$decoded = json_decode($payload, true); +var_dump($decoded['context']['service'] === 'svc-flush'); +var_dump($decoded['context']['env'] === 'test'); +var_dump($decoded['context']['version'] === '9.9.9'); +var_dump(count($decoded['exposures'])); +var_dump(DDTrace\ffe_flush_exposures()); + +?> +--EXPECT-- +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +int(2) +NULL diff --git a/tests/ext/ffe/fork_resets_dedup.phpt b/tests/ext/ffe/fork_resets_dedup.phpt new file mode 100644 index 00000000000..a7b4e9f09c0 --- /dev/null +++ b/tests/ext/ffe/fork_resets_dedup.phpt @@ -0,0 +1,52 @@ +--TEST-- +FFE: fork handler resets exposure dedup in child +--SKIPIF-- + +--ENV-- +DD_TRACE_ENABLED=0 +--INI-- +datadog.trace.generate_root_span=0 +--FILE-- + 1, + 'flag' => ['key' => 'f'], + 'allocation' => ['key' => 'a'], + 'variant' => ['key' => 'on'], + 'subject' => ['id' => 'u', 'attributes' => new stdClass()], +]); + +$parentFirst = DDTrace\ffe_send_exposure($event, 'f', 'a', 'u', 'on'); +echo 'parent_first=' . ($parentFirst ? 'true' : 'false') . "\n"; + +$pid = pcntl_fork(); +if ($pid === -1) { + die('fork failed'); +} + +if ($pid === 0) { + DDTrace\Internal\handle_fork(); + $child = DDTrace\ffe_send_exposure($event, 'f', 'a', 'u', 'on'); + echo 'child=' . ($child ? 'true' : 'false') . "\n"; + DDTrace\ffe_reset_exposure_state(); + exit(0); +} + +pcntl_wait($status); + +$parentSecond = DDTrace\ffe_send_exposure($event, 'f', 'a', 'u', 'on'); +echo 'parent_second=' . ($parentSecond ? 'true' : 'false') . "\n"; + +?> +--EXPECTF-- +parent_first=true +child=true +parent_second=false diff --git a/tests/ext/ffe/native_bridge_evaluate.phpt b/tests/ext/ffe/native_bridge_evaluate.phpt new file mode 100644 index 00000000000..ab5f2d64e80 --- /dev/null +++ b/tests/ext/ffe/native_bridge_evaluate.phpt @@ -0,0 +1,67 @@ +--TEST-- +FFE native bridge evaluates through libdatadog +--FILE-- + 'US', + 'age' => 42, + 'ignored' => array('drop'), +))); +show('missing', \DDTrace\ffe_evaluate('missing.flag', 0, 'user-1', array())); +show('type_mismatch', \DDTrace\ffe_evaluate('string.flag', 3, 'user-1', array())); +show('parse_error', \DDTrace\ffe_evaluate('bad.flag', 0, 'user-1', array())); +?> +--EXPECT-- +has_config_before=false +provider_not_ready={"value_json":"null","variant":null,"allocation_key":null,"reason":5,"error_code":6,"do_log":false} +load=true +has_config_after=true +success={"value_json":"\"blue\"","variant":"blue","allocation_key":"alloc-string","reason":0,"error_code":0,"do_log":true} +missing={"value_json":"null","variant":null,"allocation_key":null,"reason":1,"error_code":3,"do_log":false} +type_mismatch={"value_json":"null","variant":null,"allocation_key":null,"reason":5,"error_code":1,"do_log":false} +parse_error={"value_json":"null","variant":null,"allocation_key":null,"reason":5,"error_code":2,"do_log":false} diff --git a/tests/ext/ffe/remote_config_lifecycle.phpt b/tests/ext/ffe/remote_config_lifecycle.phpt new file mode 100644 index 00000000000..bfcb6351e89 --- /dev/null +++ b/tests/ext/ffe/remote_config_lifecycle.phpt @@ -0,0 +1,90 @@ +--TEST-- +FFE Remote Config loads and removes UFC config +--SKIPIF-- + +--ENV-- +DD_AGENT_HOST=request-replayer +DD_TRACE_AGENT_PORT=80 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS=0.01 +DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=1 +--INI-- +datadog.trace.agent_test_session_token=ffe/remote_config_lifecycle +--FILE-- + $version); +?> +--CLEAN-- + +--EXPECT-- +before=false +loaded=true +has_config_after_add=true +success={"value_json":"\"blue\"","variant":"blue","allocation_key":"alloc-string","reason":0,"error_code":0,"do_log":true} +removed=true +has_config_after_remove=false +version_increased=true diff --git a/tests/internal-api-stress-test.php b/tests/internal-api-stress-test.php index 671bd0e03ce..e4ee45d3ed7 100644 --- a/tests/internal-api-stress-test.php +++ b/tests/internal-api-stress-test.php @@ -131,7 +131,12 @@ function ($hook = null) { return $garbage; } -$minFunctionArgs = []; +$minFunctionArgs = [ + 'DDTrace\ffe_load_config' => 1, + 'DDTrace\ffe_evaluate' => 4, + 'DDTrace\ffe_send_exposure' => 5, + 'DDTrace\ffe_set_service_context' => 3, +]; function call_function(ReflectionFunction $function) { From 782f62272015a21fe86204eeb552947219c7e43f Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Fri, 22 May 2026 04:28:33 -0400 Subject: [PATCH 02/14] feat(ffe): package runtime evaluation milestone --- .github/CODEOWNERS | 4 + .github/dependabot.yml | 7 + .gitlab-ci.yml | 4 +- .gitlab/generate-tracer.php | 20 ++ .gitmodules | 3 + Cargo.lock | 15 - Makefile | 3 + components-rs/Cargo.toml | 1 - components-rs/ddtrace.h | 16 - components-rs/ffe.rs | 224 ------------ components-rs/sidecar.h | 5 - composer.json | 8 + ext/autoload_php_files.c | 17 + ext/ddtrace.c | 65 ---- ext/ddtrace.stub.php | 30 -- ext/ddtrace_arginfo.h | 27 -- ext/sidecar.c | 25 -- libdatadog | 2 +- src/DDTrace/OpenFeature/DataDogProvider.php | 192 +++++++++++ .../OpenFeature/NoopWarningEmitter.php | 14 + src/api/FeatureFlags/Client.php | 65 +--- src/api/FeatureFlags/ExposureWriter.php | 17 - src/api/FeatureFlags/MetricsRecorder.php | 12 - src/api/FeatureFlags/NativeExposureWriter.php | 126 ------- src/api/FeatureFlags/NoopExposureWriter.php | 14 - src/api/FeatureFlags/NoopMetricsRecorder.php | 10 - src/api/FeatureFlags/RemoteConfigClient.php | 21 -- src/bridge/_files_openfeature.php | 6 + tests/FeatureFlags/ffe-system-test-data | 1 + tests/OpenFeature/DataDogProviderTest.php | 326 ++++++++++++++++++ tests/OpenFeature/composer.json | 7 + .../Unit/FeatureFlags/ExposureWriterTest.php | 146 -------- .../FeatureFlags/RemoteConfigClientTest.php | 22 +- .../Unit/FeatureFlags/ResultMapperTest.php | 21 ++ tests/ext/ffe/flush_drains_buffer.phpt | 49 --- tests/ext/ffe/fork_resets_dedup.phpt | 52 --- tests/ext/ffe/native_bridge_evaluate.phpt | 25 ++ tests/ext/ffe/system_test_data_evaluate.phpt | 234 +++++++++++++ tests/internal-api-stress-test.php | 2 - tests/phpunit.xml | 5 +- tooling/generation/composer.json | 4 +- 41 files changed, 904 insertions(+), 943 deletions(-) create mode 100644 src/DDTrace/OpenFeature/DataDogProvider.php create mode 100644 src/DDTrace/OpenFeature/NoopWarningEmitter.php delete mode 100644 src/api/FeatureFlags/ExposureWriter.php delete mode 100644 src/api/FeatureFlags/MetricsRecorder.php delete mode 100644 src/api/FeatureFlags/NativeExposureWriter.php delete mode 100644 src/api/FeatureFlags/NoopExposureWriter.php delete mode 100644 src/api/FeatureFlags/NoopMetricsRecorder.php create mode 100644 src/bridge/_files_openfeature.php create mode 160000 tests/FeatureFlags/ffe-system-test-data create mode 100644 tests/OpenFeature/DataDogProviderTest.php create mode 100644 tests/OpenFeature/composer.json delete mode 100644 tests/api/Unit/FeatureFlags/ExposureWriterTest.php delete mode 100644 tests/ext/ffe/flush_drains_buffer.phpt delete mode 100644 tests/ext/ffe/fork_resets_dedup.phpt create mode 100644 tests/ext/ffe/system_test_data_evaluate.phpt diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bd705e8c881..8c90cfd1fc6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -18,7 +18,11 @@ compile_rust.sh @Datadog/libdatadog-apm /components-rs/ffe.rs @Datadog/libdatadog-apm @DataDog/feature-flagging-and-experimentation-sdk /libdatadog @Datadog/libdatadog-apm @DataDog/feature-flagging-and-experimentation-sdk /src/api/FeatureFlags/ @DataDog/feature-flagging-and-experimentation-sdk +/src/DDTrace/OpenFeature/ @DataDog/feature-flagging-and-experimentation-sdk +/src/bridge/_files_openfeature.php @DataDog/feature-flagging-and-experimentation-sdk +/tests/FeatureFlags/ @DataDog/feature-flagging-and-experimentation-sdk /tests/api/Unit/FeatureFlags/ @DataDog/feature-flagging-and-experimentation-sdk +/tests/OpenFeature/ @DataDog/feature-flagging-and-experimentation-sdk /tests/ext/ffe/ @DataDog/feature-flagging-and-experimentation-sdk # Release files diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 144fd65c722..ccb4a0e2cf1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,13 @@ version: 2 updates: + - package-ecosystem: "gitsubmodule" + directory: "/" + schedule: + interval: "weekly" + cooldown: + default-days: 2 + - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d5ea7b8eab1..e825784f383 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,8 +6,8 @@ stages: variables: GIT_SUBMODULE_STRATEGY: recursive - # Only clone libdatadog submodule by default - GIT_SUBMODULE_PATHS: libdatadog + # Only clone submodules required by default test jobs + GIT_SUBMODULE_PATHS: libdatadog tests/FeatureFlags/ffe-system-test-data RELIABILITY_ENV_BRANCH: value: "master" description: "Run a specific datadog-reliability-env branch downstream" diff --git a/.gitlab/generate-tracer.php b/.gitlab/generate-tracer.php index 42510ff8ce5..eed16d70e36 100644 --- a/.gitlab/generate-tracer.php +++ b/.gitlab/generate-tracer.php @@ -386,6 +386,26 @@ function before_script_steps($with_docker_auth = false) { - make test_unit PHPUNIT_JUNIT="artifacts/tests/php-tests.xml" +=")): ?> +"Feature flags tests: []": + extends: .debug_test + needs: + - job: "compile extension: debug" + parallel: + matrix: + - PHP_MAJOR_MINOR: "" + ARCH: "amd64" + artifacts: true + - job: "Prepare code" + artifacts: true + variables: + PHP_MAJOR_MINOR: "" + ARCH: "amd64" + script: + - make test_featureflags PHPUNIT_JUNIT="artifacts/tests/php-tests.xml" + + + "API unit tests: []": extends: .debug_test needs: diff --git a/.gitmodules b/.gitmodules index 16212a50047..58f2a29bf75 100644 --- a/.gitmodules +++ b/.gitmodules @@ -18,3 +18,6 @@ path = appsec/third_party/libddwaf-rust url = https://github.com/DataDog/libddwaf-rust.git branch = glopes/v2 +[submodule "tests/FeatureFlags/ffe-system-test-data"] + path = tests/FeatureFlags/ffe-system-test-data + url = https://github.com/DataDog/ffe-system-test-data diff --git a/Cargo.lock b/Cargo.lock index ce7aba7a65f..7a0fe64ab9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1475,7 +1475,6 @@ dependencies = [ "libdd-trace-stats", "libdd-trace-utils", "log", - "lru", "paste", "regex", "regex-automata", @@ -2083,11 +2082,6 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] [[package]] name = "hdrhistogram" @@ -3293,15 +3287,6 @@ dependencies = [ "value-bag", ] -[[package]] -name = "lru" -version = "0.16.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" -dependencies = [ - "hashbrown 0.16.1", -] - [[package]] name = "mach2" version = "0.5.0" diff --git a/Makefile b/Makefile index 1d017c7308b..319ebd67489 100644 --- a/Makefile +++ b/Makefile @@ -1328,6 +1328,9 @@ test_distributed_tracing_coverage: test_metrics: global_test_run_dependencies $(call run_tests,--testsuite=metrics $(TESTS)) +test_featureflags: global_test_run_dependencies tests/OpenFeature/composer.lock-php$(PHP_MAJOR_MINOR) + $(call run_tests,--testsuite=featureflags $(TESTS)) + benchmarks_run_dependencies: global_test_run_dependencies tests/Frameworks/Symfony/Version_5_2/composer.lock-php$(PHP_MAJOR_MINOR) tests/Frameworks/Laravel/Version_10_x/composer.lock-php$(PHP_MAJOR_MINOR) tests/Benchmarks/composer.lock-php$(PHP_MAJOR_MINOR) php tests/Frameworks/Symfony/Version_5_2/bin/console cache:clear --no-warmup --env=prod diff --git a/components-rs/Cargo.toml b/components-rs/Cargo.toml index 264348ab8a6..35ba698004f 100644 --- a/components-rs/Cargo.toml +++ b/components-rs/Cargo.toml @@ -33,7 +33,6 @@ serde = "1.0.196" simd-json = "0.14.1" serde_with = "3.6.0" lazy_static = "1.4" -lru = "0.16.4" log = "0.4.20" env_logger = "0.10.1" zwohash = "0.1.2" diff --git a/components-rs/ddtrace.h b/components-rs/ddtrace.h index a4410f50bac..da498887283 100644 --- a/components-rs/ddtrace.h +++ b/components-rs/ddtrace.h @@ -87,22 +87,6 @@ bool ddog_ffe_result_do_log(const struct ddog_FfeResult *result); void ddog_ffe_free_result(struct ddog_FfeResult *result); -void ddog_ffe_set_service_context(const char *service, - const char *env, - const char *version); - -bool ddog_ffe_enqueue_exposure(const char *event_json, - const char *flag_key, - const char *allocation_key, - const char *targeting_key, - const char *variant_key); - -ddog_CharSlice ddog_ffe_flush_exposures(void); - -void ddog_ffe_free_flush_result(ddog_CharSlice slice); - -void ddog_ffe_reset_exposure_state(void); - const char *ddog_normalize_process_tag_value(ddog_CharSlice tag_value); void ddog_free_normalized_tag_value(const char *ptr); diff --git a/components-rs/ffe.rs b/components-rs/ffe.rs index 9fee4a176f2..2f20ad3409a 100644 --- a/components-rs/ffe.rs +++ b/components-rs/ffe.rs @@ -2,11 +2,8 @@ use datadog_ffe::rules_based::{ self as ffe, AssignmentReason, AssignmentValue, Attribute, Configuration, EvaluationContext, EvaluationError, ExpectedFlagType, Str, UniversalFlagConfig, }; -use libdd_common_ffi::CharSlice; -use lru::LruCache; use std::collections::HashMap; use std::ffi::{c_char, CStr, CString}; -use std::num::NonZeroUsize; use std::sync::{Arc, Mutex}; struct FfeState { @@ -334,224 +331,3 @@ fn assignment_value_to_json(value: &AssignmentValue) -> String { fn string_to_cstring(value: String) -> CString { CString::new(value).unwrap_or_default() } - -struct ServiceContext { - service: String, - env: String, - version: String, -} - -struct ExposureState { - dedup_cache: LruCache<(String, String), (String, String)>, - batch_buffer: Vec, - service_context: Option, -} - -const EXPOSURE_DEDUP_LIMIT: usize = 65_536; -const EXPOSURE_BATCH_LIMIT: usize = 1_000; - -lazy_static::lazy_static! { - static ref EXPOSURE_STATE: Mutex = Mutex::new(ExposureState { - dedup_cache: LruCache::new(NonZeroUsize::new(EXPOSURE_DEDUP_LIMIT).unwrap()), - batch_buffer: Vec::new(), - service_context: None, - }); -} - -#[no_mangle] -pub unsafe extern "C" fn ddog_ffe_set_service_context( - service: *const c_char, - env: *const c_char, - version: *const c_char, -) { - if let Ok(mut state) = EXPOSURE_STATE.lock() { - state.service_context = Some(ServiceContext { - service: optional_cstr_to_string(service), - env: optional_cstr_to_string(env), - version: optional_cstr_to_string(version), - }); - } -} - -#[no_mangle] -pub unsafe extern "C" fn ddog_ffe_enqueue_exposure( - event_json: *const c_char, - flag_key: *const c_char, - allocation_key: *const c_char, - targeting_key: *const c_char, - variant_key: *const c_char, -) -> bool { - if event_json.is_null() || flag_key.is_null() || variant_key.is_null() { - return false; - } - - let event = match required_cstr_to_string(event_json) { - Some(event) => event, - None => return false, - }; - let flag = match required_cstr_to_string(flag_key) { - Some(flag) => flag, - None => return false, - }; - let variant = match required_cstr_to_string(variant_key) { - Some(variant) => variant, - None => return false, - }; - let allocation = optional_cstr_to_string(allocation_key); - let targeting = optional_cstr_to_string(targeting_key); - - let dedup_key = (flag, targeting); - let dedup_value = (allocation, variant); - - if let Ok(mut state) = EXPOSURE_STATE.lock() { - if let Some(cached) = state.dedup_cache.get(&dedup_key) { - if *cached == dedup_value { - return false; - } - } - - state.dedup_cache.put(dedup_key, dedup_value); - if state.batch_buffer.len() >= EXPOSURE_BATCH_LIMIT { - return false; - } - - state.batch_buffer.push(event); - return true; - } - - false -} - -#[no_mangle] -pub extern "C" fn ddog_ffe_flush_exposures() -> CharSlice<'static> { - if let Ok(mut state) = EXPOSURE_STATE.lock() { - if state.batch_buffer.is_empty() { - return CharSlice::default(); - } - - let events = state.batch_buffer.drain(..).collect::>(); - let context = match &state.service_context { - Some(context) => serde_json::json!({ - "service": context.service.as_str(), - "env": context.env.as_str(), - "version": context.version.as_str(), - }), - None => serde_json::json!({ - "service": "", - "env": "", - "version": "", - }), - }; - - let payload = format!( - r#"{{"context":{},"exposures":[{}]}}"#, - context, - events.join(",") - ); - let mut bytes = payload.into_bytes().into_boxed_slice(); - let ptr = bytes.as_mut_ptr(); - let len = bytes.len(); - std::mem::forget(bytes); - - return unsafe { CharSlice::from_raw_parts(ptr as *const c_char, len) }; - } - - CharSlice::default() -} - -#[no_mangle] -pub unsafe extern "C" fn ddog_ffe_free_flush_result(slice: CharSlice<'static>) { - use libdd_common_ffi::slice::AsBytes; - - let bytes = slice.as_bytes(); - let len = bytes.len(); - let ptr = bytes.as_ptr() as *mut u8; - if !ptr.is_null() && len > 0 { - let _ = Box::from_raw(std::slice::from_raw_parts_mut(ptr, len) as *mut [u8]); - } -} - -#[no_mangle] -pub extern "C" fn ddog_ffe_reset_exposure_state() { - if let Ok(mut state) = EXPOSURE_STATE.lock() { - state.dedup_cache.clear(); - state.batch_buffer.clear(); - state.service_context = None; - } -} - -unsafe fn required_cstr_to_string(value: *const c_char) -> Option { - CStr::from_ptr(value) - .to_str() - .ok() - .map(|value| value.to_string()) -} - -unsafe fn optional_cstr_to_string(value: *const c_char) -> String { - if value.is_null() { - return String::new(); - } - - required_cstr_to_string(value).unwrap_or_default() -} - -#[cfg(test)] -mod tests { - use super::*; - use libdd_common_ffi::slice::AsBytes; - - #[test] - fn exposure_flush_drains_buffer_and_keeps_context() { - ddog_ffe_reset_exposure_state(); - - let service = CString::new("svc-flush").unwrap(); - let env = CString::new("test").unwrap(); - let version = CString::new("1.2.3").unwrap(); - let event = CString::new( - r#"{"timestamp":1,"flag":{"key":"demo"},"allocation":{"key":"alloc-a"},"variant":{"key":"on"},"subject":{"id":"user-1","attributes":{}}}"#, - ) - .unwrap(); - let flag = CString::new("demo").unwrap(); - let allocation = CString::new("alloc-a").unwrap(); - let targeting = CString::new("user-1").unwrap(); - let on = CString::new("on").unwrap(); - let off = CString::new("off").unwrap(); - - unsafe { - ddog_ffe_set_service_context(service.as_ptr(), env.as_ptr(), version.as_ptr()); - assert!(ddog_ffe_enqueue_exposure( - event.as_ptr(), - flag.as_ptr(), - allocation.as_ptr(), - targeting.as_ptr(), - on.as_ptr(), - )); - assert!(!ddog_ffe_enqueue_exposure( - event.as_ptr(), - flag.as_ptr(), - allocation.as_ptr(), - targeting.as_ptr(), - on.as_ptr(), - )); - assert!(ddog_ffe_enqueue_exposure( - event.as_ptr(), - flag.as_ptr(), - allocation.as_ptr(), - targeting.as_ptr(), - off.as_ptr(), - )); - } - - let payload = ddog_ffe_flush_exposures(); - assert!(!payload.as_bytes().is_empty()); - let decoded: serde_json::Value = serde_json::from_slice(payload.as_bytes()).unwrap(); - assert_eq!(decoded["context"]["service"], "svc-flush"); - assert_eq!(decoded["context"]["env"], "test"); - assert_eq!(decoded["context"]["version"], "1.2.3"); - assert_eq!(decoded["exposures"].as_array().unwrap().len(), 2); - unsafe { ddog_ffe_free_flush_result(payload) }; - - let empty = ddog_ffe_flush_exposures(); - assert!(empty.as_bytes().is_empty()); - } -} diff --git a/components-rs/sidecar.h b/components-rs/sidecar.h index 45fb1f8e846..4a3aa617416 100644 --- a/components-rs/sidecar.h +++ b/components-rs/sidecar.h @@ -298,11 +298,6 @@ ddog_MaybeError ddog_sidecar_send_debugger_datum(struct ddog_SidecarTransport ** ddog_QueueId queue_id, struct ddog_DebuggerPayload *payload); -ddog_MaybeError ddog_sidecar_send_ffe_exposures(struct ddog_SidecarTransport **transport, - const struct ddog_InstanceId *instance_id, - const ddog_QueueId *queue_id, - ddog_CharSlice payload); - ddog_MaybeError ddog_sidecar_send_debugger_diagnostics(struct ddog_SidecarTransport **transport, const struct ddog_InstanceId *instance_id, ddog_QueueId queue_id, diff --git a/composer.json b/composer.json index eedfbd81763..8823021517f 100644 --- a/composer.json +++ b/composer.json @@ -87,6 +87,14 @@ "create-lockfile": false } }, + "openfeature": { + "require": { + "open-feature/sdk": "^2.1" + }, + "scenario-options": { + "create-lockfile": false + } + }, "opentelemetry1": { "require": { "open-telemetry/sdk": "@stable", diff --git a/ext/autoload_php_files.c b/ext/autoload_php_files.c index 246008d4c19..e698ad083d6 100644 --- a/ext/autoload_php_files.c +++ b/ext/autoload_php_files.c @@ -32,7 +32,9 @@ static zend_class_entry *(*dd_prev_autoloader)(zend_string *name, zend_string *l static zend_bool dd_api_is_preloaded = false; static zend_bool dd_otel_is_preloaded = false; static zend_bool dd_legacy_tracer_is_preloaded = false; +static zend_bool dd_openfeature_is_preloaded = false; #endif +static zend_bool dd_openfeature_is_loaded = false; #if PHP_VERSION_ID < 80000 #define LAST_ERROR_STRING PG(last_error_message) @@ -234,6 +236,18 @@ static zend_class_entry *dd_perform_autoload(zend_string *class_name, zend_strin return ce; } } + if (zend_string_starts_with_literal(lc_name, "ddtrace\\openfeature\\")) { +#if PHP_VERSION_ID >= 80000 + if (!dd_openfeature_is_loaded) { + dd_openfeature_is_loaded = 1; + dd_load_files("openfeature"); + } + if ((ce = zend_hash_find_ptr(EG(class_table), lc_name))) { + return ce; + } +#endif + return NULL; + } if (!DDTRACE_G(legacy_tracer_is_loaded) && !zend_string_starts_with_literal(lc_name, "ddtrace\\integration\\")) { DDTRACE_G(legacy_tracer_is_loaded) = 1; dd_load_files("tracer"); @@ -420,13 +434,16 @@ void ddtrace_autoload_rshutdown(void) { dd_api_is_preloaded = DDTRACE_G(api_is_loaded); dd_otel_is_preloaded = DDTRACE_G(otel_is_loaded); dd_legacy_tracer_is_preloaded = DDTRACE_G(legacy_tracer_is_loaded); + dd_openfeature_is_preloaded = dd_openfeature_is_loaded; } else { DDTRACE_G(api_is_loaded) = dd_api_is_preloaded; DDTRACE_G(otel_is_loaded) = dd_otel_is_preloaded; DDTRACE_G(legacy_tracer_is_loaded) = dd_legacy_tracer_is_preloaded; + dd_openfeature_is_loaded = dd_openfeature_is_preloaded; } #else DDTRACE_G(api_is_loaded) = 0; DDTRACE_G(otel_is_loaded) = 0; + dd_openfeature_is_loaded = 0; #endif } diff --git a/ext/ddtrace.c b/ext/ddtrace.c index 3362c0e003f..1d231a98bd3 100644 --- a/ext/ddtrace.c +++ b/ext/ddtrace.c @@ -3083,71 +3083,6 @@ PHP_FUNCTION(DDTrace_ffe_evaluate) { ddog_ffe_free_result(result); } -PHP_FUNCTION(DDTrace_ffe_send_exposure) { - char *event_json, *flag_key, *variant_key; - size_t event_json_len, flag_key_len, variant_key_len; - char *allocation_key = NULL; - size_t allocation_key_len = 0; - char *targeting_key = NULL; - size_t targeting_key_len = 0; - - ZEND_PARSE_PARAMETERS_START(5, 5) - Z_PARAM_STRING(event_json, event_json_len) - Z_PARAM_STRING(flag_key, flag_key_len) - Z_PARAM_STRING_OR_NULL(allocation_key, allocation_key_len) - Z_PARAM_STRING_OR_NULL(targeting_key, targeting_key_len) - Z_PARAM_STRING(variant_key, variant_key_len) - ZEND_PARSE_PARAMETERS_END(); - - UNUSED(event_json_len); - UNUSED(flag_key_len); - UNUSED(variant_key_len); - - RETURN_BOOL(ddog_ffe_enqueue_exposure( - event_json, - flag_key, - allocation_key_len > 0 ? allocation_key : NULL, - targeting_key_len > 0 ? targeting_key : NULL, - variant_key)); -} - -PHP_FUNCTION(DDTrace_ffe_flush_exposures) { - ddog_CharSlice payload; - - ZEND_PARSE_PARAMETERS_NONE(); - - payload = ddog_ffe_flush_exposures(); - if (payload.ptr == NULL || payload.len == 0) { - RETURN_NULL(); - } - - RETVAL_STRINGL(payload.ptr, payload.len); - ddog_ffe_free_flush_result(payload); -} - -PHP_FUNCTION(DDTrace_ffe_set_service_context) { - char *service, *env, *version; - size_t service_len, env_len, version_len; - - ZEND_PARSE_PARAMETERS_START(3, 3) - Z_PARAM_STRING(service, service_len) - Z_PARAM_STRING(env, env_len) - Z_PARAM_STRING(version, version_len) - ZEND_PARSE_PARAMETERS_END(); - - UNUSED(service_len); - UNUSED(env_len); - UNUSED(version_len); - - ddog_ffe_set_service_context(service, env, version); -} - -PHP_FUNCTION(DDTrace_ffe_reset_exposure_state) { - ZEND_PARSE_PARAMETERS_NONE(); - - ddog_ffe_reset_exposure_state(); -} - PHP_FUNCTION(dd_trace_send_traces_via_thread) { char *payload = NULL; ddtrace_zpplong_t num_traces = 0; diff --git a/ext/ddtrace.stub.php b/ext/ddtrace.stub.php index c1da1c442ed..471137bdafd 100644 --- a/ext/ddtrace.stub.php +++ b/ext/ddtrace.stub.php @@ -888,36 +888,6 @@ function ffe_config_version(): int {} */ function ffe_load_config(string $json): bool {} - /** - * Enqueue a serialized FFE exposure event for native deduplication and batched delivery. - * - * @internal Used by the Datadog feature flag client. - */ - function ffe_send_exposure(string $eventJson, string $flagKey, ?string $allocationKey, ?string $targetingKey, string $variantKey): bool {} - - /** - * Drain buffered FFE exposure events and return the native batch payload. - * Used by tests; production delivery is owned by request/module shutdown. - * - * @return string|null Serialized batch payload, or null when the buffer is empty. - * - * @internal Used by tests. - */ - function ffe_flush_exposures(): ?string {} - - /** - * Set service/env/version context for native FFE exposure batch payloads. - * - * @internal Used by the Datadog feature flag client. - */ - function ffe_set_service_context(string $service, string $env, string $version): void {} - - /** - * Reset native FFE exposure state. - * - * @internal Used by tests and fork handling. - */ - function ffe_reset_exposure_state(): void {} } namespace DDTrace\System { diff --git a/ext/ddtrace_arginfo.h b/ext/ddtrace_arginfo.h index 1424feeb913..d782a8fba24 100644 --- a/ext/ddtrace_arginfo.h +++ b/ext/ddtrace_arginfo.h @@ -193,25 +193,6 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_ffe_load_config, 0, 1, _ ZEND_ARG_TYPE_INFO(0, json, IS_STRING, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_ffe_send_exposure, 0, 5, _IS_BOOL, 0) - ZEND_ARG_TYPE_INFO(0, eventJson, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, flagKey, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, allocationKey, IS_STRING, 1) - ZEND_ARG_TYPE_INFO(0, targetingKey, IS_STRING, 1) - ZEND_ARG_TYPE_INFO(0, variantKey, IS_STRING, 0) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_ffe_flush_exposures, 0, 0, IS_STRING, 1) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_ffe_set_service_context, 0, 3, IS_VOID, 0) - ZEND_ARG_TYPE_INFO(0, service, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, env, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, version, IS_STRING, 0) -ZEND_END_ARG_INFO() - -#define arginfo_DDTrace_ffe_reset_exposure_state arginfo_DDTrace_flush - ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_System_container_id, 0, 0, IS_STRING, 1) ZEND_END_ARG_INFO() @@ -434,10 +415,6 @@ ZEND_FUNCTION(DDTrace_ffe_evaluate); ZEND_FUNCTION(DDTrace_ffe_has_config); ZEND_FUNCTION(DDTrace_ffe_config_version); ZEND_FUNCTION(DDTrace_ffe_load_config); -ZEND_FUNCTION(DDTrace_ffe_send_exposure); -ZEND_FUNCTION(DDTrace_ffe_flush_exposures); -ZEND_FUNCTION(DDTrace_ffe_set_service_context); -ZEND_FUNCTION(DDTrace_ffe_reset_exposure_state); ZEND_FUNCTION(DDTrace_System_container_id); ZEND_FUNCTION(DDTrace_System_process_tags_base_hash); ZEND_FUNCTION(DDTrace_Config_integration_analytics_enabled); @@ -537,10 +514,6 @@ static const zend_function_entry ext_functions[] = { ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_has_config"), zif_DDTrace_ffe_has_config, arginfo_DDTrace_ffe_has_config, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_config_version"), zif_DDTrace_ffe_config_version, arginfo_DDTrace_ffe_config_version, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_load_config"), zif_DDTrace_ffe_load_config, arginfo_DDTrace_ffe_load_config, 0, NULL, NULL) - ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_send_exposure"), zif_DDTrace_ffe_send_exposure, arginfo_DDTrace_ffe_send_exposure, 0, NULL, NULL) - ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_flush_exposures"), zif_DDTrace_ffe_flush_exposures, arginfo_DDTrace_ffe_flush_exposures, 0, NULL, NULL) - ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_set_service_context"), zif_DDTrace_ffe_set_service_context, arginfo_DDTrace_ffe_set_service_context, 0, NULL, NULL) - ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_reset_exposure_state"), zif_DDTrace_ffe_reset_exposure_state, arginfo_DDTrace_ffe_reset_exposure_state, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\System", "container_id"), zif_DDTrace_System_container_id, arginfo_DDTrace_System_container_id, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\System", "process_tags_base_hash"), zif_DDTrace_System_process_tags_base_hash, arginfo_DDTrace_System_process_tags_base_hash, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Config", "integration_analytics_enabled"), zif_DDTrace_Config_integration_analytics_enabled, arginfo_DDTrace_Config_integration_analytics_enabled, 0, NULL, NULL) diff --git a/ext/sidecar.c b/ext/sidecar.c index 99e03846a62..1713e86691e 100644 --- a/ext/sidecar.c +++ b/ext/sidecar.c @@ -444,8 +444,6 @@ void ddtrace_sidecar_handle_fork(void) { bool appsec_config = false; bool enable_sidecar = ddtrace_sidecar_should_enable(&appsec_activation, &appsec_config); - ddog_ffe_reset_exposure_state(); - if (!enable_sidecar) { return; } @@ -537,11 +535,7 @@ void ddtrace_sidecar_finalize(bool clear_id) { } } -static void dd_flush_ffe_exposures(void); - void ddtrace_sidecar_shutdown(void) { - dd_flush_ffe_exposures(); - ddtrace_sidecar_for_signal = NULL; // In thread mode, drop the main thread's connection before shutting down the @@ -884,28 +878,9 @@ void ddtrace_sidecar_rinit(void) { } void ddtrace_sidecar_rshutdown(void) { - dd_flush_ffe_exposures(); ddog_Vec_Tag_drop(DDTRACE_G(active_global_tags)); } -static void dd_flush_ffe_exposures(void) { - if (!DDTRACE_G(sidecar) || !ddtrace_sidecar_instance_id) { - return; - } - - ddog_CharSlice payload = ddog_ffe_flush_exposures(); - if (payload.ptr == NULL || payload.len == 0) { - return; - } - - ddtrace_ffi_try("Failed forwarding FFE exposures to sidecar", - ddog_sidecar_send_ffe_exposures(&DDTRACE_G(sidecar), - ddtrace_sidecar_instance_id, - &DDTRACE_G(sidecar_queue_id), - payload)); - ddog_ffe_free_flush_result(payload); -} - void ddtrace_sidecar_gshutdown(zend_ddtrace_globals *ddtrace_globals) { // NOTE: do not use DDTRACE_G() in this function; it may be called from the // main thread via ts_free_id() diff --git a/libdatadog b/libdatadog index a649b7f658d..cea1e44eddd 160000 --- a/libdatadog +++ b/libdatadog @@ -1 +1 @@ -Subproject commit a649b7f658d7df1449333929f42224bfbbc72f8d +Subproject commit cea1e44edddd9124f75d5095f31026904a1f58d8 diff --git a/src/DDTrace/OpenFeature/DataDogProvider.php b/src/DDTrace/OpenFeature/DataDogProvider.php new file mode 100644 index 00000000000..79a232ec8c7 --- /dev/null +++ b/src/DDTrace/OpenFeature/DataDogProvider.php @@ -0,0 +1,192 @@ +client = $client ?? FeatureFlagsClient::create(null, new NoopWarningEmitter()); + $this->warningEmitter = $warningEmitter ?? new TriggerErrorWarningEmitter(); + } + + public function resolveBooleanValue( + string $flagKey, + bool $defaultValue, + ?EvaluationContext $context = null + ): ResolutionDetailsInterface { + return $this->resolve($flagKey, FlagValueType::BOOLEAN, $defaultValue, $context); + } + + public function resolveStringValue( + string $flagKey, + string $defaultValue, + ?EvaluationContext $context = null + ): ResolutionDetailsInterface { + return $this->resolve($flagKey, FlagValueType::STRING, $defaultValue, $context); + } + + public function resolveIntegerValue( + string $flagKey, + int $defaultValue, + ?EvaluationContext $context = null + ): ResolutionDetailsInterface { + return $this->resolve($flagKey, FlagValueType::INTEGER, $defaultValue, $context); + } + + public function resolveFloatValue( + string $flagKey, + float $defaultValue, + ?EvaluationContext $context = null + ): ResolutionDetailsInterface { + return $this->resolve($flagKey, FlagValueType::FLOAT, $defaultValue, $context); + } + + /** + * @param array $defaultValue + */ + public function resolveObjectValue( + string $flagKey, + array $defaultValue, + ?EvaluationContext $context = null + ): ResolutionDetailsInterface { + return $this->resolve($flagKey, FlagValueType::OBJECT, $defaultValue, $context); + } + + private function resolve( + string $flagKey, + string $expectedType, + mixed $defaultValue, + ?EvaluationContext $context + ): ResolutionDetailsInterface { + $details = $this->evaluate($flagKey, $expectedType, $defaultValue, $this->normalizeContext($context)); + $this->warnIfNonProductionRuntime($details); + + $builder = (new ResolutionDetailsBuilder()) + ->withValue($details->getValue()) + ->withReason($this->mapReason($details->getReason())); + + $variant = $details->getVariant(); + if ($variant !== null && $variant !== '') { + $builder->withVariant($variant); + } + + if ($details->getErrorCode() !== null) { + $builder->withError(new ResolutionError( + $this->mapErrorCode($details->getErrorCode()), + $details->getErrorMessage() + )); + } + + return $builder->build(); + } + + /** + * @param bool|string|int|float|array $defaultValue + * @param array $context + */ + private function evaluate( + string $flagKey, + string $expectedType, + mixed $defaultValue, + array $context + ): EvaluationDetails { + return match ($expectedType) { + FlagValueType::BOOLEAN => $this->client->getBooleanDetails($flagKey, $defaultValue, $context), + FlagValueType::STRING => $this->client->getStringDetails($flagKey, $defaultValue, $context), + FlagValueType::INTEGER => $this->client->getIntegerDetails($flagKey, $defaultValue, $context), + FlagValueType::FLOAT => $this->client->getFloatDetails($flagKey, $defaultValue, $context), + FlagValueType::OBJECT => $this->client->getObjectDetails($flagKey, $defaultValue, $context), + default => throw new \InvalidArgumentException('Unknown OpenFeature flag value type: ' . $expectedType), + }; + } + + /** + * @return array{targetingKey?: ?string, attributes?: array} + */ + private function normalizeContext(?EvaluationContext $context): array + { + if ($context === null) { + return []; + } + + $attributes = []; + foreach ($context->getAttributes()->toArray() as $key => $value) { + if (is_bool($value) || is_int($value) || is_float($value) || is_string($value)) { + $attributes[(string) $key] = $value; + } + } + + return [ + 'targetingKey' => $context->getTargetingKey(), + 'attributes' => $attributes, + ]; + } + + private function warnIfNonProductionRuntime(EvaluationDetails $details): void + { + if ($this->warnedAboutNonProductionRuntime) { + return; + } + + $providerState = $details->getProviderState(); + if (!array_key_exists('productionRuntime', $providerState) || $providerState['productionRuntime'] !== false) { + return; + } + + $message = $details->getErrorMessage(); + if (!is_string($message) || $message === '') { + $message = 'Datadog-backed PHP OpenFeature evaluation is not fully enabled yet.'; + } + + $this->warningEmitter->warning($message); + $this->warnedAboutNonProductionRuntime = true; + } + + private function mapReason(string $reason): string + { + return match ($reason) { + EvaluationReason::STATIC_REASON => EvaluationReason::STATIC_REASON, + EvaluationReason::DEFAULT_REASON => OpenFeatureReason::DEFAULT, + EvaluationReason::TARGETING_MATCH => OpenFeatureReason::TARGETING_MATCH, + EvaluationReason::SPLIT => OpenFeatureReason::SPLIT, + EvaluationReason::DISABLED => OpenFeatureReason::DISABLED, + EvaluationReason::ERROR => OpenFeatureReason::ERROR, + default => OpenFeatureReason::UNKNOWN, + }; + } + + private function mapErrorCode(string $errorCode): ErrorCode + { + return match ($errorCode) { + EvaluationErrorCode::PROVIDER_NOT_READY => ErrorCode::PROVIDER_NOT_READY(), + EvaluationErrorCode::FLAG_NOT_FOUND => ErrorCode::FLAG_NOT_FOUND(), + EvaluationErrorCode::PARSE_ERROR => ErrorCode::PARSE_ERROR(), + EvaluationErrorCode::TYPE_MISMATCH => ErrorCode::TYPE_MISMATCH(), + default => ErrorCode::GENERAL(), + }; + } +} diff --git a/src/DDTrace/OpenFeature/NoopWarningEmitter.php b/src/DDTrace/OpenFeature/NoopWarningEmitter.php new file mode 100644 index 00000000000..e9cd5611fb1 --- /dev/null +++ b/src/DDTrace/OpenFeature/NoopWarningEmitter.php @@ -0,0 +1,14 @@ +evaluator = $evaluator; $this->warningEmitter = $warningEmitter; - $this->exposureWriter = $exposureWriter ?: new NoopExposureWriter(); - $this->metricsRecorder = $metricsRecorder ?: new NoopMetricsRecorder(); } public static function create( $evaluator = null, - $warningEmitter = null, - $exposureWriter = null, - $metricsRecorder = null + $warningEmitter = null ) { if ($evaluator !== null && !$evaluator instanceof Evaluator) { throw new \InvalidArgumentException('Expected an Evaluator instance'); @@ -46,9 +30,7 @@ public static function create( return new self( $evaluator ?: NativeEvaluator::createOrUnavailable(), - $warningEmitter ?: new TriggerErrorWarningEmitter(), - $exposureWriter ?: NativeExposureWriter::createOrNoop(), - $metricsRecorder + $warningEmitter ?: new TriggerErrorWarningEmitter() ); } @@ -116,51 +98,10 @@ private function evaluate($flagKey, $expectedType, $defaultValue, array $context ); $this->warnIfNonProductionRuntime($details); - $this->metricsRecorder->recordEvaluation( - $flagKey, - $details->getValueType(), - $details->getReason(), - $details->getErrorCode() - ); - $this->writeExposure($flagKey, $targetingKey, $attributes, $details); return $details; } - private function writeExposure($flagKey, $targetingKey, array $attributes, EvaluationDetails $details) - { - if ($details->isError()) { - return; - } - - $exposureData = $details->getExposureData(); - if (!$exposureData || (array_key_exists('doLog', $exposureData) && $exposureData['doLog'] === false)) { - return; - } - - $event = array( - 'flagKey' => $flagKey, - 'targetingKey' => $targetingKey, - 'attributes' => $attributes, - 'value' => $details->getValue(), - 'valueType' => $details->getValueType(), - 'reason' => $details->getReason(), - 'variant' => $details->getVariant(), - 'flagMetadata' => $details->getFlagMetadata(), - 'exposureData' => $exposureData, - ); - - if (array_key_exists('allocationKey', $exposureData)) { - $event['allocationKey'] = $exposureData['allocationKey']; - } - - if (array_key_exists('doLog', $exposureData)) { - $event['doLog'] = $exposureData['doLog']; - } - - $this->exposureWriter->write($event); - } - private function normalizeContext(array $context) { $targetingKey = null; diff --git a/src/api/FeatureFlags/ExposureWriter.php b/src/api/FeatureFlags/ExposureWriter.php deleted file mode 100644 index 5a35d3ebe20..00000000000 --- a/src/api/FeatureFlags/ExposureWriter.php +++ /dev/null @@ -1,17 +0,0 @@ - $event - * @return void - */ - public function write(array $event); - - /** - * @return void - */ - public function flush(); -} diff --git a/src/api/FeatureFlags/MetricsRecorder.php b/src/api/FeatureFlags/MetricsRecorder.php deleted file mode 100644 index 609135d0ad7..00000000000 --- a/src/api/FeatureFlags/MetricsRecorder.php +++ /dev/null @@ -1,12 +0,0 @@ -setServiceContext(); - } - - public static function isAvailable() - { - return function_exists('DDTrace\\ffe_send_exposure') - && function_exists('DDTrace\\ffe_set_service_context'); - } - - public static function createOrNoop() - { - return self::isAvailable() ? new self() : new NoopExposureWriter(); - } - - public function write(array $event) - { - if (!self::isAvailable()) { - return; - } - - if (array_key_exists('doLog', $event) && $event['doLog'] === false) { - return; - } - - $flagKey = $this->stringValue($event, 'flagKey'); - if ($flagKey === '') { - return; - } - - $variantKey = $this->stringValue($event, 'variant'); - $allocationKey = $this->nullableStringValue($event, 'allocationKey'); - $targetingKey = $this->nullableStringValue($event, 'targetingKey'); - $eventJson = $this->eventJson($event, $flagKey, $allocationKey, $targetingKey, $variantKey); - if ($eventJson === null) { - return; - } - - \DDTrace\ffe_send_exposure($eventJson, $flagKey, $allocationKey, $targetingKey, $variantKey); - } - - public function flush() - { - // Native exposure delivery is owned by the extension lifecycle. Calling - // DDTrace\ffe_flush_exposures() here would drain the buffer without - // forwarding it to the sidecar. - } - - private function setServiceContext() - { - \DDTrace\ffe_set_service_context( - $this->configValue('DD_SERVICE', 'datadog.service'), - $this->configValue('DD_ENV', 'datadog.env'), - $this->configValue('DD_VERSION', 'datadog.version') - ); - } - - private function eventJson(array $event, $flagKey, $allocationKey, $targetingKey, $variantKey) - { - $attributes = array(); - if (array_key_exists('attributes', $event) && is_array($event['attributes'])) { - foreach ($event['attributes'] as $key => $value) { - if (is_bool($value) || is_int($value) || is_float($value) || is_string($value)) { - $attributes[(string) $key] = $value; - } - } - } - - $json = json_encode(array( - 'timestamp' => (int) floor(microtime(true) * 1000), - 'flag' => array('key' => $flagKey), - 'allocation' => array('key' => $allocationKey === null ? '' : $allocationKey), - 'variant' => array('key' => $variantKey), - 'subject' => array( - 'id' => $targetingKey === null ? '' : $targetingKey, - 'attributes' => empty($attributes) ? new \stdClass() : (object) $attributes, - ), - ), JSON_UNESCAPED_SLASHES); - - return is_string($json) ? $json : null; - } - - private function configValue($envName, $iniName) - { - if (function_exists('dd_trace_env_config')) { - $value = \dd_trace_env_config($envName); - if (is_scalar($value)) { - return (string) $value; - } - } - - $value = ini_get($iniName); - return is_string($value) ? $value : ''; - } - - private function stringValue(array $event, $key) - { - $value = $this->nullableStringValue($event, $key); - return $value === null ? '' : $value; - } - - private function nullableStringValue(array $event, $key) - { - if (!array_key_exists($key, $event) || $event[$key] === null) { - return null; - } - - if (is_scalar($event[$key])) { - return (string) $event[$key]; - } - - $json = json_encode($event[$key]); - return is_string($json) ? $json : null; - } -} diff --git a/src/api/FeatureFlags/NoopExposureWriter.php b/src/api/FeatureFlags/NoopExposureWriter.php deleted file mode 100644 index 60fe0b5bc02..00000000000 --- a/src/api/FeatureFlags/NoopExposureWriter.php +++ /dev/null @@ -1,14 +0,0 @@ -configVersion); return is_int($version) ? $version : (int) $version; } - - public function waitUntilReady($timeoutSeconds = 5.0, $pollIntervalMicroseconds = 10000) - { - if ($this->hasConfig()) { - return true; - } - - if ($timeoutSeconds <= 0) { - return false; - } - - $deadline = microtime(true) + (float) $timeoutSeconds; - while (microtime(true) < $deadline) { - usleep((int) $pollIntervalMicroseconds); - if ($this->hasConfig()) { - return true; - } - } - - return $this->hasConfig(); - } } diff --git a/src/bridge/_files_openfeature.php b/src/bridge/_files_openfeature.php new file mode 100644 index 00000000000..7572940becd --- /dev/null +++ b/src/bridge/_files_openfeature.php @@ -0,0 +1,6 @@ +clientForEvaluator(new OpenFeatureTestEvaluator())); + + self::assertSame('Datadog', $provider->getMetadata()->getName()); + } + + public function testOpenFeatureClientResolvesTypedValuesThroughDatadogClient(): void + { + $evaluator = new OpenFeatureTestEvaluator(); + $evaluator + ->setSuccess('bool.flag', true, EvaluationReason::TARGETING_MATCH, 'on') + ->setSuccess('string.flag', 'blue') + ->setSuccess('integer.flag', 42) + ->setSuccess('float.flag', 3.5) + ->setSuccess('object.flag', ['enabled' => true]); + + $client = $this->openFeatureClientFor(new DataDogProvider($this->clientForEvaluator($evaluator))); + + self::assertTrue($client->getBooleanValue('bool.flag', false)); + self::assertSame('blue', $client->getStringValue('string.flag', 'red')); + self::assertSame(42, $client->getIntegerValue('integer.flag', 0)); + self::assertSame(3.5, $client->getFloatValue('float.flag', 0.0)); + self::assertSame(['enabled' => true], $client->getObjectValue('object.flag', [])); + + $details = $client->getBooleanDetails('bool.flag', false); + self::assertSame('bool.flag', $details->getFlagKey()); + self::assertTrue($details->getValue()); + self::assertSame(Reason::TARGETING_MATCH, $details->getReason()); + self::assertSame('on', $details->getVariant()); + self::assertNull($details->getError()); + } + + public function testStaticReasonIsPreservedAsDatadogReason(): void + { + $evaluator = new OpenFeatureTestEvaluator(); + $evaluator->setSuccess('static.flag', 'value', EvaluationReason::STATIC_REASON); + + $client = $this->openFeatureClientFor(new DataDogProvider($this->clientForEvaluator($evaluator))); + $details = $client->getStringDetails('static.flag', 'fallback'); + + self::assertSame(EvaluationReason::STATIC_REASON, $details->getReason()); + } + + public function testEvaluationContextIsNormalizedForDatadogClient(): void + { + $evaluator = new OpenFeatureTestEvaluator(); + $evaluator->setSuccess('context.flag', 'on'); + + $provider = new DataDogProvider($this->clientForEvaluator($evaluator)); + $provider->resolveStringValue('context.flag', 'off', new EvaluationContext( + 'user-123', + new Attributes([ + 'plan' => 'pro', + 'age' => 41, + 'rate' => 1.5, + 'beta' => true, + 'nested' => ['drop'], + 'null' => null, + 'date' => new \DateTimeImmutable(), + ]) + )); + + $calls = $evaluator->getCalls(); + self::assertCount(1, $calls); + self::assertSame('user-123', $calls[0]['targetingKey']); + self::assertSame([ + 'plan' => 'pro', + 'age' => 41, + 'rate' => 1.5, + 'beta' => true, + ], $calls[0]['attributes']); + } + + public function testUnavailableRuntimeReturnsDefaultDetailsAndOneWarning(): void + { + $warnings = new OpenFeatureRecordingWarningEmitter(); + $client = $this->openFeatureClientFor(new DataDogProvider(null, $warnings)); + + $value = $client->getBooleanValue('checkout.enabled', true); + $details = $client->getStringDetails('checkout.copy', 'fallback'); + + self::assertTrue($value); + self::assertSame('fallback', $details->getValue()); + self::assertSame(Reason::ERROR, $details->getReason()); + self::assertSame(ErrorCode::PROVIDER_NOT_READY()->getValue(), $details->getError()->getResolutionErrorCode()->getValue()); + self::assertContains($details->getError()->getResolutionErrorMessage(), [ + NativeEvaluator::WARNING_MESSAGE, + UnavailableEvaluator::WARNING_MESSAGE, + ]); + self::assertSame([$details->getError()->getResolutionErrorMessage()], $warnings->warnings()); + } + + public function testProviderWarningIsEmittedOncePerProvider(): void + { + $warnings = new OpenFeatureRecordingWarningEmitter(); + $evaluator = new OpenFeatureTestEvaluator(); + $evaluator + ->setUnavailable('first.flag', true, 'temporary unavailable') + ->setUnavailable('second.flag', true, 'temporary unavailable'); + + $client = $this->openFeatureClientFor(new DataDogProvider($this->clientForEvaluator($evaluator), $warnings)); + + $client->getBooleanValue('first.flag', false); + $client->getBooleanValue('second.flag', false); + + self::assertSame(['temporary unavailable'], $warnings->warnings()); + } + + public function testProviderErrorsMapToOpenFeatureDetails(): void + { + $evaluator = new OpenFeatureTestEvaluator(); + $evaluator->setFlagNotFound('missing.flag'); + + $client = $this->openFeatureClientFor(new DataDogProvider($this->clientForEvaluator($evaluator))); + $details = $client->getStringDetails('missing.flag', 'fallback'); + + self::assertSame('fallback', $details->getValue()); + self::assertSame(Reason::ERROR, $details->getReason()); + self::assertSame(EvaluationErrorCode::FLAG_NOT_FOUND, $details->getError()->getResolutionErrorCode()->getValue()); + self::assertSame('Feature flag "missing.flag" was not found', $details->getError()->getResolutionErrorMessage()); + } + + public function testTypeMismatchReturnsDefaultWithOpenFeatureError(): void + { + $evaluator = new OpenFeatureTestEvaluator(); + $evaluator->setSuccess('integer.flag', 'not-an-int'); + + $client = $this->openFeatureClientFor(new DataDogProvider($this->clientForEvaluator($evaluator))); + $details = $client->getIntegerDetails('integer.flag', 7); + + self::assertSame(7, $details->getValue()); + self::assertSame(Reason::ERROR, $details->getReason()); + self::assertSame(EvaluationErrorCode::TYPE_MISMATCH, $details->getError()->getResolutionErrorCode()->getValue()); + } + + private function clientForEvaluator(Evaluator $evaluator): FeatureFlagsClient + { + return FeatureFlagsClient::create($evaluator, new NoopWarningEmitter()); + } + + private function openFeatureClientFor(DataDogProvider $provider) + { + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + + return $api->getClient('datadog-test'); + } +} + +final class OpenFeatureTestEvaluator implements Evaluator +{ + /** @var array */ + private array $details = []; + + /** @var list> */ + private array $calls = []; + + public function setSuccess( + string $flagKey, + mixed $value, + string $reason = EvaluationReason::STATIC_REASON, + ?string $variant = null + ): self { + $this->details[$flagKey] = new EvaluationDetails( + $value, + $this->typeForValue($value), + $reason, + $variant + ); + + return $this; + } + + public function setUnavailable(string $flagKey, mixed $defaultValue, string $message): self + { + $this->details[$flagKey] = new EvaluationDetails( + $defaultValue, + $this->typeForValue($defaultValue), + EvaluationReason::ERROR, + null, + EvaluationErrorCode::PROVIDER_NOT_READY, + $message, + [], + [], + ['ready' => false, 'productionRuntime' => false, 'reason' => 'test_unavailable'] + ); + + return $this; + } + + public function setFlagNotFound(string $flagKey): self + { + $this->details[$flagKey] = new EvaluationDetails( + 'fallback', + EvaluationType::STRING, + EvaluationReason::ERROR, + null, + EvaluationErrorCode::FLAG_NOT_FOUND, + 'Feature flag "' . $flagKey . '" was not found' + ); + + return $this; + } + + public function evaluate($flagKey, $expectedType, $defaultValue, $targetingKey = null, array $attributes = []) + { + $this->calls[] = [ + 'flagKey' => $flagKey, + 'expectedType' => $expectedType, + 'targetingKey' => $targetingKey, + 'attributes' => $attributes, + ]; + + if (array_key_exists($flagKey, $this->details)) { + $details = $this->details[$flagKey]; + if ($this->matchesExpectedType($details->getValue(), $expectedType)) { + return $details; + } + + return new EvaluationDetails( + $defaultValue, + $expectedType, + EvaluationReason::ERROR, + null, + EvaluationErrorCode::TYPE_MISMATCH, + 'Expected ' . $expectedType . ' flag value' + ); + } + + return new EvaluationDetails( + $defaultValue, + $expectedType, + EvaluationReason::ERROR, + null, + EvaluationErrorCode::PROVIDER_NOT_READY, + UnavailableEvaluator::WARNING_MESSAGE, + [], + [], + ['ready' => false, 'productionRuntime' => false, 'reason' => 'test_missing_result'] + ); + } + + /** + * @return list> + */ + public function getCalls(): array + { + return $this->calls; + } + + private function typeForValue(mixed $value): string + { + if (is_bool($value)) { + return EvaluationType::BOOLEAN; + } + if (is_int($value)) { + return EvaluationType::INTEGER; + } + if (is_float($value)) { + return EvaluationType::FLOAT; + } + if (is_array($value)) { + return EvaluationType::OBJECT; + } + return EvaluationType::STRING; + } + + private function matchesExpectedType(mixed $value, string $expectedType): bool + { + return match ($expectedType) { + EvaluationType::BOOLEAN => is_bool($value), + EvaluationType::STRING => is_string($value), + EvaluationType::INTEGER => is_int($value), + EvaluationType::FLOAT => is_float($value) || is_int($value), + EvaluationType::OBJECT => is_array($value), + default => false, + }; + } +} + +final class OpenFeatureRecordingWarningEmitter implements WarningEmitter +{ + /** @var string[] */ + private array $warnings = []; + + public function warning($message) + { + $this->warnings[] = $message; + } + + /** + * @return string[] + */ + public function warnings(): array + { + return $this->warnings; + } +} +} diff --git a/tests/OpenFeature/composer.json b/tests/OpenFeature/composer.json new file mode 100644 index 00000000000..faab284a54c --- /dev/null +++ b/tests/OpenFeature/composer.json @@ -0,0 +1,7 @@ +{ + "name": "datadog/dd-trace-tests-openfeature", + "require": { + "open-feature/sdk": "^2.1" + }, + "minimum-stability": "stable" +} diff --git a/tests/api/Unit/FeatureFlags/ExposureWriterTest.php b/tests/api/Unit/FeatureFlags/ExposureWriterTest.php deleted file mode 100644 index 305e92e3cea..00000000000 --- a/tests/api/Unit/FeatureFlags/ExposureWriterTest.php +++ /dev/null @@ -1,146 +0,0 @@ -setDetails('checkout-redesign', new EvaluationDetails( - true, - EvaluationType::BOOLEAN, - EvaluationReason::SPLIT, - 'treatment', - null, - null, - array('owner' => 'ffe'), - array('allocationKey' => 'alloc-1', 'doLog' => true) - )); - - $client = Client::create( - $evaluator, - new ExposureTestWarningEmitter(), - $writer, - new NoopMetricsRecorder() - ); - - $client->getBooleanValue('checkout-redesign', false, array( - 'targetingKey' => 'user-123', - 'attributes' => array('plan' => 'pro'), - )); - - $this->assertSame(array(array( - 'flagKey' => 'checkout-redesign', - 'targetingKey' => 'user-123', - 'attributes' => array('plan' => 'pro'), - 'value' => true, - 'valueType' => 'boolean', - 'reason' => EvaluationReason::SPLIT, - 'variant' => 'treatment', - 'flagMetadata' => array('owner' => 'ffe'), - 'exposureData' => array('allocationKey' => 'alloc-1', 'doLog' => true), - 'allocationKey' => 'alloc-1', - 'doLog' => true, - )), $writer->events()); - } - - public function testClientSkipsExposureWhenDoLogFalseOrEvaluationErrors() - { - $writer = new RecordingExposureWriter(); - - $evaluator = new ExposureTestEvaluator(); - $evaluator - ->setDetails('do-not-log', new EvaluationDetails( - true, - EvaluationType::BOOLEAN, - EvaluationReason::SPLIT, - 'control', - null, - null, - array(), - array('allocationKey' => 'alloc-1', 'doLog' => false) - )) - ->setDetails('provider-not-ready', new EvaluationDetails( - false, - EvaluationType::BOOLEAN, - EvaluationReason::ERROR, - null, - EvaluationErrorCode::PROVIDER_NOT_READY, - UnavailableEvaluator::WARNING_MESSAGE, - array(), - array(), - array('ready' => false, 'productionRuntime' => false) - )); - - $client = Client::create( - $evaluator, - new ExposureTestWarningEmitter(), - $writer, - new NoopMetricsRecorder() - ); - - $client->getBooleanValue('do-not-log', false); - $client->getBooleanValue('provider-not-ready', false); - - $this->assertSame(array(), $writer->events()); - } -} - -final class RecordingExposureWriter implements ExposureWriter -{ - private $events = array(); - - public function write(array $event) - { - $this->events[] = $event; - } - - public function flush() - { - } - - public function events() - { - return $this->events; - } -} - -final class ExposureTestEvaluator implements Evaluator -{ - private $details = array(); - - public function setDetails($flagKey, EvaluationDetails $details) - { - $this->details[$flagKey] = $details; - return $this; - } - - public function evaluate($flagKey, $expectedType, $defaultValue, $targetingKey = null, array $attributes = array()) - { - return array_key_exists($flagKey, $this->details) - ? $this->details[$flagKey] - : new EvaluationDetails($defaultValue, $expectedType, EvaluationReason::ERROR); - } -} - -final class ExposureTestWarningEmitter implements WarningEmitter -{ - public function warning($message) - { - } -} diff --git a/tests/api/Unit/FeatureFlags/RemoteConfigClientTest.php b/tests/api/Unit/FeatureFlags/RemoteConfigClientTest.php index bd0e75cab75..cfb636f994d 100644 --- a/tests/api/Unit/FeatureFlags/RemoteConfigClientTest.php +++ b/tests/api/Unit/FeatureFlags/RemoteConfigClientTest.php @@ -22,24 +22,7 @@ function () { $this->assertSame(42, $client->configVersion()); } - public function testWaitUntilReadyPollsUntilConfigArrives() - { - $attempts = 0; - $client = new RemoteConfigClient( - function () use (&$attempts) { - ++$attempts; - return $attempts >= 2; - }, - function () { - return 0; - } - ); - - $this->assertTrue($client->waitUntilReady(0.05, 1000)); - $this->assertGreaterThanOrEqual(2, $attempts); - } - - public function testZeroTimeoutDoesNotBlock() + public function testUnavailableConfigIsReportedWithoutBlocking() { $client = new RemoteConfigClient( function () { @@ -50,6 +33,7 @@ function () { } ); - $this->assertFalse($client->waitUntilReady(0)); + $this->assertFalse($client->hasConfig()); + $this->assertSame(0, $client->configVersion()); } } diff --git a/tests/api/Unit/FeatureFlags/ResultMapperTest.php b/tests/api/Unit/FeatureFlags/ResultMapperTest.php index f4a0d7ccd59..e4574080d9c 100644 --- a/tests/api/Unit/FeatureFlags/ResultMapperTest.php +++ b/tests/api/Unit/FeatureFlags/ResultMapperTest.php @@ -121,6 +121,27 @@ public function testIntegerJsonCanMapToFloat() $this->assertSame(EvaluationReason::SPLIT, $details->getReason()); } + public function testJsonObjectMapsToObjectDetails() + { + $details = (new ResultMapper())->map(array( + 'value_json' => '{"enabled":true,"threshold":2,"labels":["a","b"]}', + 'variant' => 'json-a', + 'allocation_key' => 'alloc-json', + 'reason' => ResultMapper::BRIDGE_REASON_SPLIT, + 'error_code' => ResultMapper::BRIDGE_ERROR_NONE, + 'do_log' => true, + ), EvaluationType::OBJECT, array('fallback' => true)); + + $this->assertSame(array( + 'enabled' => true, + 'threshold' => 2, + 'labels' => array('a', 'b'), + ), $details->getValue()); + $this->assertSame(EvaluationReason::SPLIT, $details->getReason()); + $this->assertSame('json-a', $details->getVariant()); + $this->assertSame(array('allocationKey' => 'alloc-json', 'doLog' => true), $details->getExposureData()); + } + /** * @dataProvider reasonProvider */ diff --git a/tests/ext/ffe/flush_drains_buffer.phpt b/tests/ext/ffe/flush_drains_buffer.phpt deleted file mode 100644 index ef5d306c88e..00000000000 --- a/tests/ext/ffe/flush_drains_buffer.phpt +++ /dev/null @@ -1,49 +0,0 @@ ---TEST-- -FFE: native exposure flush drains the batch buffer ---SKIPIF-- - ---ENV-- -DD_TRACE_ENABLED=0 ---INI-- -datadog.trace.generate_root_span=0 ---FILE-- - 1713382853716, - 'flag' => ['key' => 'demo-flag'], - 'allocation' => ['key' => 'alloc-a'], - 'variant' => ['key' => 'on'], - 'subject' => ['id' => 'user-1', 'attributes' => new stdClass()], -]); - -var_dump(DDTrace\ffe_send_exposure($event, 'demo-flag', 'alloc-a', 'user-1', 'on')); -var_dump(DDTrace\ffe_send_exposure($event, 'demo-flag', 'alloc-a', 'user-1', 'on')); -var_dump(DDTrace\ffe_send_exposure($event, 'demo-flag', 'alloc-a', 'user-1', 'off')); - -$payload = DDTrace\ffe_flush_exposures(); -var_dump(is_string($payload) && strlen($payload) > 0); - -$decoded = json_decode($payload, true); -var_dump($decoded['context']['service'] === 'svc-flush'); -var_dump($decoded['context']['env'] === 'test'); -var_dump($decoded['context']['version'] === '9.9.9'); -var_dump(count($decoded['exposures'])); -var_dump(DDTrace\ffe_flush_exposures()); - -?> ---EXPECT-- -bool(true) -bool(false) -bool(true) -bool(true) -bool(true) -bool(true) -bool(true) -int(2) -NULL diff --git a/tests/ext/ffe/fork_resets_dedup.phpt b/tests/ext/ffe/fork_resets_dedup.phpt deleted file mode 100644 index a7b4e9f09c0..00000000000 --- a/tests/ext/ffe/fork_resets_dedup.phpt +++ /dev/null @@ -1,52 +0,0 @@ ---TEST-- -FFE: fork handler resets exposure dedup in child ---SKIPIF-- - ---ENV-- -DD_TRACE_ENABLED=0 ---INI-- -datadog.trace.generate_root_span=0 ---FILE-- - 1, - 'flag' => ['key' => 'f'], - 'allocation' => ['key' => 'a'], - 'variant' => ['key' => 'on'], - 'subject' => ['id' => 'u', 'attributes' => new stdClass()], -]); - -$parentFirst = DDTrace\ffe_send_exposure($event, 'f', 'a', 'u', 'on'); -echo 'parent_first=' . ($parentFirst ? 'true' : 'false') . "\n"; - -$pid = pcntl_fork(); -if ($pid === -1) { - die('fork failed'); -} - -if ($pid === 0) { - DDTrace\Internal\handle_fork(); - $child = DDTrace\ffe_send_exposure($event, 'f', 'a', 'u', 'on'); - echo 'child=' . ($child ? 'true' : 'false') . "\n"; - DDTrace\ffe_reset_exposure_state(); - exit(0); -} - -pcntl_wait($status); - -$parentSecond = DDTrace\ffe_send_exposure($event, 'f', 'a', 'u', 'on'); -echo 'parent_second=' . ($parentSecond ? 'true' : 'false') . "\n"; - -?> ---EXPECTF-- -parent_first=true -child=true -parent_second=false diff --git a/tests/ext/ffe/native_bridge_evaluate.phpt b/tests/ext/ffe/native_bridge_evaluate.phpt index ab5f2d64e80..fc7a3687b79 100644 --- a/tests/ext/ffe/native_bridge_evaluate.phpt +++ b/tests/ext/ffe/native_bridge_evaluate.phpt @@ -28,6 +28,20 @@ $config = <<<'JSON' "doLog": true }] }, + "object.flag": { + "key": "object.flag", + "enabled": true, + "variationType": "JSON", + "variations": { + "json-a": {"key": "json-a", "value": {"enabled": true, "threshold": 2}} + }, + "allocations": [{ + "key": "alloc-json", + "rules": [], + "splits": [{"variationKey": "json-a", "serialId": 8, "shards": []}], + "doLog": true + }] + }, "bad.flag": { "key": "bad.flag", "enabled": true, @@ -52,6 +66,15 @@ show('success', \DDTrace\ffe_evaluate('string.flag', 0, 'user-1', array( 'age' => 42, 'ignored' => array('drop'), ))); +$object = \DDTrace\ffe_evaluate('object.flag', 4, 'user-1', array()); +show('object_success_value', json_decode($object['value_json'], true)); +show('object_success_metadata', array( + 'variant' => $object['variant'], + 'allocation_key' => $object['allocation_key'], + 'reason' => $object['reason'], + 'error_code' => $object['error_code'], + 'do_log' => $object['do_log'], +)); show('missing', \DDTrace\ffe_evaluate('missing.flag', 0, 'user-1', array())); show('type_mismatch', \DDTrace\ffe_evaluate('string.flag', 3, 'user-1', array())); show('parse_error', \DDTrace\ffe_evaluate('bad.flag', 0, 'user-1', array())); @@ -62,6 +85,8 @@ provider_not_ready={"value_json":"null","variant":null,"allocation_key":null,"re load=true has_config_after=true success={"value_json":"\"blue\"","variant":"blue","allocation_key":"alloc-string","reason":0,"error_code":0,"do_log":true} +object_success_value={"enabled":true,"threshold":2} +object_success_metadata={"variant":"json-a","allocation_key":"alloc-json","reason":0,"error_code":0,"do_log":true} missing={"value_json":"null","variant":null,"allocation_key":null,"reason":1,"error_code":3,"do_log":false} type_mismatch={"value_json":"null","variant":null,"allocation_key":null,"reason":5,"error_code":1,"do_log":false} parse_error={"value_json":"null","variant":null,"allocation_key":null,"reason":5,"error_code":2,"do_log":false} diff --git a/tests/ext/ffe/system_test_data_evaluate.phpt b/tests/ext/ffe/system_test_data_evaluate.phpt new file mode 100644 index 00000000000..fb7ad8eeb2c --- /dev/null +++ b/tests/ext/ffe/system_test_data_evaluate.phpt @@ -0,0 +1,234 @@ +--TEST-- +FFE canonical system test data evaluates through the Datadog client +--ENV-- +DD_TRACE_GENERATE_ROOT_SPAN=0 +--FILE-- + $case) { + $caseCount++; + try { + run_fixture_case($client, basename($caseFile), $index, $case, $failures); + } catch (\Throwable $exception) { + $failures[] = basename($caseFile) . '#' . $index . ': ' . $exception->getMessage(); + } + } +} + +foreach ($failures as $failure) { + echo "failure=" . $failure . "\n"; +} + +show('fixture_files', count($caseFiles)); +show('cases', $caseCount); +show('failures', count($failures)); + +function require_feature_flag_api($root) +{ + $apiRoot = $root . '/src/api/FeatureFlags'; + foreach (array( + 'EvaluationType', + 'EvaluationReason', + 'EvaluationErrorCode', + 'EvaluationDetails', + 'Evaluator', + 'WarningEmitter', + 'ResultMapper', + 'RemoteConfigClient', + 'UnavailableEvaluator', + 'TriggerErrorWarningEmitter', + 'NativeEvaluator', + 'Client', + ) as $classFile) { + require_once $apiRoot . '/' . $classFile . '.php'; + } +} + +function run_fixture_case($client, $fileName, $index, array $case, array &$failures) +{ + foreach (array('flag', 'variationType', 'defaultValue', 'targetingKey', 'attributes', 'result') as $requiredKey) { + if (!array_key_exists($requiredKey, $case)) { + $failures[] = $fileName . '#' . $index . ': missing key ' . $requiredKey; + return; + } + } + + $context = array( + 'targetingKey' => $case['targetingKey'], + 'attributes' => is_array($case['attributes']) ? $case['attributes'] : array(), + ); + + $details = evaluate_fixture_case( + $client, + $case['variationType'], + $case['flag'], + $case['defaultValue'], + $context + ); + + if (!array_key_exists('value', $case['result'])) { + $failures[] = $fileName . '#' . $index . ': result must include value'; + return; + } + + if (!values_match($details->getValue(), $case['result']['value'], $case['variationType'])) { + $failures[] = $fileName . '#' . $index + . ': value got=' . encode_value($details->getValue()) + . ' want=' . encode_value($case['result']['value']); + } +} + +function evaluate_fixture_case($client, $variationType, $flag, $defaultValue, array $context) +{ + switch ($variationType) { + case 'BOOLEAN': + return $client->getBooleanDetails($flag, $defaultValue, $context); + case 'STRING': + return $client->getStringDetails($flag, $defaultValue, $context); + case 'INTEGER': + return $client->getIntegerDetails($flag, $defaultValue, $context); + case 'NUMERIC': + return $client->getFloatDetails($flag, $defaultValue, $context); + case 'JSON': + return $client->getObjectDetails($flag, $defaultValue, $context); + } + + throw new \RuntimeException('unsupported variationType ' . encode_value($variationType)); +} + +function values_match($actual, $expected, $variationType) +{ + if ($variationType === 'NUMERIC') { + return is_numeric($actual) + && is_numeric($expected) + && abs((float) $actual - (float) $expected) < 0.000001; + } + + if (is_array($actual) || is_array($expected)) { + return arrays_match($actual, $expected); + } + + return $actual === $expected; +} + +function arrays_match($actual, $expected) +{ + if (!is_array($actual) || !is_array($expected)) { + return false; + } + + if (count($actual) !== count($expected)) { + return false; + } + + foreach ($expected as $key => $expectedValue) { + if (!array_key_exists($key, $actual)) { + return false; + } + + $actualValue = $actual[$key]; + if (is_array($actualValue) || is_array($expectedValue)) { + if (!arrays_match($actualValue, $expectedValue)) { + return false; + } + continue; + } + + if (is_float($actualValue) || is_float($expectedValue)) { + if (!is_float($actualValue) || !is_float($expectedValue)) { + return false; + } + if (abs($actualValue - $expectedValue) >= 0.000001) { + return false; + } + continue; + } + + if ($actualValue !== $expectedValue) { + return false; + } + } + + return true; +} + +function decode_json_file($path) +{ + $json = file_get_contents($path); + if ($json === false) { + throw new \RuntimeException('failed to read ' . $path); + } + + $decoded = json_decode($json, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException('failed to decode ' . $path . ': ' . json_last_error_msg()); + } + + return $decoded; +} + +function encode_value($value) +{ + return json_encode($value, JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION); +} + +function show($label, $value) +{ + echo $label . '=' . encode_value($value) . "\n"; +} +?> +--EXPECTF-- +config_loaded=true +fixture_files=%d +cases=%d +failures=0 diff --git a/tests/internal-api-stress-test.php b/tests/internal-api-stress-test.php index e4ee45d3ed7..3bbd671f62f 100644 --- a/tests/internal-api-stress-test.php +++ b/tests/internal-api-stress-test.php @@ -134,8 +134,6 @@ function ($hook = null) { $minFunctionArgs = [ 'DDTrace\ffe_load_config' => 1, 'DDTrace\ffe_evaluate' => 4, - 'DDTrace\ffe_send_exposure' => 5, - 'DDTrace\ffe_set_service_context' => 3, ]; function call_function(ReflectionFunction $function) diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 399e4f8273d..2a064f1b294 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -141,6 +141,9 @@ ./Unit/ + + ./OpenFeature/ + @@ -151,4 +154,4 @@ ../src/dogstatsd - \ No newline at end of file + diff --git a/tooling/generation/composer.json b/tooling/generation/composer.json index 0bf4f24cc0f..4e8a7316659 100644 --- a/tooling/generation/composer.json +++ b/tooling/generation/composer.json @@ -8,9 +8,11 @@ "vendor/bin/classpreloader compile --config=../../src/bridge/_files_api.php --output=../../src/bridge/_generated_api.php", "vendor/bin/classpreloader compile --config=../../src/bridge/_files_tracer.php --output=../../src/bridge/_generated_tracer.php", "vendor/bin/classpreloader compile --config=../../src/bridge/_files_opentelemetry.php --output=../../src/bridge/_generated_opentelemetry.php", + "vendor/bin/classpreloader compile --config=../../src/bridge/_files_openfeature.php --output=../../src/bridge/_generated_openfeature.php", "sed -i \"s/'[^']\\+bridge\\/\\.\\./__DIR__ . '\\/../g;s/\\s*\\(^\\|\\s\\)\\/\\/.*//g;s/\\/\\*\\([^*]\\|\\*[^/]\\)*\\*\\///g;/\\/\\*/,/\\*\\//d;/^\\s*$/d\" ../../src/bridge/_generated_api.php", "sed -i \"s/'[^']\\+bridge\\/\\.\\./__DIR__ . '\\/../g;s/\\s*\\(^\\|\\s\\)\\/\\/.*//g;s/\\/\\*\\([^*]\\|\\*[^/]\\)*\\*\\///g;/\\/\\*/,/\\*\\//d;/^\\s*$/d\" ../../src/bridge/_generated_tracer.php", - "sed -i \"s/'[^']\\+bridge\\/\\.\\./__DIR__ . '\\/../g;s/\\s*\\(^\\|\\s\\)\\/\\/.*//g;s/\\/\\*\\([^*]\\|\\*[^/]\\)*\\*\\///g;/\\/\\*/,/\\*\\//d;/^\\s*$/d\" ../../src/bridge/_generated_opentelemetry.php" + "sed -i \"s/'[^']\\+bridge\\/\\.\\./__DIR__ . '\\/../g;s/\\s*\\(^\\|\\s\\)\\/\\/.*//g;s/\\/\\*\\([^*]\\|\\*[^/]\\)*\\*\\///g;/\\/\\*/,/\\*\\//d;/^\\s*$/d\" ../../src/bridge/_generated_opentelemetry.php", + "sed -i \"s/'[^']\\+bridge\\/\\.\\./__DIR__ . '\\/../g;s/\\s*\\(^\\|\\s\\)\\/\\/.*//g;s/\\/\\*\\([^*]\\|\\*[^/]\\)*\\*\\///g;/\\/\\*/,/\\*\\//d;/^\\s*$/d\" ../../src/bridge/_generated_openfeature.php" ], "verify": "php -r 'require \"../../src/bridge/_files_api.php\"; require \"../../src/bridge/_files_tracer.php\";'" } From 9163839f72c2d2734164618f5aa01c5b400cb555 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Fri, 22 May 2026 05:32:34 -0400 Subject: [PATCH 03/14] Fix FFE fixture validation in package CI --- .gitlab-ci.yml | 2 +- tests/ext/ffe/system_test_data_evaluate.phpt | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e825784f383..a0f19808052 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -119,7 +119,7 @@ package-trigger: pipeline_variables: true variables: PARENT_PIPELINE_ID: $CI_PIPELINE_ID - GIT_SUBMODULE_PATHS: libdatadog appsec/third_party/cpp-base64 appsec/third_party/libddwaf appsec/third_party/libddwaf-rust appsec/third_party/msgpack-c + GIT_SUBMODULE_PATHS: libdatadog tests/FeatureFlags/ffe-system-test-data appsec/third_party/cpp-base64 appsec/third_party/libddwaf appsec/third_party/libddwaf-rust appsec/third_party/msgpack-c NIGHTLY_BUILD: $NIGHTLY_BUILD # Runs after the full CI completes. Triggered in two situations: diff --git a/tests/ext/ffe/system_test_data_evaluate.phpt b/tests/ext/ffe/system_test_data_evaluate.phpt index fb7ad8eeb2c..9d9fbfa34fb 100644 --- a/tests/ext/ffe/system_test_data_evaluate.phpt +++ b/tests/ext/ffe/system_test_data_evaluate.phpt @@ -1,5 +1,11 @@ --TEST-- FFE canonical system test data evaluates through the Datadog client +--SKIPIF-- + --ENV-- DD_TRACE_GENERATE_ROOT_SPAN=0 --FILE-- From cc6058f8ea076013dbd2551d939a56bedd0fb623 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Fri, 22 May 2026 06:30:17 -0400 Subject: [PATCH 04/14] Fix FFE fixture submodules in child pipelines --- .gitlab-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a0f19808052..030f1a62b31 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -71,6 +71,8 @@ tracer-trigger: strategy: depend variables: PARENT_PIPELINE_ID: $CI_PIPELINE_ID + GIT_SUBMODULE_STRATEGY: recursive + GIT_SUBMODULE_PATHS: libdatadog tests/FeatureFlags/ffe-system-test-data appsec-trigger: stage: tests @@ -119,6 +121,7 @@ package-trigger: pipeline_variables: true variables: PARENT_PIPELINE_ID: $CI_PIPELINE_ID + GIT_SUBMODULE_STRATEGY: recursive GIT_SUBMODULE_PATHS: libdatadog tests/FeatureFlags/ffe-system-test-data appsec/third_party/cpp-base64 appsec/third_party/libddwaf appsec/third_party/libddwaf-rust appsec/third_party/msgpack-c NIGHTLY_BUILD: $NIGHTLY_BUILD From e55ebb976b0e29afe858282392e32842679927fb Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Fri, 22 May 2026 07:29:49 -0400 Subject: [PATCH 05/14] Skip FFE fixture sweep under valgrind --- tests/ext/ffe/system_test_data_evaluate.phpt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/ext/ffe/system_test_data_evaluate.phpt b/tests/ext/ffe/system_test_data_evaluate.phpt index 9d9fbfa34fb..1fbe17bd135 100644 --- a/tests/ext/ffe/system_test_data_evaluate.phpt +++ b/tests/ext/ffe/system_test_data_evaluate.phpt @@ -5,6 +5,9 @@ FFE canonical system test data evaluates through the Datadog client if (getenv('PHP_PEAR_RUNTESTS') === '1') { die('skip: canonical FFE fixtures are not shipped in the PECL test package'); } +if (getenv('USE_ZEND_ALLOC') === '0' && !getenv('SKIP_ASAN')) { + die('skip: canonical FFE fixture sweep is too slow for valgrind'); +} ?> --ENV-- DD_TRACE_GENERATE_ROOT_SPAN=0 From 4a810a2afa77bb0966fe9700d806b0340040a4a5 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Fri, 22 May 2026 14:34:05 -0400 Subject: [PATCH 06/14] Make FFE runtime state thread-local --- components-rs/ffe.rs | 98 ++++++++++++++++++++++++++++++++------------ 1 file changed, 71 insertions(+), 27 deletions(-) diff --git a/components-rs/ffe.rs b/components-rs/ffe.rs index 2f20ad3409a..988d83f3d05 100644 --- a/components-rs/ffe.rs +++ b/components-rs/ffe.rs @@ -2,34 +2,37 @@ use datadog_ffe::rules_based::{ self as ffe, AssignmentReason, AssignmentValue, Attribute, Configuration, EvaluationContext, EvaluationError, ExpectedFlagType, Str, UniversalFlagConfig, }; +use std::cell::RefCell; use std::collections::HashMap; use std::ffi::{c_char, CStr, CString}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; struct FfeState { config: Option, version: u64, } -lazy_static::lazy_static! { - static ref FFE_STATE: Mutex = Mutex::new(FfeState { +thread_local! { + static FFE_STATE: RefCell = const { RefCell::new(FfeState { config: None, version: 0, - }); + }) }; } pub fn store_config(config: Configuration) { - if let Ok(mut state) = FFE_STATE.lock() { + FFE_STATE.with(|state| { + let mut state = state.borrow_mut(); state.config = Some(config); state.version = state.version.wrapping_add(1); - } + }); } pub fn clear_config() { - if let Ok(mut state) = FFE_STATE.lock() { + FFE_STATE.with(|state| { + let mut state = state.borrow_mut(); state.config = None; state.version = state.version.wrapping_add(1); - } + }); } #[no_mangle] @@ -54,15 +57,12 @@ pub extern "C" fn ddog_ffe_load_config(json: *const c_char) -> bool { #[no_mangle] pub extern "C" fn ddog_ffe_has_config() -> bool { - FFE_STATE - .lock() - .map(|state| state.config.is_some()) - .unwrap_or(false) + FFE_STATE.with(|state| state.borrow().config.is_some()) } #[no_mangle] pub extern "C" fn ddog_ffe_config_version() -> u64 { - FFE_STATE.lock().map(|state| state.version).unwrap_or(0) + FFE_STATE.with(|state| state.borrow().version) } const REASON_STATIC: i32 = 0; @@ -145,20 +145,18 @@ pub extern "C" fn ddog_ffe_evaluate( let attributes = parse_attributes(attributes, attributes_count); let context = EvaluationContext::new(targeting_key, Arc::new(attributes)); - let state = match FFE_STATE.lock() { - Ok(state) => state, - Err(_) => return std::ptr::null_mut(), - }; - - let assignment = ffe::get_assignment( - state.config.as_ref(), - flag_key, - &context, - expected_type, - ffe::now(), - ); - - Box::into_raw(Box::new(result_from_assignment(assignment))) + FFE_STATE.with(|state| { + let state = state.borrow(); + let assignment = ffe::get_assignment( + state.config.as_ref(), + flag_key, + &context, + expected_type, + ffe::now(), + ); + + Box::into_raw(Box::new(result_from_assignment(assignment))) + }) } #[no_mangle] @@ -331,3 +329,49 @@ fn assignment_value_to_json(value: &AssignmentValue) -> String { fn string_to_cstring(value: String) -> CString { CString::new(value).unwrap_or_default() } + +#[cfg(test)] +mod tests { + use super::*; + + const EMPTY_CONFIG: &str = r#"{ + "createdAt": "2026-05-22T00:00:00.000Z", + "format": "SERVER", + "environment": { + "name": "Test" + }, + "flags": {} + }"#; + + fn load_empty_config() -> bool { + let json = CString::new(EMPTY_CONFIG).expect("test fixture is valid cstring"); + ddog_ffe_load_config(json.as_ptr()) + } + + #[test] + fn configuration_state_is_thread_local() { + clear_config(); + let empty_version = ddog_ffe_config_version(); + assert!(!ddog_ffe_has_config()); + + assert!(load_empty_config()); + assert!(ddog_ffe_has_config()); + let loaded_version = ddog_ffe_config_version(); + assert_eq!(loaded_version, empty_version.wrapping_add(1)); + + let child = std::thread::spawn(|| { + assert!(!ddog_ffe_has_config()); + assert_eq!(ddog_ffe_config_version(), 0); + + assert!(load_empty_config()); + assert!(ddog_ffe_has_config()); + assert_eq!(ddog_ffe_config_version(), 1); + }); + + child.join().expect("child thread should not panic"); + + assert!(ddog_ffe_has_config()); + assert_eq!(ddog_ffe_config_version(), loaded_version); + clear_config(); + } +} From 2f46d9759c344d3d79bd8b19871ef75f39a25e60 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Fri, 22 May 2026 14:41:01 -0400 Subject: [PATCH 07/14] Preload FFE API classes for OpenFeature bridge --- src/bridge/_files_openfeature.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/bridge/_files_openfeature.php b/src/bridge/_files_openfeature.php index 7572940becd..1c4e9142bdb 100644 --- a/src/bridge/_files_openfeature.php +++ b/src/bridge/_files_openfeature.php @@ -1,6 +1,18 @@ Date: Fri, 22 May 2026 14:46:06 -0400 Subject: [PATCH 08/14] Return defaults for null FFE assignments --- .../{ => Internal}/ResultMapper.php | 44 +++++++++++++++++-- .../Unit/FeatureFlags/ResultMapperTest.php | 38 +++++++++++++++- 2 files changed, 78 insertions(+), 4 deletions(-) rename src/api/FeatureFlags/{ => Internal}/ResultMapper.php (87%) diff --git a/src/api/FeatureFlags/ResultMapper.php b/src/api/FeatureFlags/Internal/ResultMapper.php similarity index 87% rename from src/api/FeatureFlags/ResultMapper.php rename to src/api/FeatureFlags/Internal/ResultMapper.php index 0967ab16d39..9a971192bb3 100644 --- a/src/api/FeatureFlags/ResultMapper.php +++ b/src/api/FeatureFlags/Internal/ResultMapper.php @@ -1,6 +1,11 @@ mapReason($this->read($rawResult, array('reason'), self::BRIDGE_REASON_DEFAULT)); + if ($this->isDefaultReturn($rawResult, $reason)) { + return $this->defaultDetails($defaultValue, $expectedType, $reason, $rawResult); + } + $decoded = null; $decodeError = $this->decodeValue($rawResult, $expectedType, $decoded); if ($decodeError !== null) { @@ -80,8 +90,6 @@ public function map($rawResult, $expectedType, $defaultValue) ); } - $reason = $this->mapReason($this->read($rawResult, array('reason'), self::BRIDGE_REASON_DEFAULT)); - return new EvaluationDetails( $decoded, $expectedType, @@ -95,6 +103,21 @@ public function map($rawResult, $expectedType, $defaultValue) ); } + private function defaultDetails($defaultValue, $expectedType, $reason, array $rawResult) + { + return new EvaluationDetails( + $defaultValue, + $expectedType, + $reason, + null, + null, + null, + $this->readArray($rawResult, array('flag_metadata', 'flagMetadata', 'metadata')), + array(), + $this->providerState($rawResult) + ); + } + private function errorDetails( $defaultValue, $expectedType, @@ -138,6 +161,21 @@ private function decodeValue(array $rawResult, $expectedType, &$decoded) return null; } + private function isDefaultReturn(array $rawResult, $reason) + { + if ($reason !== EvaluationReason::DEFAULT_REASON && $reason !== EvaluationReason::DISABLED) { + return false; + } + + if (array_key_exists('value', $rawResult)) { + return $rawResult['value'] === null; + } + + $valueJson = $this->read($rawResult, array('value_json', 'valueJson'), null); + + return is_string($valueJson) && trim($valueJson) === 'null'; + } + private function coerceValue($value, $expectedType, &$coerced) { switch ($expectedType) { diff --git a/tests/api/Unit/FeatureFlags/ResultMapperTest.php b/tests/api/Unit/FeatureFlags/ResultMapperTest.php index e4574080d9c..4fad4147783 100644 --- a/tests/api/Unit/FeatureFlags/ResultMapperTest.php +++ b/tests/api/Unit/FeatureFlags/ResultMapperTest.php @@ -6,7 +6,7 @@ use DDTrace\FeatureFlags\EvaluationErrorCode; use DDTrace\FeatureFlags\EvaluationReason; use DDTrace\FeatureFlags\EvaluationType; -use DDTrace\FeatureFlags\ResultMapper; +use DDTrace\FeatureFlags\Internal\ResultMapper; use PHPUnit\Framework\TestCase; final class ResultMapperTest extends TestCase @@ -109,6 +109,42 @@ public function testDecodedTypeMismatchMapsToTypeMismatch() $this->assertSame(EvaluationErrorCode::TYPE_MISMATCH, $details->getErrorCode()); } + public function testDisabledResultReturnsDefaultWithoutTypeMismatch() + { + $details = (new ResultMapper())->map(array( + 'value_json' => 'null', + 'variant' => null, + 'allocation_key' => null, + 'reason' => ResultMapper::BRIDGE_REASON_DISABLED, + 'error_code' => ResultMapper::BRIDGE_ERROR_NONE, + 'do_log' => false, + 'has_config' => true, + 'config_version' => 7, + ), EvaluationType::BOOLEAN, true); + + $this->assertTrue($details->getValue()); + $this->assertSame(EvaluationReason::DISABLED, $details->getReason()); + $this->assertNull($details->getErrorCode()); + $this->assertNull($details->getVariant()); + $this->assertSame(array(), $details->getExposureData()); + $this->assertSame(array('hasConfig' => true, 'configVersion' => 7), $details->getProviderState()); + $this->assertFalse($details->isError()); + } + + public function testDefaultNullResultReturnsDefaultWithoutTypeMismatch() + { + $details = (new ResultMapper())->map(array( + 'value_json' => 'null', + 'reason' => ResultMapper::BRIDGE_REASON_DEFAULT, + 'error_code' => ResultMapper::BRIDGE_ERROR_NONE, + ), EvaluationType::STRING, 'fallback'); + + $this->assertSame('fallback', $details->getValue()); + $this->assertSame(EvaluationReason::DEFAULT_REASON, $details->getReason()); + $this->assertNull($details->getErrorCode()); + $this->assertFalse($details->isError()); + } + public function testIntegerJsonCanMapToFloat() { $details = (new ResultMapper())->map(array( From eaa44fc1852b8577c00064e40529b9feb0526637 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Fri, 22 May 2026 14:51:53 -0400 Subject: [PATCH 09/14] Avoid duplicate OpenFeature bridge preloads --- src/bridge/_files_openfeature.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/bridge/_files_openfeature.php b/src/bridge/_files_openfeature.php index 1c4e9142bdb..d46c20f4164 100644 --- a/src/bridge/_files_openfeature.php +++ b/src/bridge/_files_openfeature.php @@ -1,18 +1,5 @@ Date: Fri, 22 May 2026 14:56:50 -0400 Subject: [PATCH 10/14] Validate FFE runtime through PHP system tests --- Makefile | 3 ++ ext/ddtrace.c | 5 ++-- ext/ddtrace.stub.php | 21 +++++++------- ext/ddtrace_arginfo.h | 6 ++-- ext/remote_config.c | 11 +++++++ ext/remote_config.h | 1 + src/DDTrace/OpenFeature/DataDogProvider.php | 29 +++++++++++++++---- src/api/FeatureFlags/Client.php | 17 +++++++++-- .../FeatureFlags/{ => Internal}/Evaluator.php | 4 ++- .../{ => Internal}/NativeEvaluator.php | 4 ++- .../Internal}/NoopWarningEmitter.php | 4 +-- .../{ => Internal}/RemoteConfigClient.php | 2 +- .../TriggerErrorWarningEmitter.php | 2 +- .../{ => Internal}/UnavailableEvaluator.php | 6 +++- .../{ => Internal}/WarningEmitter.php | 2 +- tests/OpenFeature/DataDogProviderTest.php | 28 +++++++++--------- tests/api/Unit/FeatureFlags/ClientTest.php | 25 +++++++++------- .../FeatureFlags/RemoteConfigClientTest.php | 2 +- tests/ext/ffe/native_bridge_evaluate.phpt | 2 +- tests/ext/ffe/system_test_data_evaluate.phpt | 19 ++++++++---- tests/internal-api-stress-test.php | 1 - 21 files changed, 129 insertions(+), 65 deletions(-) rename src/api/FeatureFlags/{ => Internal}/Evaluator.php (83%) rename src/api/FeatureFlags/{ => Internal}/NativeEvaluator.php (98%) rename src/{DDTrace/OpenFeature => api/FeatureFlags/Internal}/NoopWarningEmitter.php (66%) rename src/api/FeatureFlags/{ => Internal}/RemoteConfigClient.php (96%) rename src/api/FeatureFlags/{ => Internal}/TriggerErrorWarningEmitter.php (81%) rename src/api/FeatureFlags/{ => Internal}/UnavailableEvaluator.php (82%) rename src/api/FeatureFlags/{ => Internal}/WarningEmitter.php (77%) diff --git a/Makefile b/Makefile index 319ebd67489..51db7ad45cd 100644 --- a/Makefile +++ b/Makefile @@ -1329,7 +1329,10 @@ test_metrics: global_test_run_dependencies $(call run_tests,--testsuite=metrics $(TESTS)) test_featureflags: global_test_run_dependencies tests/OpenFeature/composer.lock-php$(PHP_MAJOR_MINOR) + $(eval FEATUREFLAGS_TEST_EXTRA_INI := $(TEST_EXTRA_INI)) + $(eval TEST_EXTRA_INI=$(FEATUREFLAGS_TEST_EXTRA_INI) -d auto_prepend_file=$(PWD)/tests/OpenFeature/vendor/autoload.php) $(call run_tests,--testsuite=featureflags $(TESTS)) + $(eval TEST_EXTRA_INI=$(FEATUREFLAGS_TEST_EXTRA_INI)) benchmarks_run_dependencies: global_test_run_dependencies tests/Frameworks/Symfony/Version_5_2/composer.lock-php$(PHP_MAJOR_MINOR) tests/Frameworks/Laravel/Version_10_x/composer.lock-php$(PHP_MAJOR_MINOR) tests/Benchmarks/composer.lock-php$(PHP_MAJOR_MINOR) php tests/Frameworks/Symfony/Version_5_2/bin/console cache:clear --no-warmup --env=prod diff --git a/ext/ddtrace.c b/ext/ddtrace.c index 1d231a98bd3..2d93536ea5f 100644 --- a/ext/ddtrace.c +++ b/ext/ddtrace.c @@ -2963,7 +2963,7 @@ PHP_FUNCTION(DDTrace_ffe_config_version) { RETURN_LONG((zend_long) ddog_ffe_config_version()); } -PHP_FUNCTION(DDTrace_ffe_load_config) { +PHP_FUNCTION(DDTrace_Testing_ffe_load_config) { char *json; size_t json_len; @@ -3005,7 +3005,7 @@ PHP_FUNCTION(DDTrace_ffe_evaluate) { UNUSED(flag_key_len); type_id = (int32_t) type_id_zl; - tk = targeting_key_len > 0 ? targeting_key : NULL; + tk = targeting_key != NULL ? targeting_key : NULL; attributes = Z_ARRVAL_P(attrs_zv); attrs_count = zend_hash_num_elements(attributes); @@ -3047,6 +3047,7 @@ PHP_FUNCTION(DDTrace_ffe_evaluate) { attrs_count = idx; } + ddtrace_process_remote_config_now(); result = ddog_ffe_evaluate(flag_key, type_id, tk, c_attrs, attrs_count); if (c_attrs) { efree(c_attrs); diff --git a/ext/ddtrace.stub.php b/ext/ddtrace.stub.php index 471137bdafd..460cd6d9402 100644 --- a/ext/ddtrace.stub.php +++ b/ext/ddtrace.stub.php @@ -877,17 +877,6 @@ function ffe_has_config(): bool {} */ function ffe_config_version(): int {} - /** - * Load a UFC JSON configuration string into the FFE engine. - * Used for testing without Remote Config. - * - * @param string $json UFC JSON configuration string. - * @return bool True if the configuration was parsed and loaded successfully. - * - * @internal Used by tests. - */ - function ffe_load_config(string $json): bool {} - } namespace DDTrace\System { @@ -967,6 +956,16 @@ function set_blocking_function(\DDTrace\RootSpanData $span, callable $blockingFu } namespace DDTrace\Testing { + /** + * Load a UFC JSON configuration string into the FFE engine. + * + * @param string $json UFC JSON configuration string. + * @return bool True if the configuration was parsed and loaded successfully. + * + * @internal Used by extension tests only. + */ + function ffe_load_config(string $json): bool {} + /** * Overrides PHP's default error handling. * diff --git a/ext/ddtrace_arginfo.h b/ext/ddtrace_arginfo.h index d782a8fba24..313515a9862 100644 --- a/ext/ddtrace_arginfo.h +++ b/ext/ddtrace_arginfo.h @@ -189,7 +189,7 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_ffe_config_version, 0, 0, IS_LONG, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_ffe_load_config, 0, 1, _IS_BOOL, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_Testing_ffe_load_config, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, json, IS_STRING, 0) ZEND_END_ARG_INFO() @@ -414,7 +414,7 @@ ZEND_FUNCTION(DDTrace_flush_endpoints); ZEND_FUNCTION(DDTrace_ffe_evaluate); ZEND_FUNCTION(DDTrace_ffe_has_config); ZEND_FUNCTION(DDTrace_ffe_config_version); -ZEND_FUNCTION(DDTrace_ffe_load_config); +ZEND_FUNCTION(DDTrace_Testing_ffe_load_config); ZEND_FUNCTION(DDTrace_System_container_id); ZEND_FUNCTION(DDTrace_System_process_tags_base_hash); ZEND_FUNCTION(DDTrace_Config_integration_analytics_enabled); @@ -513,7 +513,6 @@ static const zend_function_entry ext_functions[] = { ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_evaluate"), zif_DDTrace_ffe_evaluate, arginfo_DDTrace_ffe_evaluate, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_has_config"), zif_DDTrace_ffe_has_config, arginfo_DDTrace_ffe_has_config, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_config_version"), zif_DDTrace_ffe_config_version, arginfo_DDTrace_ffe_config_version, 0, NULL, NULL) - ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace", "ffe_load_config"), zif_DDTrace_ffe_load_config, arginfo_DDTrace_ffe_load_config, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\System", "container_id"), zif_DDTrace_System_container_id, arginfo_DDTrace_System_container_id, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\System", "process_tags_base_hash"), zif_DDTrace_System_process_tags_base_hash, arginfo_DDTrace_System_process_tags_base_hash, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Config", "integration_analytics_enabled"), zif_DDTrace_Config_integration_analytics_enabled, arginfo_DDTrace_Config_integration_analytics_enabled, 0, NULL, NULL) @@ -522,6 +521,7 @@ static const zend_function_entry ext_functions[] = { ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\UserRequest", "notify_start"), zif_DDTrace_UserRequest_notify_start, arginfo_DDTrace_UserRequest_notify_start, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\UserRequest", "notify_commit"), zif_DDTrace_UserRequest_notify_commit, arginfo_DDTrace_UserRequest_notify_commit, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\UserRequest", "set_blocking_function"), zif_DDTrace_UserRequest_set_blocking_function, arginfo_DDTrace_UserRequest_set_blocking_function, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Testing", "ffe_load_config"), zif_DDTrace_Testing_ffe_load_config, arginfo_DDTrace_Testing_ffe_load_config, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Testing", "trigger_error"), zif_DDTrace_Testing_trigger_error, arginfo_DDTrace_Testing_trigger_error, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Testing", "emit_asm_event"), zif_DDTrace_Testing_emit_asm_event, arginfo_DDTrace_Testing_emit_asm_event, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Testing", "normalize_tag_value"), zif_DDTrace_Testing_normalize_tag_value, arginfo_DDTrace_Testing_normalize_tag_value, 0, NULL, NULL) diff --git a/ext/remote_config.c b/ext/remote_config.c index a6600b6f620..ed9bbec929f 100644 --- a/ext/remote_config.c +++ b/ext/remote_config.c @@ -64,6 +64,17 @@ void ddtrace_check_for_new_config_now(void) { } } +void ddtrace_process_remote_config_now(void) { + if (!DDTRACE_G(remote_config_state)) { + return; + } + + DDTRACE_G(reread_remote_configuration) = 0; + if (ddog_process_remote_configs(DDTRACE_G(remote_config_state))) { + ddtrace_set_all_thread_vm_interrupt(); + } +} + #ifndef _WIN32 static void dd_sigvtalarm_handler(int signal, siginfo_t *siginfo, void *ctx) { UNUSED(signal, siginfo, ctx); diff --git a/ext/remote_config.h b/ext/remote_config.h index 88e04808c5b..521909c7d55 100644 --- a/ext/remote_config.h +++ b/ext/remote_config.h @@ -8,6 +8,7 @@ void ddtrace_mshutdown_remote_config(void); void ddtrace_rinit_remote_config(void); void ddtrace_rshutdown_remote_config(void); void ddtrace_check_for_new_config_now(void); +void ddtrace_process_remote_config_now(void); DDTRACE_PUBLIC void ddtrace_set_all_thread_vm_interrupt(void); diff --git a/src/DDTrace/OpenFeature/DataDogProvider.php b/src/DDTrace/OpenFeature/DataDogProvider.php index 79a232ec8c7..fcc534ab92a 100644 --- a/src/DDTrace/OpenFeature/DataDogProvider.php +++ b/src/DDTrace/OpenFeature/DataDogProvider.php @@ -8,8 +8,9 @@ use DDTrace\FeatureFlags\EvaluationDetails; use DDTrace\FeatureFlags\EvaluationErrorCode; use DDTrace\FeatureFlags\EvaluationReason; -use DDTrace\FeatureFlags\TriggerErrorWarningEmitter; -use DDTrace\FeatureFlags\WarningEmitter; +use DDTrace\FeatureFlags\Internal\NoopWarningEmitter; +use DDTrace\FeatureFlags\Internal\TriggerErrorWarningEmitter; +use DDTrace\FeatureFlags\Internal\WarningEmitter; use OpenFeature\implementation\provider\AbstractProvider; use OpenFeature\implementation\provider\ResolutionDetailsBuilder; use OpenFeature\implementation\provider\ResolutionError; @@ -27,10 +28,28 @@ final class DataDogProvider extends AbstractProvider private WarningEmitter $warningEmitter; private bool $warnedAboutNonProductionRuntime = false; - public function __construct(?FeatureFlagsClient $client = null, ?WarningEmitter $warningEmitter = null) + public function __construct() { - $this->client = $client ?? FeatureFlagsClient::create(null, new NoopWarningEmitter()); - $this->warningEmitter = $warningEmitter ?? new TriggerErrorWarningEmitter(); + $this->client = FeatureFlagsClient::createWithDependencies(null, new NoopWarningEmitter()); + $this->warningEmitter = new TriggerErrorWarningEmitter(); + } + + /** + * @internal Tests and Datadog-owned bridge adapters only. + */ + public static function createWithDependencies( + ?FeatureFlagsClient $client = null, + ?WarningEmitter $warningEmitter = null + ): self { + $provider = new self(); + if ($client !== null) { + $provider->client = $client; + } + if ($warningEmitter !== null) { + $provider->warningEmitter = $warningEmitter; + } + + return $provider; } public function resolveBooleanValue( diff --git a/src/api/FeatureFlags/Client.php b/src/api/FeatureFlags/Client.php index 522b40ff689..8bc2cdc6fc9 100644 --- a/src/api/FeatureFlags/Client.php +++ b/src/api/FeatureFlags/Client.php @@ -2,13 +2,18 @@ namespace DDTrace\FeatureFlags; +use DDTrace\FeatureFlags\Internal\Evaluator; +use DDTrace\FeatureFlags\Internal\NativeEvaluator; +use DDTrace\FeatureFlags\Internal\TriggerErrorWarningEmitter; +use DDTrace\FeatureFlags\Internal\WarningEmitter; + final class Client { private $evaluator; private $warningEmitter; private $warnedAboutNonProductionRuntime = false; - public function __construct( + private function __construct( Evaluator $evaluator, WarningEmitter $warningEmitter ) { @@ -16,7 +21,15 @@ public function __construct( $this->warningEmitter = $warningEmitter; } - public static function create( + public static function create() + { + return self::createWithDependencies(); + } + + /** + * @internal Tests and Datadog-owned bridge adapters only. + */ + public static function createWithDependencies( $evaluator = null, $warningEmitter = null ) { diff --git a/src/api/FeatureFlags/Evaluator.php b/src/api/FeatureFlags/Internal/Evaluator.php similarity index 83% rename from src/api/FeatureFlags/Evaluator.php rename to src/api/FeatureFlags/Internal/Evaluator.php index 51af0f32d65..132e26cbcb2 100644 --- a/src/api/FeatureFlags/Evaluator.php +++ b/src/api/FeatureFlags/Internal/Evaluator.php @@ -1,6 +1,8 @@ clientForEvaluator(new OpenFeatureTestEvaluator())); + $provider = new DataDogProvider(); self::assertSame('Datadog', $provider->getMetadata()->getName()); } @@ -41,7 +41,7 @@ public function testOpenFeatureClientResolvesTypedValuesThroughDatadogClient(): ->setSuccess('float.flag', 3.5) ->setSuccess('object.flag', ['enabled' => true]); - $client = $this->openFeatureClientFor(new DataDogProvider($this->clientForEvaluator($evaluator))); + $client = $this->openFeatureClientFor(DataDogProvider::createWithDependencies($this->clientForEvaluator($evaluator))); self::assertTrue($client->getBooleanValue('bool.flag', false)); self::assertSame('blue', $client->getStringValue('string.flag', 'red')); @@ -62,7 +62,7 @@ public function testStaticReasonIsPreservedAsDatadogReason(): void $evaluator = new OpenFeatureTestEvaluator(); $evaluator->setSuccess('static.flag', 'value', EvaluationReason::STATIC_REASON); - $client = $this->openFeatureClientFor(new DataDogProvider($this->clientForEvaluator($evaluator))); + $client = $this->openFeatureClientFor(DataDogProvider::createWithDependencies($this->clientForEvaluator($evaluator))); $details = $client->getStringDetails('static.flag', 'fallback'); self::assertSame(EvaluationReason::STATIC_REASON, $details->getReason()); @@ -73,7 +73,7 @@ public function testEvaluationContextIsNormalizedForDatadogClient(): void $evaluator = new OpenFeatureTestEvaluator(); $evaluator->setSuccess('context.flag', 'on'); - $provider = new DataDogProvider($this->clientForEvaluator($evaluator)); + $provider = DataDogProvider::createWithDependencies($this->clientForEvaluator($evaluator)); $provider->resolveStringValue('context.flag', 'off', new EvaluationContext( 'user-123', new Attributes([ @@ -101,7 +101,7 @@ public function testEvaluationContextIsNormalizedForDatadogClient(): void public function testUnavailableRuntimeReturnsDefaultDetailsAndOneWarning(): void { $warnings = new OpenFeatureRecordingWarningEmitter(); - $client = $this->openFeatureClientFor(new DataDogProvider(null, $warnings)); + $client = $this->openFeatureClientFor(DataDogProvider::createWithDependencies(null, $warnings)); $value = $client->getBooleanValue('checkout.enabled', true); $details = $client->getStringDetails('checkout.copy', 'fallback'); @@ -125,7 +125,7 @@ public function testProviderWarningIsEmittedOncePerProvider(): void ->setUnavailable('first.flag', true, 'temporary unavailable') ->setUnavailable('second.flag', true, 'temporary unavailable'); - $client = $this->openFeatureClientFor(new DataDogProvider($this->clientForEvaluator($evaluator), $warnings)); + $client = $this->openFeatureClientFor(DataDogProvider::createWithDependencies($this->clientForEvaluator($evaluator), $warnings)); $client->getBooleanValue('first.flag', false); $client->getBooleanValue('second.flag', false); @@ -138,7 +138,7 @@ public function testProviderErrorsMapToOpenFeatureDetails(): void $evaluator = new OpenFeatureTestEvaluator(); $evaluator->setFlagNotFound('missing.flag'); - $client = $this->openFeatureClientFor(new DataDogProvider($this->clientForEvaluator($evaluator))); + $client = $this->openFeatureClientFor(DataDogProvider::createWithDependencies($this->clientForEvaluator($evaluator))); $details = $client->getStringDetails('missing.flag', 'fallback'); self::assertSame('fallback', $details->getValue()); @@ -152,7 +152,7 @@ public function testTypeMismatchReturnsDefaultWithOpenFeatureError(): void $evaluator = new OpenFeatureTestEvaluator(); $evaluator->setSuccess('integer.flag', 'not-an-int'); - $client = $this->openFeatureClientFor(new DataDogProvider($this->clientForEvaluator($evaluator))); + $client = $this->openFeatureClientFor(DataDogProvider::createWithDependencies($this->clientForEvaluator($evaluator))); $details = $client->getIntegerDetails('integer.flag', 7); self::assertSame(7, $details->getValue()); @@ -162,7 +162,7 @@ public function testTypeMismatchReturnsDefaultWithOpenFeatureError(): void private function clientForEvaluator(Evaluator $evaluator): FeatureFlagsClient { - return FeatureFlagsClient::create($evaluator, new NoopWarningEmitter()); + return FeatureFlagsClient::createWithDependencies($evaluator, new NoopWarningEmitter()); } private function openFeatureClientFor(DataDogProvider $provider) diff --git a/tests/api/Unit/FeatureFlags/ClientTest.php b/tests/api/Unit/FeatureFlags/ClientTest.php index 1e0a3cf43cb..b5fc53b72e1 100644 --- a/tests/api/Unit/FeatureFlags/ClientTest.php +++ b/tests/api/Unit/FeatureFlags/ClientTest.php @@ -7,14 +7,19 @@ use DDTrace\FeatureFlags\EvaluationErrorCode; use DDTrace\FeatureFlags\EvaluationReason; use DDTrace\FeatureFlags\EvaluationType; -use DDTrace\FeatureFlags\Evaluator; -use DDTrace\FeatureFlags\NativeEvaluator; -use DDTrace\FeatureFlags\UnavailableEvaluator; -use DDTrace\FeatureFlags\WarningEmitter; +use DDTrace\FeatureFlags\Internal\Evaluator; +use DDTrace\FeatureFlags\Internal\NativeEvaluator; +use DDTrace\FeatureFlags\Internal\UnavailableEvaluator; +use DDTrace\FeatureFlags\Internal\WarningEmitter; use PHPUnit\Framework\TestCase; final class ClientTest extends TestCase { + public function testCreateBuildsDefaultRemoteConfigBackedClient() + { + $this->assertInstanceOf(Client::class, Client::create()); + } + public function testValueMethodsReturnEvaluatedValues() { $evaluator = new ClientTestEvaluator(); @@ -25,7 +30,7 @@ public function testValueMethodsReturnEvaluatedValues() ->setSuccess('float.flag', 3.5) ->setSuccess('object.flag', array('enabled' => true)); - $client = Client::create($evaluator, new RecordingWarningEmitter()); + $client = Client::createWithDependencies($evaluator, new RecordingWarningEmitter()); $this->assertTrue($client->getBooleanValue('bool.flag', false)); $this->assertSame('blue', $client->getStringValue('string.flag', 'red')); @@ -47,7 +52,7 @@ public function testDetailsMethodsExposeEvaluationDetails() array('runtime' => 'test', 'hasConfig' => true) ); - $client = Client::create($evaluator, new RecordingWarningEmitter()); + $client = Client::createWithDependencies($evaluator, new RecordingWarningEmitter()); $details = $client->getBooleanDetails('checkout-redesign', false); @@ -65,7 +70,7 @@ public function testContextNormalizesTargetingKeyAndPrimitiveAttributes() $evaluator = new ClientTestEvaluator(); $evaluator->setSuccess('flag.context', 'on'); - $client = Client::create($evaluator, new RecordingWarningEmitter()); + $client = Client::createWithDependencies($evaluator, new RecordingWarningEmitter()); $client->getStringValue('flag.context', 'off', array( 'targetingKey' => 123, 'attributes' => array( @@ -93,7 +98,7 @@ public function testContextNormalizesTargetingKeyAndPrimitiveAttributes() public function testUnavailableRuntimeReturnsDefaultWithProviderNotReadyDetailsAndWarning() { $warnings = new RecordingWarningEmitter(); - $client = Client::create(null, $warnings); + $client = Client::createWithDependencies(null, $warnings); $value = $client->getBooleanValue('checkout-redesign', true); $details = $client->getStringDetails('checkout-copy', 'fallback'); @@ -120,7 +125,7 @@ public function testUnavailableRuntimeReturnsDefaultWithProviderNotReadyDetailsA public function testWarningIsEmittedOncePerClientNotOncePerEvaluation() { $warnings = new RecordingWarningEmitter(); - $client = Client::create(null, $warnings); + $client = Client::createWithDependencies(null, $warnings); $client->getBooleanValue('flag-1', false); $client->getBooleanValue('flag-2', false); @@ -134,7 +139,7 @@ public function testWarningIsEmittedOncePerClientNotOncePerEvaluation() */ public function testTypedMethodsRejectInvalidDefaults($method, $defaultValue) { - $client = Client::create(new ClientTestEvaluator(), new RecordingWarningEmitter()); + $client = Client::createWithDependencies(new ClientTestEvaluator(), new RecordingWarningEmitter()); $this->expectException(\InvalidArgumentException::class); diff --git a/tests/api/Unit/FeatureFlags/RemoteConfigClientTest.php b/tests/api/Unit/FeatureFlags/RemoteConfigClientTest.php index cfb636f994d..176c4ae4aed 100644 --- a/tests/api/Unit/FeatureFlags/RemoteConfigClientTest.php +++ b/tests/api/Unit/FeatureFlags/RemoteConfigClientTest.php @@ -2,7 +2,7 @@ namespace DDTrace\Tests\Api\Unit\FeatureFlags; -use DDTrace\FeatureFlags\RemoteConfigClient; +use DDTrace\FeatureFlags\Internal\RemoteConfigClient; use PHPUnit\Framework\TestCase; final class RemoteConfigClientTest extends TestCase diff --git a/tests/ext/ffe/native_bridge_evaluate.phpt b/tests/ext/ffe/native_bridge_evaluate.phpt index fc7a3687b79..29943e5f9a1 100644 --- a/tests/ext/ffe/native_bridge_evaluate.phpt +++ b/tests/ext/ffe/native_bridge_evaluate.phpt @@ -59,7 +59,7 @@ $config = <<<'JSON' } JSON; -show('load', \DDTrace\ffe_load_config($config)); +show('load', \DDTrace\Testing\ffe_load_config($config)); show('has_config_after', \DDTrace\ffe_has_config()); show('success', \DDTrace\ffe_evaluate('string.flag', 0, 'user-1', array( 'country' => 'US', diff --git a/tests/ext/ffe/system_test_data_evaluate.phpt b/tests/ext/ffe/system_test_data_evaluate.phpt index 1fbe17bd135..7cf653f947b 100644 --- a/tests/ext/ffe/system_test_data_evaluate.phpt +++ b/tests/ext/ffe/system_test_data_evaluate.phpt @@ -21,7 +21,7 @@ $fixtureRoot = $root . '/tests/FeatureFlags/ffe-system-test-data'; require_feature_flag_api($root); -final class FfeFixtureWarningEmitter implements \DDTrace\FeatureFlags\WarningEmitter +final class FfeFixtureWarningEmitter implements \DDTrace\FeatureFlags\Internal\WarningEmitter { public function warning($message) { @@ -38,7 +38,7 @@ if ($configJson === false) { throw new \RuntimeException('failed to read fixture config: ' . $configPath); } -show('config_loaded', \DDTrace\ffe_load_config($configJson)); +show('config_loaded', \DDTrace\Testing\ffe_load_config($configJson)); $caseFiles = glob($fixtureRoot . '/evaluation-cases/*.json'); if ($caseFiles === false) { @@ -50,8 +50,8 @@ if (count($caseFiles) === 0) { throw new \RuntimeException('no evaluation-case fixture files found under ' . $fixtureRoot); } -$client = \DDTrace\FeatureFlags\Client::create( - new \DDTrace\FeatureFlags\NativeEvaluator(), +$client = \DDTrace\FeatureFlags\Client::createWithDependencies( + new \DDTrace\FeatureFlags\Internal\NativeEvaluator(), new FfeFixtureWarningEmitter() ); @@ -90,6 +90,12 @@ function require_feature_flag_api($root) 'EvaluationReason', 'EvaluationErrorCode', 'EvaluationDetails', + ) as $classFile) { + require_once $apiRoot . '/' . $classFile . '.php'; + } + + $internalRoot = $apiRoot . '/Internal'; + foreach (array( 'Evaluator', 'WarningEmitter', 'ResultMapper', @@ -97,10 +103,11 @@ function require_feature_flag_api($root) 'UnavailableEvaluator', 'TriggerErrorWarningEmitter', 'NativeEvaluator', - 'Client', ) as $classFile) { - require_once $apiRoot . '/' . $classFile . '.php'; + require_once $internalRoot . '/' . $classFile . '.php'; } + + require_once $apiRoot . '/Client.php'; } function run_fixture_case($client, $fileName, $index, array $case, array &$failures) diff --git a/tests/internal-api-stress-test.php b/tests/internal-api-stress-test.php index 3bbd671f62f..8f881d821ec 100644 --- a/tests/internal-api-stress-test.php +++ b/tests/internal-api-stress-test.php @@ -132,7 +132,6 @@ function ($hook = null) { } $minFunctionArgs = [ - 'DDTrace\ffe_load_config' => 1, 'DDTrace\ffe_evaluate' => 4, ]; From 555e9a09b8e26f2c45a9ec04058187551bd0cfb4 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Fri, 22 May 2026 15:26:13 -0400 Subject: [PATCH 11/14] fix: preserve empty FFE targeting keys --- components-rs/ffe.rs | 70 ++++++++++++++++++++++- tests/ext/ffe/native_bridge_evaluate.phpt | 23 ++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/components-rs/ffe.rs b/components-rs/ffe.rs index 988d83f3d05..cf8da11a18c 100644 --- a/components-rs/ffe.rs +++ b/components-rs/ffe.rs @@ -137,7 +137,7 @@ pub extern "C" fn ddog_ffe_evaluate( None } else { match unsafe { CStr::from_ptr(targeting_key) }.to_str() { - Ok(targeting_key) if !targeting_key.is_empty() => Some(Str::from(targeting_key)), + Ok(targeting_key) => Some(Str::from(targeting_key)), _ => None, } }; @@ -348,6 +348,74 @@ mod tests { ddog_ffe_load_config(json.as_ptr()) } + const EMPTY_TARGETING_KEY_CONFIG: &str = r#"{ + "createdAt": "2026-05-22T00:00:00.000Z", + "format": "SERVER", + "environment": { + "name": "Test" + }, + "flags": { + "empty.targeting.shard.flag": { + "key": "empty.targeting.shard.flag", + "enabled": true, + "variationType": "STRING", + "variations": { + "empty-target": { + "key": "empty-target", + "value": "empty-targeting-key" + } + }, + "allocations": [{ + "key": "alloc-empty-targeting-key", + "rules": [], + "splits": [{ + "variationKey": "empty-target", + "shards": [{ + "salt": "empty-targeting-key-regression", + "totalShards": 10000, + "ranges": [{"start": 8022, "end": 8023}] + }] + }], + "doLog": true + }] + } + } + }"#; + + #[test] + fn empty_targeting_key_is_not_dropped() { + clear_config(); + let config = + CString::new(EMPTY_TARGETING_KEY_CONFIG).expect("test fixture is valid cstring"); + assert!(ddog_ffe_load_config(config.as_ptr())); + + let flag_key = + CString::new("empty.targeting.shard.flag").expect("test flag key is valid cstring"); + let targeting_key = CString::new("").expect("empty string is a valid cstring"); + let result = ddog_ffe_evaluate( + flag_key.as_ptr(), + TYPE_STRING, + targeting_key.as_ptr(), + std::ptr::null(), + 0, + ); + + assert!(!result.is_null()); + unsafe { + assert_eq!((*result).reason, REASON_SPLIT); + assert_eq!((*result).error_code, ERROR_NONE); + assert_eq!((*result).do_log, true); + assert_eq!( + CStr::from_ptr(ddog_ffe_result_value(result)) + .to_str() + .unwrap(), + r#""empty-targeting-key""# + ); + ddog_ffe_free_result(result); + } + clear_config(); + } + #[test] fn configuration_state_is_thread_local() { clear_config(); diff --git a/tests/ext/ffe/native_bridge_evaluate.phpt b/tests/ext/ffe/native_bridge_evaluate.phpt index 29943e5f9a1..18cb8ec5775 100644 --- a/tests/ext/ffe/native_bridge_evaluate.phpt +++ b/tests/ext/ffe/native_bridge_evaluate.phpt @@ -42,6 +42,27 @@ $config = <<<'JSON' "doLog": true }] }, + "empty.targeting.shard.flag": { + "key": "empty.targeting.shard.flag", + "enabled": true, + "variationType": "STRING", + "variations": { + "empty-target": {"key": "empty-target", "value": "empty-targeting-key"} + }, + "allocations": [{ + "key": "alloc-empty-targeting-key", + "rules": [], + "splits": [{ + "variationKey": "empty-target", + "shards": [{ + "salt": "empty-targeting-key-regression", + "totalShards": 10000, + "ranges": [{"start": 8022, "end": 8023}] + }] + }], + "doLog": true + }] + }, "bad.flag": { "key": "bad.flag", "enabled": true, @@ -75,6 +96,7 @@ show('object_success_metadata', array( 'error_code' => $object['error_code'], 'do_log' => $object['do_log'], )); +show('empty_targeting_key', \DDTrace\ffe_evaluate('empty.targeting.shard.flag', 0, '', array())); show('missing', \DDTrace\ffe_evaluate('missing.flag', 0, 'user-1', array())); show('type_mismatch', \DDTrace\ffe_evaluate('string.flag', 3, 'user-1', array())); show('parse_error', \DDTrace\ffe_evaluate('bad.flag', 0, 'user-1', array())); @@ -87,6 +109,7 @@ has_config_after=true success={"value_json":"\"blue\"","variant":"blue","allocation_key":"alloc-string","reason":0,"error_code":0,"do_log":true} object_success_value={"enabled":true,"threshold":2} object_success_metadata={"variant":"json-a","allocation_key":"alloc-json","reason":0,"error_code":0,"do_log":true} +empty_targeting_key={"value_json":"\"empty-targeting-key\"","variant":"empty-target","allocation_key":"alloc-empty-targeting-key","reason":3,"error_code":0,"do_log":true} missing={"value_json":"null","variant":null,"allocation_key":null,"reason":1,"error_code":3,"do_log":false} type_mismatch={"value_json":"null","variant":null,"allocation_key":null,"reason":5,"error_code":1,"do_log":false} parse_error={"value_json":"null","variant":null,"allocation_key":null,"reason":5,"error_code":2,"do_log":false} From 75c5c1060b6bbc1ce1682a3903d029ce9a49726a Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Sat, 23 May 2026 19:26:07 -0400 Subject: [PATCH 12/14] docs(ffe): add PR-stack and system diagrams for #3906 (M1 Evaluations) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings the PHP FFE diagram convention to the M1 PR. Each subsequent PR in the stack (#3909, #3910, #3911) already carried its own stack + system diagram; #3906 was missing them. Mirrors the format used by the rest of the stack: - `stack-pr3906.mmd` — the 4-PR stack with #3906 badged as current and the downstream layers shown as "future". - `system-pr3906.mmd` — the target end-to-end architecture with M1's scope (UserCode, OpenFeature Client, DataDogProvider, DDTrace FeatureFlags Client, NativeEvaluator, Remote Config client) highlighted, and everything from the Hook layer onward dashed. All conventions match the other branches: quoted YAML titles (to keep `#PR-number` out of the YAML comment parser), `flowchart TD` orientation, rendered with `-w 2400 -H 2400 --scale 3 -b white`. --- docs/php-ffe-stack/README.md | 62 +++++++++++++++++++++++++++ docs/php-ffe-stack/stack-pr3906.mmd | 16 +++++++ docs/php-ffe-stack/stack-pr3906.png | Bin 0 -> 245127 bytes docs/php-ffe-stack/system-pr3906.mmd | 49 +++++++++++++++++++++ docs/php-ffe-stack/system-pr3906.png | Bin 0 -> 500812 bytes 5 files changed, 127 insertions(+) create mode 100644 docs/php-ffe-stack/README.md create mode 100644 docs/php-ffe-stack/stack-pr3906.mmd create mode 100644 docs/php-ffe-stack/stack-pr3906.png create mode 100644 docs/php-ffe-stack/system-pr3906.mmd create mode 100644 docs/php-ffe-stack/system-pr3906.png diff --git a/docs/php-ffe-stack/README.md b/docs/php-ffe-stack/README.md new file mode 100644 index 00000000000..6ecd4efd038 --- /dev/null +++ b/docs/php-ffe-stack/README.md @@ -0,0 +1,62 @@ +# PHP FFE PR-stack diagrams + +This directory holds the per-PR Mermaid diagrams attached to the dd-trace-php +PHP Feature Flag Evaluation (FFE) PR stack: + +| PR | Layer | Diagrams | +| --- | --- | --- | +| #3906 | M1 — Evaluations (this PR's local copy) — libdatadog runtime + RC + PHP 7/8 APIs | `stack-pr3906.mmd` + `system-pr3906.mmd` | +| #3909 | Hook seam | `stack-pr3909.mmd` + `system-pr3909.mmd` | +| #3910 | EVP exposures (sibling of metrics under #3909) | `stack-pr3910.mmd` + `system-pr3910.mmd` | +| #3911 | OTLP evaluation metrics (sibling of EVP under #3909) | `stack-pr3911.mmd` + `system-pr3911.mmd` | + +Each PR carries two diagrams: + +- **`stack-prN.mmd`** — the 4-PR stack with the current PR badged. Shows + where this PR sits relative to siblings and ancestors. +- **`system-prN.mmd`** — the target end-to-end architecture (PHP → Hook + → Writers → Sidecar FFI → libdatadog sidecar → Agent/OTLP intake) + with the current PR's scope highlighted and "(future)" nodes dashed. + +## Regenerating the PNGs + +The `.mmd` files are the source of truth. PNGs are committed for PR +review and rendered on GitHub. To regenerate after editing: + +```sh +cd /path/to/dd-trace-php +for kind in stack system; do + for pr in 3906; do + npx --yes @mermaid-js/mermaid-cli@latest \ + -i "docs/php-ffe-stack/${kind}-pr${pr}.mmd" \ + -o "docs/php-ffe-stack/${kind}-pr${pr}.png" \ + -w 2400 -H 2400 --scale 3 -b white + done +done +``` + +`-w 2400 -H 2400 --scale 3 -b white` yields crisp, white-background +PNGs (~1800×2000 for stack, ~3000×4500 for system) that read well on +PR pages and survive zooming. The first `npx` invocation downloads +a headless Chromium (~150 MB, ~60 s); subsequent runs are fast. + +All diagrams use `flowchart TD` (top-to-bottom). For tall system +diagrams that keeps the PHP-process → host-sidecar → backend lanes +stacked vertically, which is easier to follow than the previous +left-to-right rendering. + +`title:` values **must be quoted** in the YAML frontmatter because they +contain `#` (the PR number). Unquoted, Mermaid's YAML parser truncates +the title at the first `#`, leaving the rendered diagram with a header +like "PHP FFE 4-PR stack — current =" and no way to tell which PR it +belongs to. + +## Architecture rule encoded in these diagrams + +The system diagrams all route the EVP-exposures and OTLP-metrics paths +through the libdatadog sidecar process (`ddog_sidecar_send_ffe_exposures` +and `ddog_sidecar_send_ffe_metrics` FFIs). This honors the dd-trace-php +architectural rule that the tracer extension performs no I/O outside the +sidecar — same pattern as DogStatsD, trace stats, telemetry self-metrics, +and traces themselves. See `DataDog/dd-trace-php#3910` review thread for +the rule and `DataDog/libdatadog#2026` for the sidecar-side support. diff --git a/docs/php-ffe-stack/stack-pr3906.mmd b/docs/php-ffe-stack/stack-pr3906.mmd new file mode 100644 index 00000000000..1f714f672fc --- /dev/null +++ b/docs/php-ffe-stack/stack-pr3906.mmd @@ -0,0 +1,16 @@ +--- +title: "PHP FFE 4-PR stack — current = #3906 (M1 Evaluations)" +--- +flowchart TD + pr3906["PR #3906 — M1 Evaluations (this PR)
libdatadog FFE + Remote Config + PHP 7/8 APIs"]:::current + pr3909["PR #3909 — Hook
EvaluationCompleted envelope + Hook interface +
Client.evaluate callback wiring"]:::future + pr3910["PR #3910 — EVP exposures
ExposureHook + SidecarExposureTransport
→ libdatadog sidecar → Agent EVP proxy"]:::future + pr3911["PR #3911 — Metrics
EvaluationMetricHook + OpenFeature EvalMetricsHook
+ SidecarOtlpMetricsTransport
→ ddog_sidecar_send_ffe_metrics
→ libdatadog sidecar → OTLP HTTP intake"]:::future + + pr3906 --> pr3909 + pr3909 --> pr3910 + pr3909 --> pr3911 + + classDef done fill:#bbf7d0,stroke:#16a34a,color:#052e16 + classDef current fill:#ddd6fe,stroke:#6d28d9,color:#1e1b4b,stroke-width:3px + classDef future fill:#e5e7eb,stroke:#6b7280,color:#374151,stroke-dasharray: 5 5 diff --git a/docs/php-ffe-stack/stack-pr3906.png b/docs/php-ffe-stack/stack-pr3906.png new file mode 100644 index 0000000000000000000000000000000000000000..3987091bd589c15dddef9d6dd50e06260225f7d7 GIT binary patch literal 245127 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

- z`0etsH280Nf_EEWHB$Y&0T;G(bZ2uEYpP8-coqx(^mZyCLj$>83UK3c=JHJ);c{{> z(*UvggQSWLsAeOD>(|P3J{414`~z4%|GNtmfW*ilU^F#j2wX5o&yetTy7Jx8RI+_F z6BSyuNBmx9^;P)O6h3$#K|br+vFqATw)Kkksw%jXI5{yaZur0MzFk-35|8Nu+Q8yV zpuZ2MD`rm^v3q%W!}T87$$I(i(f7=Bt@y#+_r^N>rn?0-A~M}JVL0XAlUWlQj+3HDb_O`7p<190|)V6_c-^xgq~|lS*YBOWOjUGARL^Z zK}BTsC`KZ-HC>wsqz9#?O%bmJQmXhM4T7orCWFW_Qp!^oH_2)C#g7nqH9a1ib z&ab&OeQY0^?pSJsWR3nV;4OBj%=LSbZ{wi*OA>SiYl)AwW5>rYFI{*frKL43=*GIc zwDW<>Moe6qk%nQGJ-J{{Nd3Zvn-mVl*RabN(rRU7%KrdPJCiW>?dE3*z6G=O!dlq( z`HN!w-}9O22|BS8PS5o$j@m2Gt&wotKzAw_3a==q0g|$JNaFlIi>x|D9?-P8<^QV% zj4DC^0E%Nk*VE%rFS*Noj>!rB9y++?juLU~MAzaD=s{QSs5fosdVwi&&ifIOOg!z)@3U z#Np^Ne=V-uxDydJWNT}Sfo3J*%gXp08fayi6R_iQ=;B`;40RUj;K@NnMGO|ASarwM$*!Kjh9 z@WJ7wrsE60R5$dmeTb8g6TwE`hlnk5qjRdm6|$GK>1njIX@g#%AjQ2e=Hz2wXpO4H z$!T4AX!jRWx=Do{;RF6dn>6LO#CPpHDt*kiZot-SG0sp|cjcWPi`i%+pf)oiRhwKN zmzt$6RJnlvd99(I03ZKeNN|@Iw9|kwxjQl?TaWa>l;iO8^BC~ZIqF)QAQ12ty@SN+ zbc)SRAsQ2$5le{S1z$U~sKHhXJqZy(_=a>QIG<`wPL4}Ju>LH0X=zDw8h3rvtG$UK zHUj3Xo`D{=XU{$q6z{B!xJIkKY3`E7F>j~AJal!3;o$SrD;jYZFFa@Jcp4@G9mxXW z^}?fWkA3EfnZP4X2!a^bwwV_xOC;*|;6M*-BfmMG=_#9x|ELkuLJgKUk4+Yn_OA}w zFEkfEYs!q`M8+*%ywVoR-Bnw15E3*;-yj`5RuQ8@@|kbB{r6{nh)wzx7Q(9~y6mG= z!7_v(r-Yac3fN9m;zE{gstWJ4-8h*laSqRjhp!wyn5;lVWzFXsDmj zlp)+u3sDDMU8}>HO;FdRw0X=9H8)BdsIHF#z7epft!X*JC2J!wVe)pT?^?1aI^USp z)K}?$Cog3q91>-x)yXc3fObN!DpS4UYtC$-VEd!8S>_obN#Usk5DM*-T-L7S^^(T# z_uDJt*9D@PTj098Rc8_O_9i!*l%4bEh&z6`^`5?VJs|%3d%<9n$d!5Qu)lQV^;$3e z2!L#n_&@wOG;E{C%)y9%T0<4u2R<9@8xMs944~9N#XJIS6^B@Xq&yVZ`5s#Vo2LTO zl|t-x<5NE;aa&bFr-6ocs1eRC9mV<-1ctPIm56bhvp5uI6~||*AI|(mV9UOWvA=Qf zT!H&AweV#9avTR);<(*PZGucbxN166>Z1)_r$K~GB1Euwou8bmJ^Y2wHk?*lOFa<< zFO`()!aFMGkJArNLJVPt$n+0@Aa<5q&udU%>UkmU^&M~z}hY(5~l~7Sk@~JnB|;Z)@u4l z9iOt9>2`Kl9LiDPwTH9&6)@V>uN}ppOpAz42Ig0s4GqVSNTnimc4+9b_oqf;);(hE z{Dgc8*kT-c5Xav^2um$mco%EReHaI{$O+q%SC~9Rm(;LyH~-dqJ_XzRZunL#ElnB0 zf)VdA*T%EVKHQbgPw(3FCd7#6sm(bkcXw>gvWLDq4-KC$lLz-xj!j@YJu8yi+V42J zPaqX3Mg$9iBk&lvxwv@3b_n#yPb}{%t{tVF>2}rHwiNLtZhud`bFQ5$Vb%n?B@F#? zzq@C^L&QP(twKDZ0DSA_hZAm<9VA!3|Ba_KeV5_hd@_jnwHR{OiD}&yHGSF=_2$pi=I z?%ob%=j=P9Kk%{w)z*rSyJ<yP5M;BZ(s_v|z);qmq7nT-^)xq%{-l zCp&vC+5O_}w9#M}rC5Gx<&xqWQIRQ+L<+)53fd+TRvP0lmjwK{{fH z2Pgg=rO{C)Aj4wV-^70jtQ3X~7P}2ZCZ+B}YN?!lNi8Qw!&RCS` z$_VS}0i1AaYhrB9Gdd5IOlGvM&>Km5J)=CQ3Mo)HzY44EO$~B|fRD-MO}M*zTqQq( zkySRni#(q@HpVp|LCwT5?mJ<9PI5!*UsLMFdJx_L9`w+9pvOOeP1I>O+wKyW2tvgx zFMXZ^_*wDFU`r_$k{A%1jjhdotxQnW%xUQx6VDb!QO76gZ-t9gtM|S!!zxZv>{&a5 z_d|w!BxvX0nSszdTO}^OADl^dzts9rHz$-Djtt2@;u|bKEHO01swR<7ZjU;gSOyrO zQ71af&w_?;1Pm==6<=dW$e*@L+wm0ueF}1%Ln zd-3uvi+dPaeG#r-lRbhU3Ab z!|fF!>NvHQB3`lVfs0G8*z+bKFLLYSCAB;M zF!=T3p#8C(1BI*QoGruQSCUu}U;g@34O(U4j|mZ8XJUXD;c3lT4jsu~tn{2H8tqom z+)v4{c+59t|KMWjb)Ws?x!m~uTfKp+59a!)#ka9lSa|N5EhnW6w2syR2VLWtJz?3% zpR6U%QOMdKQ02kq!!|s)>`TpTeXv2UCf)O2WhLT%oW)r zuyZZq_?66spJv^9tr`@4`QyA-NGMkBd`Eb=Ur<7NLYw7ZYwE_ix;QugG2(LZb=BsQ zfp3;B-|lp((Agp;Soz-9%51?b4=HvwW_@aVtChx|U}5}rYtc481)FqArsw(G>yy#) zW3Q_9cRpJQe0#))dgtDwdjKsRcRLqF8ZJq)FVm<-Ty2Ld z5ETNFnG#nZUK*5LUgBBMyWK+YOwa6mJF#s2W?lX4YBScnbc%rG)cg=wRPF9=-R~e&k9x%4hKFR}2XtlG^AF&4p35vt(9%S_ z95cdpg)Hjvz{JsY9EYp)rV~ito|k9$KAz{(GCjXkPeHqTtz$s9g)Z7Dd|O>wT1-nZ zmQu>e4~Ro{P(o!roytPM^(wb`m3yP_tfHYx{UWlDGbn1zzKpln^5W;G|3L&er%^nW zV`F~y=mwyxneaP4(50u%pf!$8m%(_aC6b$n$Ql2_)pMTcxV*^}gj(l&iTl911t5cw zkJU^o4@X-V=_fsh6Sx)u#J_NL$cy!G$mV+(mObLuQS}x^WNolxjYg>^lb+lGhI1kp ztDWx|85gft+CNp?b(zI2G7~4HRQjP;5cS|p4+$j0pAN6#tQwyEFwyU8rjY+`HG7cldaW4Ea0BkgYHdVSq;NhpUvv6mgAca<3X3CpV{cql#~AV03X8QA7rRfp(=!y;iiVa#YQ^pYBp@DN zt{X2eHt}$Vvc2?+l_&OG0_|P;OUg8-*Za>u8qnUqr!ST zwe-CK3M}2`aKDbSY|MFdjRtDn)X0du4d`B{Kn@?Q$z7bUKQhgnTKS;Q^j^wX+@K!% z6wx;^Zp0R|{lmfOe7UrASA#kn$oS4_;e*x&U(0n}{0s))p>{Vo-1#_KoUY_De}f1h zluZX!{i4Bb$YCi2D*>0Ae??|tM#Poya40|>WWX>Eu2;t{{iUu}{w_HztUR^vPldVH zn168GhL8dJ`c-#=hmB3EtcaDw>j^qZ?w(Y6ia{o5(cCYWVH4rsFnNwfeXRC38nsij8~lrne=oT3UrH@22&^lEVa3vPHOqYkQ1KJnj$#e#@P zuJ*H6<)eBkDE@+aII+L^tCpBYRfxyTmB^O6Wan`WjpQf_X1NmF6FA7-fd0!!!YP(y zNnbgt&m<#RQw`R<>Cc$-ZgAQaA$-p+JQ)GlP@dvqQ$+lfuEBnHf@KY(3a5_VgrhNq zSJibuN(>H_&TlYQtpl(b&|ClDA1^3>&&vATQ>{nsoA~8YZuVyQrHcp}P$N@u8*)=V zG&S2gjG4i#QkGw3DuC^!TZ#Lx8#gp6Ktzi;o|8;4_j8Sdi?=k`7acLB(UM^L6`3F? zdtN5s^Vq80TT}6Y#nbB{;?x1bi>?dyk*Wp-w~vQR!o#u&@HPO~obiNt%f zGFvz|;g2)8#pgbO7L3c~6@wTzvRuAGHB0c=JWO7@wAc@?3;f@B)jK&|DM`}1_E@arRyGzo3Nb75 zP7lvhTCwECZ|()jLzm!6u(L&q#f_?s$^H>G2nOmF0>$i*C#G_D%e`{SVsCRi+~a(W z6b&ZnNSh}r)cM+%d<<)ux{`u=@ilzd3seMl2eM}MRY*_)}C03%h2 z50WA{=Xnp_ydgsGgG{1pL(zauowdB>rJH-HhWuFT_uVgFf=TZJW?dRWm3IYq*Gxv-jO z6wtdx%=2nL)l@$~qA19l@{!BcHc97RYx}|zNMFR-nzW-z@A0W^K6>odo7vM9Y1;L5O*2TW9u49 zX|l1fkSui^5GP{O|Ai&%KZdViJMXLikEySWs&b9C7EtLHkOmPEDJ7MZZjkQI4bt6G zf^?^By1ToP?(XjHj_>iDd+z=G;TY-=HgBwFtvTnKlkWAM;`{i{UE^@YkDpdGM-!8u zj?zES(NPu}8qVIcx#j{nrN|uMsHh4J4a_?szXGMzPumFZ)2F2b>G%I zfr6$cj0+jvo%O|H5q&>na7JG*P22?4-3^&vjACD4wPlo&e2J2oSwWo6NPx9%$sO=D z3eJrFR)cMKPvgjeA=--XupoqPy z05-;yy`Rxiz5nA*=(ZN;d>a#D;ssyHzm=I!iN=l-iE*a2KvD$&e z7GwL)CY0aykqQ^5AWROWgr1lv=|Dw{-^5;y*bH#gcQ^LhKzowL>e$jOo>VYrwaPQ=XDdKtbdFR~zs{YGpZGjxY5az_>R8Zs_8k!=)fw?(Dp>+Oj$hhlUG|p~4HwQVljt zM0uO#^U?G`3Ivtn#0Co*!P*q}L!|jVN9SiJAArW!ViLp0)jhn-zi`)b5h zeq(%eEG#h0c0j-eK*yNeJL}Q8=BwR99#^mT{9ca1N-k=qhBNJrc=eB8yMxvER@Txm zB;sKWqZ^YzcA2xRr|r~`AK;!x*+K3q?tn`|(`}d3Z!YK6Abr2ey~$#yD~|&vCY-IA zlX&?udr8c$Kq}20;4*$I4p(_euQi>%lgvHhpoC zBqF4sFgSC#qADe)@<`{Ot4Kvhk)O6Y!pVt>N>}U)FWuo|)eaAj^8N!g6WhV?S{yrf z&q^2`y)OtC2|SjQRqchwB{CaOdQED?Bx-%@hUjEbu(7i04sk_CDy0-HPHc`2kL|T; zN=iv8nenN906O%+=yK5b{PoluO?vNE%xwjRQlsXJK`kWwS0Bl)_YJcC0MF0L?DX-w z!)cqivD1G6TOfz{&y^lEKk5VOS78)V*VXeEBOkIIVe2JeoS6A$5*lwmQpc+tq9vnq zIr}RVX9+VdRMXKbwxlGNp6hwcu9w1xLb=^fX}A8`&EAYvCs*)Gndy)#qGW^#6Q8xc zDdlFKoSO2Gum4&mMmu1ZD5-aLb~cl*7&GvS>g4j&aV>hw_>|*)i=W&4C|A{EVCeJS z{BJPCs30#C{QBwu2#J=y1t zw;dyA@68JsDZ*twwQez#>s|F=3yC)Bxx4FtMa_DU4lig%@FzkzlM{I+y?iGz0b3pw z@i^K}%RU~qcUJbhP(j??NxwCREBE+hx`CGq?=I`?PkMolf7Ix_ zu~emE=Iwm^Sh_6`VGO~?h)Lm^DG%UixKry9|>S*rqy4K zg)Xi$gGAl1C!-_!mbBj4&p$}5w~JEc5HX(%ypZYWK-)LhFr8qTWSP{M)7UGYJUnc} z#Zwb_g^8I_U%%#JFn=H;*CCyM8X4t-*e}*e9+lt_x|UM2d&|bLzaD8s%d( zXrn7=9W(angl>rza`@ZZetkVb6crWC{k`OO^ida_i_P?LJxw_&xv8uM_XxWvTMT!y z!fE_V_!4@UdT>TrTlpI%AUOV121 z?`ciZsac*ci;GIu_u9#s$z>O0AmoS7T3=s(*=;#tiHJ>>PKt`v+h(%+rEKtPcrA#P zQC2K3CA^oM7ulUutV689v@+b%?BM;-mg2T1S3`|(yI#-wzJ;-oso?HUMLRBUrR=rs zxX-&8h(-rWvKki?<@{=gZ1%QWR(5_dB0>+<$G4w(EVb$|Hs+N*qN1aXqisOXr$H9N zyPHD>bF{xBWhN?N?oWtmY^S<1n)0+33%e;TJ#_AI?=_p2eO1X*(8S$UdpJ9A=QVai zSWP;tT2XP07Lk>kO3%<`Whyl_NK88*nG2%PiSt|dl$WUg?+T5!W#Jb}s~8$i7I6uG zc)sZQc&+kmU8SPa)2%L&JqOsRIPbz1mX-roZkxT$ms(pLhD zUA0JV0V1Njo|A3Gdfx%+o1da>BK|2EbGPyfc0kxw8%O-^j-~CVz@Pw0iPidvqMJo= zF{~$6oHbRmxB3S9x0kz}XPNX#^EZv+qT%VsIT_EFgUVUcD4JGBV z8p8e)-#Kd1TvlOYw`u-;Z?|JXB#9nHzPEX+NO;*j3;b(?6$&>jc ziEBzk>Jj*%w0d# zHd-BMakTLWVsm@(bN6eOtX}T~X6$T_k4So#!)_k9pi4vR+lrwUygDP{kz2x2aeug4 z>H-27S(3e$H~E#HUtwt{n$I>}-45K13#oc_3f0?izG1v|%L!Bav0^&>rV!hR;S@z> zQ0vlniEr2Q^_0mPZ=+)COGr{e-iPMu*945Yk*(x9oOcM>Sw^%sZd_&-paooqj>R#l za=CBHny9jyK}#${o{vrc5GF7+&=H&aI&XmVcqb5ocD)j=)d`=Uede9}3)AlO^nznO zad&kBYR{}u=WIIEiY+MEkPeo0`hh7(=eV`-&X2&MGn<*2EM$f z3W?7>uw`0`7*nLtO_0CG2KE1>@Ao2Txnkue`}e)iSpgZ{DEoVED*+6Zm|i1@V;(z; zndy=I$N)z@4LB_**iTvh_S7{LP~$Ag$Ntgj-cn{yz4YWiEPxvrPAj%*)zTNL^7&KH z(SeM+-%(KnxF1YhwPs%CiO*{lp`^01cbWy8b}6lWLxY# zLzhW#e{)Hp&Fu!y8A_2Z)hUQiVqBJvpM{{&<*>Zh{@Lbb7TJkv zve{A15n`+xDTuXJFGSL7jgN1i4+1qD;bdO`L!%U3HVmJHfu)HuJ*D`7h zhYgNF^Mz82*^Eym?-j*$bVlljO8}}pvK#iJ+n5A~>+12N!$|K*y5H=qa;uT3s2JaD zq9|gy`a$SvShHjlJqmr6&}AXk@5-no{O%aXK8AL|Lqh4Qp4c6Ny$7t`d?cIu@Q3m`%#+ zx}*71t=U6b1_L&Gi~PxtTq;9M<<`pCz5oi>Y_c-7eA29%RAiqpkd2fZ4S{}^-b+TK z@nlBGDv~-PNh)5rR}H@(bbA8%h@yIa1RpPyDcJeuJs z(ruWHjyHAja$QyS)*w7}UX*BZv`bpXQn7!%U&0S%Uy|^Zlz=z>xmSugYw} z&bx&7zZF;aqpL{s`nY^|xA6gnatr_VTT48xHnB>$ta{ko$rfD>gkYUNVRhQymbcRblz5S*rv`6LI<+w6c&@5eEYN0nt^z$ z@AD0b@>FiMwY3Epa$>3p37sKCv&pywZNfvpo1^4I{UT?`7P>lW6!*6OQ-NLbk%+JCZe3K=ki=mWgYmrfEAeM*J<^df zNhdZ+!j^61*`IfL4h2a+MsZ)bJ=28?ed7_Et?_|{-k7qx+X1<7I>~0qnf}&#UamX0 z(s*Au%q^@GquQf{FZsHmBbMH%EybJ7ON zbGC}J#LdNCUR2EKep^XG^Sl_$YH#n+VU!axeeogh;qIKk_mWNe@zh{R$?56FLB|!j zh3j)soR%hG>SK!Hee0Cm+;5d?b;pCDPx^B#Bq5K{EQ}vl8Qsmrp0e^yS6Cu9WPBI( zNw1m{-SOcKS@*=B@Dx=UMcMC1ld8O$Qi&e;OF#Xhk^Y_+;Ly;0hCfgMStC9;5J7Dv zE!um$j!vlmCH3_cvXfTxVnhCL8|ifO6YJFQ50!KSzBi)vGS%UP>?K#jyl#$7&mnlt z$v5XtWQBhgtDL|S3ke4L6ZcRkVu-G&ow*d@TR(&KVHnP?P%r(+h1U3()}6hb(Zdyk zwGiDKv-vvgDOEF0)oEru#^w<9@oFQyR15s!YQNg}PlcGeiabx*74KtQXo}VADak*@ z&%$=C_2+usqIGq=`$h%AZf*u3hPoY(bS3Br)2PN&M7!^Gba{AMia1;?c@FKTb#m&9 zmP>a!%ulZ|HKFGBnj1GvE66N5{Q(Eq_^2RlcWf%p;>g>r|;+Ctc zH>f%a=a+~5XLOb@FJ;}qSF*oPo5m5}q#j?nV&|BxwwOwoEi+vz<+BQ{xLo_eT;*MF zdNoLLpiU^077U^N_qvL3zAh_krb}RPAIBJ3TAgV+sA_)vZb-dpE=Te6Jel3<)$@*f z!;haBuU!ThNA_-cFVC-L4GPrc5|KbJ*^T<7+BhC%`pg+hCh()*I0rp6$Esu4o%0Og zg91)8PV-{}WWie1#ZjO63b=xb1#S&3<9C+{S}ja)>wjf9RF8YRd`_Yg2vEV0pc$$l2++xXL~eROVPX`V1rA> z%EC%^?^Nf3@AOM69`@1!6vow{i4J%7?YVrK-+Rv)d|WEWX;kpz?{d*ZRw4}VzHU= zm=^pI(WUK{CwBZntHLV%=QrulFtTJ$hxn8u1jP?u|9MOiexW{3adWn}vM+14Sz0<; zGP=JFZ(WKfIW*(2GgbMLqS+tJRaI4%uGV6x43d|>2u-1*p=tlbmhwY1`>Mu#xLW;FimRY0(+bTnW1M}Y zjicr7cKVs86gqgYpsLkq1&o-P+&8$%#`Z^M#>)9Q3^I$YnC^*cIxRtZoN1GT)8iXi zuApd#YN>IDlRL$<^jQ{l-hdISl=AI6o|_{y3fTugjQe6ZUWAFs$s~AWB&3y|PKChr z4b%H8&fBWqJ{s9EtDIG5c^Mf@O#H|gDDZTSj7)_0u*lDOpPTo7&)76Ro&b5fMvMuv z`Nl-Ta&?&%0babXS5|g(d8x8spUD3pDjLVGf|)#YP3fC#4lswVx$d|Y=)Qg%rSK{` zI?A?!MJIlp16J)V^%oXaZgMKB(8^J5o7ecefh3m-@`_m*8NywiZCF5I51f(CZS5Q< z-%5%~0;gfx_p}usnLI-=O8Oh_y^OD=e)U+RsGy?SN!qcXqOvHK&^$9e2U^&u1}|4S zRMae})Pn*QM!}(J<)FJ-wcoa`W zz5JcCt^65h9S^te^Q_HK%S$y@3JMyoH5-0oM*Y_a7Hw zQBhdL*vVSoQi`(ja&wDPi;D8{Xm4*y9$P-e{Z5EUjMY+?MY0-LVzP_r|>fUr4q|7VOHt3%QL0AMGpk3%8i*O zGs&l9I5*N|kDumCZpw<9g9%Y&SPONw7$;AOz{8`xji;0TDZxm1sM2Rqk?pC)Pic6ee>OGcFBI%28|J53=3icoqZTjUEUb9CHf_ zY>!eS$78??j4~pm>UvN6gQehucrsKYL5v{qe@<0|R0}~A$bQxVyb7gfXDCVkICicO z9o%n(e~$_TL~pl$k%8gR?Urzxf>LkL)Ik)sRzm;93$XrFGrS7^ z?@RwZwRvFo2?Lw>fjSoqY}jt@HgXhR|M&0TxZ0U<-TZ9rDiT*7e*RlO|NgI>@K&)G z|My1(@^Oq3i~l*Va%4=`R^wuztMQz@Yy9P|V~UIw`bE zl%EAZsvD4y=;`PTqzMQKUT^P}Lc2P^D@R$bRaRXtBP%Z=DtY&s1c#WA6rWg4S-v1I zEh8`OyMfMPnQtYuh&2~cSw#CAP}N#1+Dq~BrlzHNB?Oc~p=h)qAzeaM^*U8UQSotA z;TxZ>wY9a0wQAjS@>>ZFE)8NiY(L2I--7LV|v%-as6uDnNy3)?J+Mepm$pbj@`M!wzNCzQmCQ|s)+|xh)7^Y|Y zcA$Pb@_6h~wK0mF-%;SLov({i-N0|gLp_1@&4at# zI^12Yn%dS$`p*EGq=L#i3l`RH9R|${E{~gcLrNA)E+Tmz7yjfA;a{Sf?yY-lcpaJKR{!p zRZ#-jTquk@I)LBDMQE6F(6(08PUG{LAGP=W*6v@Fgo$Em76Q-w^7yt4n`wZ!3 z&4ud$IT;nF>**O&wj)GMc`5V5OD?}7F4qRfM-&4}!P)%i=xB^QZr6MD7Oa2(@x%Em zzpmVN4t>HBiuH0lPr|DG%^O)Z26O|1kR?$6s!i7HRa5#8ddiiGy%&Q?R>mg!_;=N8 zcVG1>>UnE5S|2FJsT_|kE4Aw__V<7TGg7suyI-^1ilg!c<1Nr01%3*6p)ykoFheb< zkpcEU(#;{Uy4sq8IHr3h}IGfHG~yN`$_oTeKoJ&7$^ zBAIWdva55{OIH?BSSG6JvlY|J2Wr8Srn=vjT1|Yb`7D+ zld)&b!b8TvS)D(tSQUTX_Jf3FfBzhZRMOnyPO7&WwN^%P(pvT)kK+%E7D-P^pqw~A zoZ)wM-EF_n{o?iARRMx;0{YB%wRr(z;x4AdbipP5+YaQU+L`h3uS>PSYKq3w71|+X zMM*g;tG=SVyrI6Kudu*ao4`{S!@PRZT!pzHL6~wC0rK~HI*zLm``1oiel{jaGSM(m zQ|ESXPlpX#s`52H9p_F;p>ggVoWdcV0*ELl(OX_=rKGg#&HvWeJVFV*5r^%|Mn9P1 zC8W64-g(=Rh8<3=GIR4UCB;|1G)?<4kw)~DR#qQbnYAUGkdT^O4_GhgBD>J>G8gmn z%h#e1DOko8X6P_nxLnU&k6PC%{14V^`)rIP(yF3f#exukkgAsCccD9@xm=aNs@`?t z)@(d8sn)VEUkToRVq9A0Q$s5_?TB;ST=*5qW)s?E!1yw|R_SE=Cx z0Wbbvjfd2&%vfSL&`E%?zN{L+b7N`aQnPODLO;;a+!;Ncz&~;8RJZ63Xqeg(j`~ zm(0~v5U|$p@c#F}gniZqJp?ho;^kKIJ6Sa#GPjs_{wAYR`;g2(&K(4}1)%==`AuP) z(d=ev1J!f30x{hE(h4aePey!f~h==ZnRTk6;HphcHInz(Z4s*kuN#BQ*(tfxFB zhq~7_5i&Zt>TKVlN+}7ubhj+R8osBY;j!9etjzRCJ~TH|zaFqKH{`V5erD2Ff2EVb z>u;6sDXwHOQU@nZ%)=K#-qrYinh^f-B4&%obZKqp@_57|4h#ZY$J=2vhaF(~a#r|1-@=h_+8b^`DJ;B}C>#Jndija-aS4ila>$01Q-=O)NZbTw_RXB0OEf=nvWfLlSTOX z7X}oD5#;OGRR!W;8tWTT5Yj>n1n9aDJA(;E`$vQP5j%{kkM7E;$Zcq;sSPGDM8w2W z2X}V&5q1iH2=abKfg~kA$=QpGnXvEuiivqyK(u>swne|t-Q5r6bXV3xSWy41iUcM9 zybfnKI`PL-b?gUKTx(m~KJi8{5Cy%wb-v%cvUE^TP<;2s5&v58>x*Zf%vtJ(XU0EtQ*`|!|SXfwco#DBzh!S6r^ET8wl$9}Ov(P7SfI;n-KVjsV)a7^u1J|3iHSC;&w0X{=yez(? zAn|39m)Vj;ERF!_alpZe{mtoMaN6(dK+0(nx0)w^dWr%PYBvK?6Emv(HMI7Gz_p`} zPnYmq4uJn&kxZSE1IzQY&!ecIAlSjHB8oVwa|mz73O&oo@t6M$`*%?I`@u}G{>}zo zBCdyqBF)C+)Cvr-p8%va-qUdJ3N2SqRK(KJ|AHr252MbFEn zU&91FG4T)BuMX@r}2;0E@5^0^||jERrtPZ}S6wMgN( z9`^V`P%yS=QMY=Z$F;Yo)E5v||9|byg!Q=w%f1|r`zQ`BpwWyWhVO7;@DMaGkj3gk z-H~WyV9OvRw0tF>X<*N3pl>~{w4oHCAS+vzlz-N#&`hWDF_5Fw4exYiWbD~!oU!HN zQF25KxduNfE`1<)poARPNX7%zK!i;pvu8+E7)dKX{007a^VZ+9g$5sXqrGbo^S^!) z;TK|7DZ+`OtU7?6x!sGK5Zyg?=W*U}&!|I^iUwn^CA5;$rHqB_(Jq`5%U|Tpx3AhC z?PL`M1vNE8mio=jl7!JoyP-bDO&WwoyC`#4MAVqVe>DHQY&qng^bI6}P-*)m!5+Uz z`rpdI3C7(f_cQKFgVAXn5BfXbo-e;2VuQ{Y zf-(-S{%rJ*LcIpq zXBt?OT&OTZul$o$Yj9Nr1-Am(11e?4@PYouw2#N`h}H36^Wn{)?|JGg)RJmbsr*>y zvm(-FL0&9}n^`lHz@m^UNhV|yaal3I!S>mpzE%+0am^cA_dkGgd;Br_^p=gy;wgMg zV7?$xZS?pXS*V96JUBpd6aQnPQ*|}T`kfi7LhbI$pPaLzgey0 zg`vCz(f__E-xmHhIes9*6;z4gvJrf_E-_JeZe@ue6hkRuaW`sq*2iRTDj#}QfchJ8 zysh<<{>|aEiB&l=K0RERgzOSQPQ&3m{dE_G&77;oW+~YP0!AS<2V#H!pe)u9QWj-Z z)dViY;=#Cj+qKOF&gRkl6o(3290P+(n|DSJGoT^;wHZB}nU(oeXU*lXGg{}Z@X6Iu zOLc9w+1|$^VDl%Pgw!ix>lYO))HYLKq=YIR(5 zp|PTp2mSKJClz(R(UrBW#pli0O(S)+2Bt>vCVkDJ&~}gHCKMmx&>|>_dEtO$5WY2v za$9;fj0-wR_81)bZwB22{ zzk6&Bmnq$kx%SBr?3)A(3LrfSVdU`Kkp*_+Rb~)HsjCmU_3f5@o!ISc*P1AsH9?K# zW#n+)Zb4PEFIOXL)F`n%_`S21s1o-nFCS@rW1|_xwfrHuy`uP&`MCD%;aeofzVlxc zI(M=>Czp*LjrDeKR@e58JuI)Z{iClf2TO|#4h#(S&2&vE_%v0ftw!J# zdkG|a38t{?b z)g;yYrd*FS^NYy)EH3_IQ{k|{91a<)i;m88`fPAdKdn!rXyHZm6J@bf?msNxWq&{~ z(xEcx=P%Qh-*xy+Ua(%ZG%ey4xeiQTk+9Fq_V9{}dz1OR4?>Bfq@@Z#0;OB1sl;T`7|eP`kUJT(H2i!FJ;U7{-+tv{LfZ>8=j|7dw>9 zI>=nIZ51udyEx5C{NM3I&A%(RXrqkCc=-gW!AtaZuW0jO!+sJi&JB zPuGIhy&i1jqGFc^BekB*&j2cW6e=*`@s$3}G_i_QKR5^MIB91=(RdLs%k+kHOxz|m z0=GU_RpjRsNJtNf0Ckz9WNdi23?S1L6h1M1^3o0;SN{uAo2Y7Px`U0q495>2dY$WL z)6nHXj2|eF(NHL2Xjy=@Br2*I9V(q9s^AMJ%-C-#D)Hgr(Q&_FwcxP0xXkD%DBvgs zU!2mdZ{Pr{8$}26h;NEA@`|#fBa`DJqi=&H{$8y6Qaobf*rde7#8~vmlJ_h)29t5$ zpOcT5y!w|+FflPqEHtLe|R>D5@odZ=%_7EIa zgshzG*z`e(ReT-0R0tiB+U#k*CMA7fOmRBy;Zm&=%l!}`yvR?|2J_Hv$U_N<14+Kp4?UPDjVzP#u-~p2o#GWY-e| zD=Dk)gZGHPP+c2<6j_LYvZ^rriz!w;i8x!D zbS!12(4%VHrf(_abMeBoj0>JrXTR#uiBOk}4IUdcTq)62cV90gJzGcf)A0}L%b zm;PXS4cc)xHt;(Cizwv$>KW`&Ii53dm!U<~FT=;5;nZ@*KmN&%1p(M)b2zJv_YDT_ zr@dI5`!r>OX-#oO^XWQ04=YjBpKIO~Y|SZQ%r5}+vcF}q=o@evlb83w-nZgdT4tTF zr+2WZFNGj2r=Xx=*Clk;I-T8HC}&4(sQb+}{9hdXpUR&@?O6i3;}JGqD@IC5O*J<( z={U=LYcD$d^p+|c^ZdZaSD3G^w|uVEZA8=H>?nW#RO{x<~ z@QRBozPl;Md$Vu@2%jzzIr4S4*AQjUyfVOgP2N|!tx6*&a~Cg-XswjzP||)?=M3rf zPsjk2rrH(Boc8TR=p zSfE~hGqllsOsIyldKawbAmrJ!cWC%4nrv(JQqz3Y(HbIp1ubjBL{qQU!^tf6sy?u@5Qnx-()+`OD167H|O z>zI<^j?uBPPnCA)blu*;9QNxN)m4Mzsk&#U$X<&+zdyUgPckq94P{$&A`mm3s&o59 zNYV$qSoxjFCohiW6b1qDJFv zWM^xxL7<(-{Ri!zeM%NGemBWb`y8B_juI{FrgNf;|)8qW* zv#2qU>xdUJQj|+(NY%{_nFAM}AFWW?==or-Y^cup;*9IV>GAw$ieXf8Jjb_73)Z zvE4_h6#VD7$DapTzTV^W5-6drTgSSi$damg%4K){Ef>6^ED23Y)rU{x_m)y zDlj(h8XvQ=uApFv4Ud1=G!xnG@&^Lb`36{EjYt9K2Ir>X_BR7OtE@~nI0k%e5={IH z0Pq9r;OnK4`FT!!dZH7;rX#B2{4_ue+jPYqHMpm2Z}@XvE?~Qb>UtI;uh95}A(aHr zx-^DYY%FeS2uaON6*4JmXdseZ{%}fpKK@O~!rTBY7$|(eOOT$fWB0DFr!P16J;=hE zott5MuG?rfpI%eelUa|r`wN76hTx!$jyR+il=z zvEKtx%Iusp{3g4m-n^X0`0-+%xl%l_S+or>=|s2>L-(|OWwnHn-4O5FQZ}frp z+x_|u&F8P6Nz{%%W&*Jl01t4szX|~Ie9=4gdv>5-sms}7fMfYAEr4`-nl``XnScS1 zk375uc84GE|GXo&95)(1w+Z*1VH z*E=17TE(e}e0QG=Xgl8CnQ}j+BN9z8AkC9HuT6TVEMutN)ejeQeKJc>lOkf0uvD0Y z6CUzWg14dNU@QJvaogQv1JKLt0mNK*e#MukQOD9F6(=>$wg>M&U#FPNv%0c{E-(!tyLdSMQ1Lyu>H||_&8BVIAN9R!z&u`GkiHAi#fY6#kS~; z`;Ej?>mYLP+w<*tnXkw2WQMEeU=`#0X^jr2+{N;@#RbFkv^AfsQqs0JA;0U6J&|8eYhWheq`yPF9^s0Xtbn|c=CaC_d41>tvQkGGnf3UU0#yC#p`qlaRD1D1+P$N}g&7j`#;@$mJy&Ba~ z5wwDP>R6NK$tZhJE3=X9yqV?YUUp+*z8ayCzZfOuA16F(Iz`O8FZxMr<~%9UN``vL zuJ1xg7Yl8KsUNjm_KLjsN!|62a47|StB?5DES8cZ6UCE)AKbv47DURW42UTkh?A3L zS)I$}s+}sw-94PJEmdgMd@19F=?{;pA};&QG$7cUCxtO$I=zut{gR_4E%ONDg{*sr zo*aYqoB~(Vzq(?g=htQ=pIAJL(bTcD}ITB?5 zhNek6xp`G{9N;n#x)KLso?{-bCJJ2o3U8W167Xj+p7=n9mlG;y!?W;;*pQf zw|Dx$I8I~BDgr|Wm%#00+xO($esX@lv1e9~hCn<>@@y4?{~s2RhTW}iG2hU9yp_9; zW)A3j!s-Lh?@?tmn1L zyvja}W;TbhnvI|hkqI9uHB+sA;|kU^i8$Oa>18lHmu=Yo5fDW7VE<&YvlD6blVGEbGd`z{7a_+L9n7YXeC2D(9Rls+F$PZMRO;! zH}ET#xtsDKX$Tf z9dJIc6sT9nZ}}pzUPCMe`~(FaPUbja$|aM|XfU@y%jx$hjW?96GxT(?@{e3Zr6!8f zQO5FgzG>?iNEUS2 z%*4rW-|X_29=8YEW8g^`=9A%oD+5Dhgb+GogQO?w+8G%m&07a)_>qwizjpTll21fqYhT73ACUiG17qUQsh-q_Yx- zmxpz~)$<=H$6X+-b9RnJT5{-3j_9XPjK(@+`@~zsGv)3o+y-skMM<;B^Z~tDxw)x% zX?%G$qB5JI00Ca4?th6~3XYYb@!((L7?~E-eQQQAT~ZqBn({U@HY_X(`1rYTH67B) zX~sLdAi9N^_jj1Vpq-SJ8{-iYlCgR3&zpOd$n?6b-Sw83)Bb1G zb2&YJN(0L7j2fF{y=?jliz(HpgrL+YjwN!#N{FG1s^aDF6ZL%^l&eAc0DvUMVm zZ(5alcbo~ zuaM9!DL5T;PL~y6!j)dTL`a<2Vo@3jeI7)};$yGnByUU9{P2aA6IPZA>iZd;ghRYA zsI^UhmzjwzQYoxBC+Ei>AxlN?7XWV5eCjIRZy)HGZYg(Db3DCtdu?aosewR3bQBf# zEuZ`R+LPFOp%5zXmuQ$Jl)DpOpt(hy$0H~elA4;TDWl0Nz~Mulkgb4gs6%Wk^b;sj z+klVZ?vT$$C<%Ai1(9MK(BVLm{?p;SQSa9#Ei9tHSd`X^Qs`j(%TuzqSsD&cPWE^A zuP+h;#2E$cMw_98kQ=q2-QMi?IKWF}$9Q~%hhuKmXY!{#5y#tJiS|H3p-mZd>o)8= zqez3S$#2qgjZn4}F9n#7?WaR_cH&DBe{rTSa`{PppDT&DBlYi+hO06C(;{VRs`tVu z`XANM`&k~glGEb(8m9hi22B53{^cKK)s^9~MlG8^6vm4k92E7Bj4k?d-1+X%q(Uw~ z{7xc{RxnX~hR5O_*ZJn(xhEl_sjku?si?Ye-a&qhQiT{@|nVHJtp8DU#tn{B~ zju-4Tf_8q2B8U*rge;-p`g~Js5RDq9)$I%m8QI@iTM=#9TV^3_#?|~jRyP5Z}7T>Md;w6&lsYDjUC7%le<7&fp z6wbZ=09dr5KW9e9Ir(Mpgi#J-!=lo1$fWbG!rbPj%4b2T>?l5C5f`=UsT<&c1Rpbb z#54I$W)h&SfUgGS@}Og9b${#^Ef|7LPSZ86H?}yUx6IQ~viZrj*l>2-GBQLgo<#I# zI7vdvx51}4k>Kwcj(4dc{%jhVzX z*Vpa(#R|x^HEZ@=KvTcV3Cx2N9>$(u>dT{WeMn-nDN|E3x}@6N*Z^w_8=J&D)1Ht_ z7pK#mC7W-}(DB|`AdT9h*T{~cC!Zj;JK}L|`w)C;H@F(XPc=A#dp)-PNrSyIyUORM zZ?S4~u(Q*CzptrABfGycdXj2zg59p^;(lK|MicrT%?RH*1Y7u*EF&xnh*&7XDYivxq9^un2ayS z;{%D(zw;ViH8($QFJ*HSqU4vAt^UTx&bIFK1;@gWmi2sq`|7#D?CIU*ew}p>=OKNC zSOC)E0{g*qgUQ6Hmb`ovq!B84FpDX8^c7|FXTy~93W$rZE*2@ITR}^;DG+&<5 z%k-i~6gN-N8wrg&$Ux>z$!?nRt{-&fr$?`mKbh+OWInZZGOx;7f-28429pMdR zL55sNo%4z6Fnl#6xv8TuO!~LReP{4_suZ_}=+W8rz95;M^>)hQxai?5tYr`pw-a<0 z_6zs^vx@h)uL%hX)M}?AM|UDz4@up^wSs-?Y)YyhY(O&Qph#Z3LAL5bwfoc&=E1eE zuZ3oFP<`DCB5%-p72cgqRnp*<=YQ}9_Ptkgr4%n^)_5E#)Fo%C_5OH+Y%$JdjVP=ha(e_Kw$P6fPj`7mt^3u=LC3p1yF}O%_XQy^LU|`S791c@3@d&-nE5)D6!G5ffaSKL95^`c25FFPkZzDJX^<2Yq@_Eg8wu&|?sC)8Av_nyuVofFqcfcV^cLyOx!8PO04k2scQEuD<~kRc+NjA1v2w1#qhd#&!?uWQ@dg3 zqgnuRZgYW^g9B@{vkrt4()b+ER1p?Fx_Y^QLf3pn;05y3XLFT{B0Cg&-pq6 zL9B%a8%QaHz1(}yg%zP6No;go-}OVeeSR+fg09cehl2S9va3tSp0%?9-m00YW&aUX z@7JD4VsTqg(AMJh!0N^JXu%_lE+(&_zoNQ#Kp+~eL#){X&QBRS(S9tqi+65L7WJ*; z1(xl9)7oHvBIYw3`Vf3p^y$H0xS88-xy9=DG~i~cAl@tLk3#FB30bAPpw)}%ju$pA z)_Zb$4*gLH{@vX^B&pE8sweY*@6@GLh9V>n^3-pXeaO4kJGE5 z#;Fhw&@o|;xLcpUJ$|oWov^DJEffgUj!BY{{&C_;U&Wa67F8q6n6NuY2tq4zW@`&@ zMQFp4c5}wgWYZA4yzGuV8=J=GaEIvnKWEJmuAySi)DL+L21mgwYW~S6zp)e;*j{>G zk$dp+s^B2V-M~kJ5oaGaRkvDK?C0~<66%j}^a4?wY@1%_$N_J(S4K)oQp@c*RttJz zw3ymXYM^C-DGc#_%4w7z#>MW$-2br!-MV_nerbHw*)`%4sDu%67PeYvl%0)#g*I(I zhcmR&DjHNyn4}g%_z@y@4aVyvg9LgCzyToActGguO^TTDh<=HIt+pGdX06W5#agtr zD_71d#teqIJqfNkwfb#an%dShQknLdpK;JpANbhhMs=cbJHhEl6^D{S2)wJHpQ_OE zCr~evlc5zD;F?lM@)F?BU8;j4>$jH6YCe63eA?RjT#cXZvlZ0eyIS9(o$w+g{qs3u zmI_i~VUL0~pOl-d)jkH}VB}hxtzKFIWo-)*l9kzY?o-Cn#CJdet2cW>Xy@LQ*!xqj zHx#7!E|}JUlvBMvP%6ILWo~CBmlhdOoX9afq-OKfDA?g%62yN%$+wD_wl?Hg5=yuY z{Fi``4$Q*wCX;oV1)JTY*)#|okk0nzd@k>^_wlXlr*r;^Ms+D0j=qs2fsqX? z#!e?aDEQp2ssdk1d)1SLPgOA%^YmX~n9205SvXrB?}*2TF6B9#`{l#whCmIex}Fhz zP)_yP>(eoqzprT6FCXue2i*QXd84d#eL*6%KG$Fpl}%g=K3dFA%C+r4p#f$S2`gA% z1%3V2%1RX(M`YU^FM^!}abh8f?$j1{L7soA3pAxK$0O0!d`!^4h!A>IA?6N?r38Yqx98|%G# zuCHh{JO9`kdz1_@?|pu4>lFY;ayqFkL@2)W2YtZRRR`V*m~$m{G*;(FjPB^YM0eq| z+KRdhfW2@A>244gDqF6XO?&u0Hle8B44KUj1$Jy~L@XTaQhUa8GNgH19DMvd@vCv3 zVY6zJgl_4e7rmn~X#YO(32G*$rEfkd7YDkB*McCuz{n&SlzNyEsAT1J_E!s_NnlQu zu%hMXPpqZYyFXV9?jK?KS|i@mE#(|ymqp9O5c;say)`i(?5c`K&n$E@d0m`^*ag~1 zM0c?mB>TJ!PeMkE4x1ae=#z_PyLn#AZ`A+P*T_%k zDKD-v(O`)IlefaCiu+4q9^bRB2B5crQ>K}En~&m4+dY6G!EZ2-b7Ny}yTkD;ZT1V;Hdj0vr z?-F)qIJdZNoAItC&&MS=4o98!m5NFn7uP3RJv&Rbh6&AJduP;!-D(crS`BXjDdCBi z!&wO-4{8fG3ypYy|K<{O13lNV$_`vge37ClXf;=Gov3*sX-0y9==;|6hn8``H0GPS zZIaEoJbB_DdIyPb+%$we4*NHgP>+P%|5>khb$OGm2FE{MZ+R*aWBfx@TSdv#Rs#N7 zIPrdc^LzHDtcnU0Bs~ZiweX04`G*DO@d=v9D?U`pjz;q+7oijsIo?g+C&n@wn(;PB z>M_Fm0Bq1-(1!Jt7V+DI!Vq$t)KL?`o zOG_=67UT4Ot*<~^<{$cIZmLL@7GB;*_*-1QRxQTgB>0f25T}zyL#mp5`5xLhye$m}{B%-YUpB$va+*V~#tb*C^68G~N-J+$}l@ z20=>M{xbnQp;Aw&L!$U-|CtgaQl@#~WU4V38ME`8WMF(Sh;Oca=yO^(a3C&u9e+tS zud&`6^u^Gi2EoC(mX4KR-=Oy@uc6Lu5+1|95moL%fIAjq%v3bCe(%}P;E`iTyl-1q zVsIj`dJarri^$s%8X_P#(rSE?+FfC``nJYt2w!{p=cXaIaqkmn_UN(M)#Gd6of@w& z!~?jKV+VVmm7~772=4m$AutOB_p9l%XApUHE}w6ZhbiV*4G=Uk+;LzoJu&zt z5=HoL9NfpAd=@%26fqx{@KMG}S|e0HoRdi~a5ACt1I5fZTdsAOo`SMWxPA^>O)X_6 z8NW~X^sig{Q|c%Zg$Uz4OeP{&Oj4m26oJG$T;?K*z4(Mt_grfn!*O}t4^tFR#vF+? zFb3s#``a2D9dH@GVh}3Qnxbej4)RXhj{8kn0w@^0mt;?O=L{kT4gwK?#qnlb#k#5*0B+n;sUZ1^kfrhAWR9=+Ttk*jZif## zDily8<;y$L32klOYOI^G4%yz-y$FwxXoA$`!04%5@%xOY%*QhkXt3e;v&)m`^YA_r;vN)56&)1OSEdGCvVHwI*uN@edad4O-(|55a zdKyx~__ngCw#3qCBJyJVT<21nWOQ7US~YaZR?8s6I{{b{nbMRXg+iuBv!S3^?&kyX zOSW5Ajey*Rs=3;FL(ZC&bxqoYGAm0557#9>6=>wqbPJ}(3Pr>m!|5<%J;TwHb`=XQ5jJ6(F^3A zilA-;15Bzs<{rcrg4&19!tE2)=-i(_p>a1K;~COlkSUu`hj4Il2_Z+v&R%tmSK51b z|7R3tWr%>_X=Wy=o{$H}$=O;*c5i<<1|FkROiw^7CK+W`K|I&=kn|8CbWfQEc&ADS z(yD+%Y_boW-{;_8S+{5AGP#Tci$uk-w2D|#>Xht(O=)l4>R)nM*5=O{`=$J)Xj@ZD zyTM`$MyBv*jy!DhVRD4V&{v*&_ifeXLzB00=)hn%;lv@R=Sk2!t-yrLg27~C!Bp*A zvz7s#)~C<7MiR;fd?0;EngJ7S#UvYaA97w4%p_H3J-gdlM~dZ0G^n;v%}gcVMZuCm zIUJv8GJ1$-k-F+Yw3uOH+TGRlaXz5sH=Q}e+T!@!yKEq-8<^o(uKWQCB{reEnMC78lGdGc@1)6Y}v;`6;S931j^w;igt>s)|lYidJm0 zwWYnRHLu4ZTVW|kdRHf;-A0SvNLuCkEKFA?0F7S_iHNYdKFB0gLuIodgPgAYl102% z(sGIB$Lvr=usr*`d_Pv|!Mt;h=xXU8k#n=M_Pdhyar8}5-q+?K)DY9;JnXB@ z_hmD=C`czqJF8)p(E)22^lh)#$z@$Bw%5XTwN;LkLOKDYu=547MU)I(;oWxP4n(EA zA<{mXU_9*K)`g)UGRNy};_1Pv<_$9a|ASWZYTB-@?BG7S zInjZXA}eUpATtcH1m2#9Q@D4P3mW1)D~?OLD6g-&GW7Yeh9n4%hej@J5|HLmn_XlWS( zXmKtiVU&|$%nh7oU#x_N#M>jT|7?G{OQJ!*WN>>yaui32)%ck5>@WFY&a(8iwi7dn z)$M2ZP!rCx_=c5p`dyOJ;HDDir-6X6)v_-$yW;NJ8MmB#n zN%VJu#w#mJn=rl$y|N`1wyLPE=D^0mp^QGfYppx#T$3Dileh80{No-&`i6G;6BScM zSTmao@V5^6y`g{-XRE87lnfyxf`WMoT_K@;;L5F_r=(XzU<=bhx+ykr>jLKd=H*mM zL6q;kqCp!np`iDB02)h==JIB~+aiYGJDy(r{?7d(fPlxwVFlFJsdUx@+jfAMs1!QU zY-KQd5PWRE2vSO10x40-lQqkC`#Ugc7ocl1u)M`v4zA%l zb>6dbAl|MrK6ACbnIwzVc78Y~(RvdgqV9SDwht#KwDM?!f%lMIcFTdJCX21BF)?6U zG@NQbY(K36o2##WwbSfZp_fEC-?73w<^yAVv9Gqrr*x{5N>g-dU0n?oD{HT2raC-W z-wy>BYr?pJ9om^4$0~ojV3$Yz?5X8s7poYT-CDh!<3&e}ADJK-_c-E(tvovC)i&0w zNbAE3fnU4f&Lu9scW1eTs@=ct7!RjvJU42SCVJ}ZrsDAQest3acw7{H7K;G3;T(~7 zNAoAq8Nx12ozvGoL)&&~{H``fMI)1@xd~wY@{#r14)d~Mu-%N^BH-;52LuT0F;yr? zpKrmq0|eL4wdwiAuIE|$JoTi4=(AqOJm)xTH|QmNze*qRaY|M)_De1f%O!pL5h@v| z#^OC4R8hxBo*wZdF7^?!&yAtk{RfAEBjGzBGHk!>vC95c1Aqf0Lv9?F1FLp9SDLCt zt{Hh4+$QdREznI z(bN*?LyLc7fEghx4K^R*a}@kFKC`{yu+yUF-q!{iB_PJ0l~kCSlk-->bn6y#jpb>; z>UaR#j74lg_`kfx$+xd~+`&Zl;-9;z8(y!i4+e$gw$~oraEA*m5|iV@khtpC$ zx(oLsMi;*t;$QoooxdZv6F7)*(J72NX1E~b>+>wj%pBGm$E64XkjI;|oS0=hD`O6? zoBs@g<}ovPXjYp-I}q#C4kZ5Pg|pd8%8ijV_9UJSy; z(EUMdWqTFt+gDf^JH1R6e)?An7@N0W@oY|?jx1bzCNO7?%aB z8)|s@~+FB^EETp{Ixqk$`os&~BEg4!eiXXHMr?7gpqiFM|N@|)vOD962 zKDrTvE>tlT9zr}uj<9d3c%H3^ToD&-o{~G+%21Bxn`=O|P`UO1XTq=;oNP>YHruHH zskFs=V;HNl|4kvVrMDoJ{M7ONZHjQYvc}2g(m^Zsl)6m=T*bA3cbe(W1Jd>@>`pS#^XiM+ zI$Ga67%s@5x1}7FmS0k1?k;pL4u0W!+EoG;N=s$N%f}LD)Df>bYs8L^S`*#;^9xgF zQO7@~f1ebOzAtal*~5SMY@QoX)1|MRA>@d5pO;%GZ7I#q)t`3OgUzZgNNkyzf%Y`A0tU8l5SPF$^N{V0PjK__lZ?#)nj$%zhMFJgE3_ zE%9xFCdv*#(yWo$*v5tBA)TjGuDG(areN=R^7q!P6yH8Mfi0*F1o38*(;~A3vm84P z&PjC^1M=6ed*4E_u&Z5M=D&GxdJ9q{gMdWaa7N$J6saCx8Nm9XgASHL;QelLIsBgf zDIp=9+q&|7eG>e$g`qu-!Ocbqz2jG#oIhee|F#_~0aTN)d;F0L+q&Q9`LsG(qN1?l ziif*NC@RmjbG6H(4}tOQpLK+$lk!t@rQ5YQ=vjFr)6 zNsSc4a|=zf<)r#K!;)w}g1(Jd^;MhpzQ&+4Zf(b=#I?`T2<@MeR$<_gZrcEG%dHm zk^zxF%-E&6ChQfU7XeRPxmZf8YCb9|e*N0+Pr= zp;!}S>9h>^$hR*gXh!t&qGE>;JUm52Uf7UWZTx|wJvqw4D_KQ!MHs=bt2?|_winE6 zonJ0>897Ybe0~OVXt4p&`jUclg$w2zv$Xdy21vB3uZrrB#1AG%1=`3B^rEsW=z?O1 z)R`KfUY9>lU-{l*>P9&@R$iW2^pxfG$F!uKKiuorIFGzYH~nPP**D;)7Z3yxZ+Txe zt3)0)#g|vWd0L}f^Z7BKrh|`pz2tb4m$_A_>1_Ljoxvs4<+!xz;n$uT3hxn!cpDspnt$=sV>maT4dQd)OM=QYs{`8g>{wgSS`K`%GdLRu&wy}qw&ai53 z(D!JcO0?(}W%Y|a#Xwo9gM}L3v&|n-3z%1Wy+0@0ZI%4&dFZf>0C>pnvQ>Blhb{GR zzNrCB1LS38#de*5R|Rkn4g8NbL3D^^X=JZRj2|g%YwNnq8#+>wUUE{1Iqa3eLUd=( zQX|YrbVot=Q+s;hy8ZK~1S*<*GjmgbQzL}ze(V;>0^;7}&?6R0YX531aP2jx2zpjd zy|}hylAi-#= zp_av4UBNCrGLqwtx3nk=jc9IgQnLud&*wX4q^xvgl>GY@SR!RrJ$Y3zv0W4p)D2;R z_tG;{ceOT`(v&E1l~&AqxFho7>Mfsn`1a*ZBCmp+0w}y>re{`Be-pq`J~C+%6RYkv z#_D(^90d9alailfvl^k_;uB>&@#5*qt57fuCL)B+tbYk`AY@MZ=O1b@vm%(=%tlCw zmHNe{ZM$pahYth8!9GP#F0w`t!$KNE8X;bRzn&sWOPZ;BdQuC39EDNF*!1}5_@okU zf|8$3MST7rVjY*7lG6JJ%dv?m@Mk+6=i)7o7hr8^e@Xf-NpoKKwW8n#E*-fiKJpVs zYTX(Pk?8kyu1AKRwkKfdNvHl{=oxYk^bZ>yd~+CsFMBmvR?Waj!^?>t5S^7}%;Q&7 zK->A@85IPdl4q^mKRzJ=BVV0qmYs8Q^%wFbZM3vFX!Q<_P7kI5)^um{Xqu>}cEi+9 z?+MVEe}wu*h5;|mhj0@;ijchz$;l9H2!wAQ63ui#tUZr%Rfn#i0^%vlEe{2yh0BH6 zO(Bi+g((v|<;vx6E)U1xCp0rx$vPI?$Pzv$#%mivpR4ur_Ac3G=P6uxRu~VH*1mU;UX#7tFm~{<$|U$tF2SxCb0<(M&CA zFu_N=Ff)Vqr^08?1V;>tE}WUy275mr@JY%-4Ur2T{#x7n!!CIco(aYP>|V2EoAMJR z?gQJD&Oi>f3KDB>$TMPctU*o(E)6$_?@)G^Gl_!itm15R%yDw^4)r%LU#_t$r0uT0 zeh$~O-XD#Ac5d-Q5JIY3fxW>=G|6KGS>WiR`R9Pw;ZAQ?O501>5xKbwy5@J=DjZY= zHr`Bi=luD*HlKY~?wE{*cUKwm=Wf_UC#ve|{iHAJ@WJuW1ah10akjq|rGpuE8J>an}u>Vm9h9}j~K3H(C zl_wPr;CrG0&{}WbEdKT*yIuBOeN8wWFZuelRsYo*XsZC+#=t&6ma!?dR;?utU%H!9 zt*+saNY#>8OZrOoXvQE}TtN9^1pJkgs7yk?y&8Ft>6uHycc4daJSFiZzpZoy4{*!A ze!aRf2a>cepP%!D@c;S0?edd{a7>TBDi<$iq+|#scl08IfD&I0!{Ibe8Q^c2r+x9l z$jA;Y28_<>>FMDZ(JQq9u>}}Sl04^u$O*@ZF~l_VgVUG!h8dXzUrS8>b8>g`b0V%P z4oalVkY|apZnbbbzEb~a@{+3KQqbb{;G;0We~i$9NJ?~tipU12&LPDm7PTeh zw0XkxA$NRYin;^PZA9&4|F|{X<@9WA<)bFg82@0wl*HOOYERZl-3EX`;ZK|ud@*u> zbqNk&48^UB{}`DWi(`qdPj~-dJ!nf6mwc_Qje^58`DU$k!CgjHW=tEXRMO}`eesLD z>RKuzXE`#oXE-|mLvj}V7G(4YX;3yWJfy0sJZ%nTJ%0z1s&Y~2GU#e=-gu+Mpn0D% z(@}9&HC6`;srE=47-STp4g`hb&M4x41xXt&A=XVFO&eWNR|S1LUvNmu4N0bF99k}G zkY|?-Ar3^kvpl{4S@tjWLA$e8>siLUAtecUA)h}d1t%4Te}fTImp*9g+Whoo*8L&} zf2@k10>?& zlKINNe1P`_xE4xwK(MzWe2yjQIT3X4=k^wXH&R3aDK#?}I|Gp~po~;zzL>}iP6JNY zPdWc!YXQdjf?MX1XeS$6C3}@~k+7bGynKxZ`)PtJ7uW)`7~yIsfkB_H@F!R#!Kfw~ z0I-;r%QH}SsekhT~1CxR(2pZ;iI~S^4QFbi~XK)|Bhe#S|CWwd{2*0|BUXH79CBk zYwEuOySMnhkik8_v?*X@VO`Mc8HV{R`&UMF8oh zF{osJzy2zx{z{z3d;M8l4i#0B+$_f5T+zo^EDrJKF6P!o6R9o7V78IZ0&8SyX1eoT z8Gweq69jTjG$+BgY0tjlToPPG3P9JEDAJ6FK%qmWgPAA4cYK6|Aj3!ip!&GixLYvXaf zp{Qx3x_X5SGc*(lNIyVfr+;S|`|j>`_d48J!$Q{Nf1?#x-lL$kN2bZ~wjF390xNr$ zyM%jvnCBjHUg0DXy8|TJIVnW!8XVt={NDTE#f8D z!90&Dd!E}1qmv336ry9|DZOax;xF4P*X5OV=mpFi>Jz^vVK@@Pk*DP%v*oU z(n|%}RN-EbUykqFBs+3D*qGAnf3aTC#KqgZwjusc9}AfaclrBA09T4Bx(Cyp;Nm zftF&{`&FodE2w+Rq0H;*Xd*kG8HGo%17+4T1uL{sPSX8hh>b5dtef9npL?N z{_aBvA(l29R1B1$xBF@LmNS>B5Zi#B?U31zVbm<;h8wkh%fa4Q z0z{kSvCl6K$2Vmzy(_ISVs zB>*hOTsr8JGg$oPo+eamGE-9^3HIEB4YdiLtyRbE&K;o@tK^{tKxHHVEe^16?(h|& zzIX(D#+1R&DX7YeBBpizN!_@j zl*G`=Upu~qe?lzCXefYr3V@bC?5mGtwcJ;-w0!Z-Y)6~`06MH}1Pf}9Wz7gu!=g;9oGHYCv# za5GaHM$?$Mgg*YeS5a6wG_hK_yVj~t%3KkVKJy3rhDye>c-4fGS@PG5bRW(lOM(Zc zkpLjGY^j;sudgU;-8lUbwS>7Q8`Y{~tb*ba=gHlu6gx&N;H2G^bhLQ6L-3=2fOWpU zmcn=oY<$28J70NpT)2r#xe){_^Kva4e-rUv(gQ-MDjE;%PA1>Zt#zy*~5L*ZEk^so{|8gC{j46|Cv$NtL zX1=>TbvnR~96pLk7Tycy1k!tZ{WvyrGVJ(aPCOX`&f&F~@%G=mUePxi{!ES>v`H@q zj%KlLdX5jKfzC_Of>J@l>e*@Ne}sY{rHxP@6QUYYJ${jG3EKvb9u^Q+`&Ew}5Mfve z!u%lIT#+8}EK212H}5IxbY~F1O-_DpX1)anANt`-h!iOA}B}upCo3%&rzz zF(++LM*KGh2u3pTv5=6wQ8O{m^;D@NaUCdmKWD;Be9I*fkdc%75G}ho97z7w?~da= zK1w{a?5mFJVfK%=STYMBS6amu9~bu_G8!EjJ)a5i&s9s)hzrzS8#H?lwMZD6@bcCc z(6T%{$Nr(PYe-gsmDwF!|6MWAFj^4!BFxRKJ|(CBe7J&_bG2AW;5%(MF*jluO?cuZ z&F;+Z0q>y+wpz!@v;@=J^v=D9s~XAApRE0F8Y(b!=5_WgEQ2J(I}>Uw?fOW40@`Cq zwkM8Ozp+=>r9g=2=@{DKF%Nwo5Cd3d}dr#Xv)i}_;{*63P~RT@vbj7Q zJpXiWx+N5_>zLnJVy%oqUsgidA;;|JoYMdoY7n=3~{a|DOy1L+ru-WdIfqB0fYYrZ>n%uz;Sc)0yPl zEVozL38uuH@4kZ~m!Xs-)GvO+9eBS`P+-lzaGS)HM&Y?N0n7ZLrHO|9mG%R{MHE=a z08`am9x!EX^ClJG!S;5!OVRxHpSQskW0suW!jj^44?8b8pTmYKrn}i5NEd53?6gyA zN5V&F11DNCbVj8Y;H)Z8MIb-y{3n0jnXWiyBUI^7WEmHr`VY0(jM-RyLWB9e+?h|# z2eKNUC+l`!AhlVa+z~>c2EH?4?Y{x4qo9dc+zbr;l~!Qe*7N(ab@=&}h)gzcEF@&t zu0ewhAb3)v!(JrOZAkU^dS4T)iD?EAoBK}!JwAU}&x~rnGxY3e_Yh#-+x2&V4;D0Z zWvg=r^N#2@3mFY6VGG@zS{eMRixO?R;DyowYwC;WxRdrBj_68?uMy8rq45` z%wqN(yZUlfX8t^7;qtfF`K}zp8VL!GR4MEc4uK=KL!#QA(M!2R7`2wwGCt?MEG>$K)QDa4CUeS+dtS-+Gpq^0FM6H}cJ%xxr1E|M zd5;bka;sM4_}HM1;>o(^O(^!DDgaLSF(%3G?PHoh<@sdc9O8V4X}l;%&bxFmL*~%( ziR)!rUdYY*weV01XA;-L$wZdbYjRnv2y>?S1{*SnkI(~O%1A)+a*l+v?2uSHc zhc)@}{xryx3 z)!36o)9CiE01ncFM%G&%n*8CzuQQo!(5S{HR@URGucGaF7{_w1Cas7+oF<8SRY1OP zZTF)|swlYO1PJyh_5Af<@>`9Up72pbd~|N2W$pAME2VeA3jX>`%T(GCvu=Y7$v){T z#@w6&@^}8z>f2}tN8cI@OQRuNPmIZ}Hu|a(!BuuH9`rf9$L`34aON+oGaw<0`} zboT>gNW|m4wv_RJkMl{6L@2t+ez|#1MNmK^J34{7 z`L(UgNNOF;N$|LAW>xHMYZ-!bb3EsYrhsG!$6tTij^efbbe;#;q?Oe#&hf5G2VR75Jis9|Gfl*q8bmXHXA_#z_|XCGs;q zkWNNXUY{L~0farM5)kw{;q!V1V8TXJ@1}=J3w|&g(qz=RPc3T2YkzoIpzY&~^SsU< z=(XA_o9Yy2DO}uJ^)o`uWy=$q+QLJSJ{DRzd3i;7*^&O?vB@{wtZd+%mWT1?b8Hu+aRMxDRACDjS*r4>F=YG~52lww7ugqEFO}rg8e9cAR$6agrJXt0GxX~n z+y(jEiKh(?4r*UsDr6p^U?ArzRPrN)cq8T4);T(U`iw1hz6EM7fEzgj5zZ84uRW33 z2EnjP>r(w)b|}VV(dno!rS}bdv+sZ)oIoK)xw#ynPE%w$4Zu_R zX5|_uXy(u>t7c|rPninE{Z$Tn2UP5lLot7YWOwwT?G5NYV|X_I{p{==+{iHait-oEg5<(eRggIF$@E^e+LWfEfLxh@M!3>|NJg{c3 z=P!AgKjh?iGeu(I;Yj)P4T|?l0rjwhDkC+vTnY;T>l~yIhfB4m*VErw#ng^ubd*fw4G!kb ze9MiJWd?5?iv*481P==QC^TVO)Xr{sYtM~YzlCo74ir?0?u?oH4(NmvIz?vqC0AR_ zK@JmrlQ^}22|Vm`dYOWSCY)EC;F&s#`GEqUvS+%ri$SFKXuc$;x4$-dagoMrx^K_G zJ*v_c8k5o43HZH_O?h1EIqiJ|tRQ^`HxkbjE%r61-Okg!l2IP@UHIM)jmVm>*i7ewC#v(d4uZ(0gtjwy+fg68qUN4LfT`Z0LjVRt*v;ziLo1% zxAD_%Y8r+o@|hhSls)n(ES|Uw6b<%*-5BD>fDkwV0iJ^IMHl4y^Qmbbs`tg4v7ZLV zcs07@GFmxavzb~MR~t>)3rKzQ!nyPPnLz|nU};}cMIi|#CC9dDG|^)3wRTW31NsJL z4D(ywolmC*w?&Ztm?mixd6)%D(dhW*tn9`{+dCbFbrhAU>ltFhY@eM?+*ZpHT7DwY zf@zR2zC6uRv*tQPiV36mvxRbklwb;%7ylk$K@}Bsj5$L=$sqWORdB$qy!zfW3l0Sq z_)=r}Or~VFNE7trOD|SH!1g|-CZ%CPXO`piaN6>&;00j)LT8#5>RM1+r~hd<V@Tj{{C=j*^R5XAE`|g`v^MI<*!<^yB$fiG)MWzfIHi61s5JQ~{70Yy-wY)*6_Esc0z5{>XxghAkP$lW(P21^wo zN%6;WjcNDYSuF--c;FajWIQqH_rvV4Z-{v81pBhK6=e%DR;j2+@2$b9`HFx5}3AdkFyznw@Q_<42dBo?p2tOaDY4iA2XD1i6`yw;ojm$ExQvj^)?jz| z9BQ~72@fwt-4Lm$Zx@ggk`Q68rM*KBcigXO1PbP}(yhytZFva4Znm8uC}(VSi{TiC zP60Rb>q94v@S$OB-v(O2v<Nnb+`qpb1XIqW4Ka59ImBskELH9FY)CqTGD=>4 zh}`8Bkt;Q`Q23-jg1IFu4COM?_FtQM224w@+Zu>c(Rr(*!j1ab-{v>#=48Q8yp~>< zHkmVN1x;R~LfRH^k^?>A70UBq1p$fMUo+6$YFLuvQvUQ$Yj51S`x!8{7IZI{xpz9gFGiE=S%AO#|A`)4as7z zVjW@l00xM6-CwloKe&0{R**sav8kzPPRQ9gFKjR{YJMxCn2rX#V3%iEvD$7H@U^Mj z^TL=&^mL^snx`Ju@BpGanIQ7piz}`*)a48m`3ko6OybY?>SY>XFS;eqXup#qX1bob zWswbFjO04~7jgYel*7Y=mm;zu>7;0@Z4_c+f-^fpmCI9W1Ex!sn%id7)d_ByWHLCo z8NH5l@LWn7ESTf<$V-A&MF4~atP55Vt?A6S4If*D&T~gA94M5$MOB{doTKU4GO1uz zRyzN!LdcXvn3z2hQLtlXB2nFfCeq-lY9#W3-~Zvt@<}0+&M*hoKL5LbNp;kP?a98u zLBPq}=w>Br?1XaL02cR#KwSUdg#!Zv zfh(cUzNx6In~&Ah zO<61T@}Sq&(h|$@tDatfD%Bs}8lbQC8bTIkLA6y+7xK^Xn#)lc?TD6A^uia`)}Fqe zSDV+`??gHpj@D6)3gJ~c0oKC9D%XZ-7+)Fh+D}&g2O1a;Qa|tB@=fUy3~L6 zpkZAa59q`Hbb0hN1nhS!$|!#XAg%UG1CT@%)qYsf&UhvMW^XB5l3YyK=Mbd^KAO?% zbt!^k8T#)yKQ*|kVeX?VWgT+PZBmpHR}NlTn@))u?HG@+KPETP6_~5Q!<8NuyQNdV z8;URv$d_&|SHzgKG~jK$k#Dkao2z;(sS)&81L>DZNf5s^HI+~i1DBNQF?-;5P56DH z0aE89*DpSAlwHYizEiT-9smwaQXx-$HMJkk_h7*0;&-H5>Gaes7%qg^G7KfgM!|sOVAs3F8N)#gB{rNAP~{X22vhb=nN!G#rU=Q%>fI(i~P?tA>buA(L6RisVPzRm1E8#nt`S;^(+$%Y>#PM%do*A92h?45c9GVq(!B#@j*TwbxbN zpT_HhFY9g>Sc%$4q#OXgfkk!dO~)wUv^LHukve12jd2OOJ&YgA*G7xDZB~53%%d>s z*&bz9?Twcit>SWD8qdnM>JD%JmP_3zx06-Gq&Ei%6J@lx1OC2c0OT7O%&QA`?ZA|; z8BHu!da$G7P>Koa+0JG>dlt{E*D^#B4p?LUlOGg?kGwGN3LC`4-=2ZP2j=C^z1>k* zp*LDG-%3jERNtOuo3q#>72`cYmH@k6K~;NP-QnWH=))Ya>7b*ZX?=7Agy+o)7=Mu+ zmX)_d2pUWlK0`te1!W{hSJ$fiFGV%)Zj*J-xHFUt`sDhPgEH_5NxW8flN`#P!wV6pXd{?6(lDpE z?W?M*JagP;PMdGysd+0H`u=|OTh*k-LXE2xpg{^+8ozz%+&>HSp(>r#(EV92_xln$ zSd?vWE~!5oX+teXYo-J z%F@_`b3uvXV#~ws7Ev9-<&mkiZLX>w z-V0u;gWd4cYK&5|JYq+p{c6PwzZNAJ0wxsHT6qX}YGFRQFd3KI@hv8xlA#i)&(%%? z>Mc$W+Fbn>8K^Kqow@fp^InuDLv>btG1SX2Gm(!ICK?|U;21RI%+S44&=3OqV) z`zc(OkqO(02l|?tXUrTnGs`B6J6XNEp3RZUJxhTm%1K(nEZV#_N@O}j3`iVC+28H` z>)MdKoqs}^<_AHO9?%a*A!|-!B(D#V7aUK1xv=L)+a7H8@{P5t-(S(84D(h)VZ;zF z@<>En+6{XOVe`<-BtMS@`2=o3Kk42U~w&s z2fJHlbEC5gI&>zxv>2xloJ6-Wg?ubw$ap}gwGT?<0xriAoDJrF`(f?hPGqWwQvjjl z*pMmeO%Hyx8z7UFMMY5B($a)I^G)M60WOs4B+x&Hgy}0A*w^G6114bo$@>#k44jH` zGV(grHc@~to7t-GszAD@bK-p6o%Z6XSG*_ryA%~I?Jt$%nn-T<%fn;eaHqd-LLu;P z^wST)75!S!v)j&JX?z~)3m87ILR(^*UGq?Tc*45(q@%~-KKD9xZvg!4vBo7YHTG4h z=I;jCubD7aS(O3iT2E*Yn-Z9K=xrXWpR3`UO?6y6bgz^iP0@7$Zw~ zrzyZH`^OayTJmA1_2R$&$ z^#>a9Zde-TJw}1I@qEXJ^L?eI>}?tmR+>U;fNkHNS_r@@AU`w$FRFi>KJ#@GYqM1A zn^L+WqMEO4llzwS3>Qc(wRnxH_Ja8j5(B){x7Eo824J>~mE7npM6H%P&8IT~xuS-h zQ~@Kn(s$qev-5*7Zigv&KKyo`g9Yg9!$k7pfP>*pPEvwPi(ibuamAh=mCw2C@qL5Z zdbVGnZK}NOE~ntTGR+$UMYmTOq(KCSwD=dyD#HJ@J*Tsr+%m0#L(sSd+U`Oi7Z1_< zFL=xZV_BMqoz7eHOaR{WpJ%ulx{?%Rx;^=AT2~CW^3}cqL&6X&KT#X4>yJ1O6qI#3 zyT=X&7u47CI$WAe`<$mgRROmCzjKf%aGG!z$=R}Cfg0)-(bKc%N9rA)zJW$9*F3rY z3Cfbif0q478$Qk{$*DIytiGR;!Oe+ZJr&r~dw4FfuL#)nnpt6=kb{j$tX;g?_*G zzE|`3ns9x4KUMdy=D(|hC6)_oB9;eEHu748Cy^*onyWETUUupmv|X3q(?0Xh7vIEk zp6xBQjQE+tz%xc~=%Jw8nSF|j3xKQ2mRbNX$5Fd8GygZn02-v%B4Ea%+kUu%WFV#z zXHTbsgaUka8LbXXK)5uYk#=u#E=~Z0DB%1Op^&=*rjbV;de=ulp2YO-tP^8^^KNN! z+dlk3-SghU>2DSEAq*nwC3JUsp^4qvwCs-y*Zso!X=Q5XS8x}`!j_hvq+>RZYVqCd z;6OZNrke*fJ{0vNpzxH3ub__S_&|qq| z#C16N%_;(_1Kt_eX7Z7quTKBe+&6>rGOlTL0fRJvq(Y(s;3rtw)fqiU-~~9cOCvwq zxu*YrI;V%>Ug-broX#HOA`5hmtDVBdg(tOce#KX7G!+N#Y8gV9k*hd+AZF0OGw0=5 zU2N$m!;8>U)*HL!MW5Hw6^mDcTlqgN)a*k4*Fs$n+1$EraM~MFCGfaV2A9X}RNOPb zK8?g(d)V4*^{QxxS)Xe^m=+XHH>Yv00ojH0CI0p~v16yzO|Hs%{MOC?e@81?EA|L`HASETObR*poN=u2v0s#T( zmQX>uTcoACLAnH_Q%Z8t()~`{_5F?i$-56cSdQU-@_yz$=XHH9$Vu!0_;dWbcf#u{ zq!~kZ?t~bmq~hYu=P=0qvr|E%thaDKCZwgM?QAm<^>lZ0gANd2e)?Hip7pa#*Fmo| zhzPlC0*qkc1u9+9v`RPg=dv$U%+0mUJwigN69O=ujfpLCybW4ts9UKi8*r$o#BN9r z3@T&nMu)I(30r zj!3hHnahY$Z*P98u6zg0AfE5w^DxM35xaj6kqaGBR33rDc_L zHpWg#Q-;`+3C;!Q#zP!ip;NSoyxiX%?NXflIx#@Q*QV7~>@{hOBpuPO&Gc7%R zGyzeA8=|Ow3lEPt7V@!m)RtzAd-y%?4ZhrW_jEk!sVkr%mjl4yOC|mX1go2X{1s~! z>+qGPqmw?8<=?Y6V85ct%|k5!Qnp%6b%)W#)J(eAd&@=0GLBm#;+AM13kuNkxuGe? zHaN(}&Zd^^OTIN$xlML$V)NCnuVTet7BhmkYfd*CTQdp^S;|@VbIuh13WeXLdCa~0 zqqnD#LOqJ0C!RZj4HKAsJITKy;m?{``13h0)E9{PdA!VJjK{eYe_AFc_WrE?&Hvzp z{#u#`ZbQo5KP?C-PLh27<>deMEBm#7xmtgI$RNMSF#5X%bMbFr{7Xsy>sON-egDAO z{`^qO3?e+h&ZsJ@K-~BPe^da6pDyjZErz(iLK?s{qX|#3qoAO`XSCz1^iWFlN9=#U z8$Th#Ch8uM@z*Stdn_rv7xBTQ@ix|-R9!}Qyj<^fNc&zyVg7S8NvQ8_{)&_S`8A-bCp%)F z14@S`qC+8l0$#TyRB}bglS3Nz?|H%obo158QoStyU-_^*6pGJ-o+&Fn$jF4amd^I> z&W`r}-o7x^APP_fdpi8)n}!{)0@`FB$+E51CJBE4>hgQyvuj?^Y#|2Bf6SfxReuaYI-z zck-aF`s50HQ8E(#N4k9a0VceV#QBo{{S(_Q-x4ql!>vfO<#B z8>=*49^n0GhSLsehU)B*#5bhUJCjl z@u5zeBhg}~;i0XSmG%C8z8Pen zw1`$X2$b%S{?A#qhzSe7b2f#%T*&@|)00zqX@yKmHk$e99lbRX{p+vz?gD5F6rauxn%R=$G2!I!s%{r#ZFmH81HaeLgBabW9Tdu7nql z>@;AK{{LKNY^@4cz%qI5I?wkV?v-}^xyHaZD2+D-zHMm3mzS^r|EUOeC>+jMGQNHd zsSjEks;IezX>)N~mnG$U_#k?E2To z0f~df&1|Iz#Jp}+PL8^QT1rwbY)qC~t=9{gSK1pA)45BF;R*F3ymr?0m#go8fV6PG zMUZ{%#7{kAkS4G}x;Rpwf1@DaZyO8PIK3~Z{oKgWh7ADLQvms2k2;P3#?QWe|igsE=sB` zo>u&FD?%l>jqFoSDPMir+g~9|Erw(M5wKlk39#K|FD^gu3SG?oqk#ncH`KEGqyn^z z^ftD|7NZS5cNYy77ZO1J#g8yROnyVGE^iOR<2D*vsSm;X;-FK32wn*ZEi zp7H}A&xfF$PYjH5Ht%B6A}?F0DKf}FS_vS{O6j19ZhI5}` zq0$$*`tFC_t2gHfE>3R)3rnSDJers*8yGB ze-b3`NZvxce(kITl2$%=Sg1fi4We@tZ|{k($eA$eM(?*zHFvN%m~31Fqq9Gx6HX8@#%Z^x6gLi!M%XX zyB*chCYhC)X@dAVKCS^0J{_~DsB&3D z7LzD~=PKeloIBGR%1y5G6HWge)yC@*%h(Y-6bVIX#0c{p%M| zg=fEj?PAg6eHJ%4O^b(jbz<#e&<8+2cTSb(VTmuOW^9}@D&U~v}fWK9I*MYlb>*_hh3&A;1lvqquk|qX;9(X3QzLXTi z_7nbKI-mc<)o!QhFrE3TL#^X(i`Kyg*lJE4?NpVX->%MZAFk&;FtgGxe(QZEpJ2I)8uX$l(W$)ZPOuQB)8PI(Z7#(;u|X6Ug!KH9z`&b7k^o6@ zN$h^N)!MB@ZLk`}*tRfD&USNtjm@iX?ZfYEx?ZYgQ#B#9xYhjTk3MhPJLMoRb&**I0xO*o1lNS+ekSP~0STB)*9YD?Dte9&A-Xro$+3@Sl9!66YWFwci}FAP zwk_;OpQGXZvt-<{^dCtvo*H&z^6Yakfzd`;jXmv3rtq&|ZVpV0-Z zZ9%xP!5d)s5fN!AnA)|DMUIW!^Jq($j#JyQ##u2Usfp_$*xvG%UTo;5j{AN6cio+`Y_csS+%pSIWb4}3ng z35X90tAA;GRa6!p3N0C1bOr@`(6|OBD2~V9UsDd_q6?+79%N$UWVN+4A-H`IF?K^J zB`L`xEbNu6?0vhi(v2VO%ANR|4E?k%;a35^Zo45UZ94En!2)4qrf2v0ZbhZo?(k5M*B1F;zi)b}sFY>6w^v$D z?))Bs7{)p|s@X+npEmcZSGq!n2UcuLTC^BKeN^N;Js*8{bw=!;Dg(Ah2!>g5eu@=f za7?!cU9QQjkP>-mWCqS=E{6prxyd7L+vrD*l8_Z(D+BP4wJI#~hU6|Z#@HdIU$rZT z-)5do5d45{-Nbl>!iGtm&Bl!2aUhOfrGrBpk6m;)4JK({U2+xuoaO3q?X&(qyEWcD z>H~f!*Qo8)<`v*3rZS~~|9+(fIpQU1LhA!OZwSRiKgATezNbeOetSmlK5MbJ7Aw25 z@?O@4NaOi_54o+Q{YvapY4!zY_ie>5DQ@0Q(~OBN)PzF8 zZk->wc>a8|4~h%b^Ai8NwRXs94)YW&_KEC_jPkc@dt3eYAFn{7lvz%!;&m!NfBETk zR`ivb3$DHXywJOi^8@Y_*wcRkw`1?*gNUo5?mhgU-QqhZal|o*H<31PAkaA_7;%f{ z-*o0(40=2=l7T_N#kX5%QeYTzIhw^oD3M`m)|}v*pk5~%-2!RKahf1G-Ivlvi)x4W zlZ{(zRSFvC&$Niw^tqMDwDjUkSMdJybF z0O*(NY-PWIQD{yZ_w;_;{qeErHen%6W+Q(ALkw?xDbcr{xb1%XvDbm7N~7*M`JaP^ zSR6+#joo~UFsL(V^rgiS=P53xDzRBcnrVpLppiK4T8Vg;tai!tb<04(&#D`ld+3&o zW{A4)(JZE8TpEJUAQD^XwEA45CT#2m?cS*0eb2Y?TrSb2C$;=lxi44(LD35ha;{5u zueD1|4w)gKvRHLx_mYSI@BOj)0e&GNArf2Fgwxu(9BP-@e|jL451^vQy6ydmkjH)3 z`B#J+tcW5pn8d`4t`sLrE6ZmrPG3*lM8=Emb2;*Gasou=5P!}8LRV}t@(`j%i*E&_ z195}t(16 z_?8PP*!QLr{K@IjW3{7;!n~GzeSPyL%SFsQluuVq67898V`@r{qcG&ayt0WP;G`J( z&E{q=4!SHZD)fh~=7;Mp_l_Na^a%vZ0ad+bXs)Y?`)n}czrpzSL?>`jX4lUB6Y+9# zdX`+wyZF%$h{Ka2*v{#;7b?TWO!(Ns0gFLCDh&B zh>$rar9w8&lES&A6~eP+(<{d_zi5y$SH9Ml&N|alrE(y$tK;H)P$fJ*9J>ZecXoVG zI}~&f%A$K@*hB`cPbGR%iU*m>gk%p%*Zbu+HDM4YUE_Eeje+;7%WnpEj_nWi!VxO? zCA?3j8A;Z2Q$h95%oXT`R1dJfl+&(6m4tLzL2Z>ekK?hQ!{Qu7&2xMR`lq4y0Yzd<*jYV_ z2y1)U2|0l`1iB7*0nd;87!)$wNya7!ejWs!dLc&gz4}NUK ztc?Zg(p`tSh+su#7kx#qlL=IJQR5P*YL?$se3n-vU!htGLQ2XUQh7Ho0fY z#Kdgu{?FO=t5LQ=9Uau1-zwp_xe>Zg+F&G4WTbB<7Q>?9&@pmwZEt&N#6EyKnnzT2 zb`}TH5s!8%GCa4zDtpHg-FbVxDO#rZ=+c$vDsP;x!`-9oewV{D)<92^k!g8LAR;;% z#)RF->CE&jVtLw}+xUsoeH{t=i`1zKG+id+$$@xJ zk-n*;qZP%XN@eD|^8MwrG8!|gir;Sj4Xp--jO5RO*2`bgX-a>Uk-Oym_nElXKSGTJ zrx5vuUPN6Y4IeXB`~4|;E@H1i%LhTSJHCr`{HC&Er_`t z5}bKqD+9-Z%a5Fus-}NGMn$k>j25^(@H`yBh#(-y&S{_f{u9_Z<3*-4sEZ%WK9{E) z9CbMj+{K}AQPKnkI3C{I@L7%LaVnrRXMiAQcP{YLbl8)NIWfT=xjaDYpK4Y+)|<DT)q^n|LDWP^hf-^I|&Lu0oFiS z3@Y?DM;;X4y^#87=40Kc+1J6axLjP$oOD zwnm198N8amJ)l5I*~V#PpeG%Zoqa#x={s@M^Ii5|8byaEoq@hf`l6I}eFH^K=^idl zCU4TBX}TXKRK4{a@Pt+q%xhr}`6$s9HbAu89eWDTM~Gf0moRn=-bPOn*-g;F^D1l`xQW@J9`T%_ja3*Lqq)SXda?$v)lF zNb8tLRfVej^2hhd9yH)E&l;_CduDgi5UDaY+^qY1p{>uicpb`S`CTE=m0!xUe1CK6 zLcPTJm$8_GgN;m1g~#Z!oQn(f3L2$~52~!mVJ~F6Dsi?x)%j9#ayDKaVLOzP^u)^! z^rKgreX-&0O#>xs}%X?AL&5yBObDKyWZ;t;nm8aT_>$-ov5xH*xv#C_7_u??|+xj=m6uOTf3s?ChQcLV%$$lXUzr z*A&YJi^z?H8D+AXC-smlV@lXu_`z=ehj_VkPsiP(h+ExFL(_Hl;+qGWmdd<~R4fii z*R)QoCSgGAd&>20BOlB$(8HXnv=%IZe8Y3{tH%SzC0Ut$8>jLzJ=rq%(%Yk3sI3AF zRVe5>cCOU6XCeSJn&Lw^eF>Z5&)Mi*(1T1;(%d@f9{#y)OA?-LW8}4<8XmS7YfgZy z*@+@Et-#!bS-ESX_wcp8)6vm`CQ(Ty|Z51J^E(65yS3FZS8 z?ux^AMjt-$i1=oi_?B*3I>dP8WZp0B~#udF!8E01-#LH@OV` z^B8Hup?;&hkPt3`=b+Sb?H(9Rt;PLY_?aWyeU<=LX#gK{nNI&c!o1HOAIn}uRzfuy zo}ANVscEce`SJW$M?b~-xvsX43`HOvxn zH#l8F69XeaxPX=xk>X=)xeaiGc6OZ`nw@v=yF8s)szo|Q4n&B^Xd+V}(Lrw}_|LsU zSRt4HJ+q8pXn$`-#D`i#qk3o?R(}|sr$Ve$laRQI4@g#VXo@9L?kPWi9>?bvJvNJ6 zH3o>VkDaNh%$O!-BA#~j zw43I|Rea(6(%Jb+Now(@U%c>SKBT7wV40F8IKwrY-5%u;*^&Iv7^^Btdn)|mxHClg zy?mVhR3KK>I(&LVTQPJV*^N~Sy7HG=toSI)0CKChg(-_&vg_IorpomF@9B4m6*b4k z`!t>=RLPojR#s4%K4C2d$}8pIo|WWrX%2!BoTp!J52;p`d;QYQ3%r^ll~sqs{C9IY zN&HH`TGK_VCz~c<&sMM}?jb#eNRy~)tp4jSkj)WlWJ3DL(v$Bo3thyb47|s%pOjj; z$7|??Wuv4cUsh&^Z-OrB)OmJ*9wMF4=}bzP5ZYO0_ce)JLa%>1C?C4j>!V8jtPv-* zX8@us37(>fxo7ztbGOE&?sNW!O(T=y(fu?xbPZF+-ZBWfH^wSI5)GR1q$&Gn&7 zEz@+M{{c>kDo!w;i3}sI>>v>A_|<5xJ*Ot14lZe7`i#j%{XjAcfpQtR8jxG63tGgU z!^G>=3w8NDKl|Qet;?9cI35mw7}k@?JNJLmr47OhGOBtN{?{iJzk!(*+LRRw1pIravgw;%SX7Sh+o0RNqO6Jzj zX|BWOlQJ?rtHW7YNlCSLj3l}>+^NnX{2Xwqd+RgJ$eozT$i$e4Ee^_^rFQh<=;NK$ zFFh$!YwP_z$CFpxkepsoi0+1|_s2UnZ;g}EvvRUiZhV|cZ(EY+(FT6SlW&npyZYY7 zRx1H6{nY-weJ55}PQZw%`jM~Y6lj?q5!`FZ7l~*h(5>G;v1zX4?x%@~CWMKu%5#<4 z^V?@EWX7BBli82g)#Jdpg|qCD@+Jr|PjOYj z%*-9R*b|Rj9s#WUF->X;2>l1#Gq2CkW=;}WOu9(=z0|{hXCbUfc*3@~z4>7YN+F01 zhT=CgLXGfst5rjVTV>f6pb5@GUE)MUlx2d^5|?bEy$RVal;+LPK{4BvJ4eIkVg zy*9?&K5^6mbVCWWIw8Nru?}5!sZEj-&-;fhgvGH)b@j}@rYCplwh7-S{euv8K@v`9 zKD%BvzJH#oo$KCa4>*e3W=^oWej6}odrX$y^S$G#q;!SN{tQsR)VALum&7}3+ZWrM z$kuQm_aF|i%YZy&e4VqqYcn@LM@L8ZFF?2?G_lUAf2K~LZr9SCO>5Wj-%ay?BxnNc z6_iyH1f6d3S-wWJcCR*N7}+t~;y;S`qq8+Y)Sd-4i{1w%@C__wyzsuhkOu-k$+K{3 zv?tbJXD;G$M8abmG35Gt0?$5wQjJmrUq-T@EbUc|A4*>qNS%cUQ*+a9dZ^l8n^KB} zsrTfyI=ksg)t{;1S3@&J)@IQ4M&G4oN!pY)uiqalEeXx(TBM7ENJqxi>`aPbo$}cg zr{mgqGixdo?|k{-;_im$oEv#AWoWVL{Mbh3#wO6yh2@aXTkSv+Q*K+<6J9sW-7D2Q9fF;I=Wodr-LJ(%>q^U}yB$AIxCYz0` zvKFSPV#~ouZUFv>S?#O&MRLS~?*@hj(2_G!{Sl(Th$YiJKP{^`x!Y!e*wlB3kl5TY z<~3-LRBGHOH@!B*;7WxC%DGYw*?WDf+x5h|If`=tQe(8BT` zsD+D}-f~3{ZD(op?KO>eMw8jCrhxUU`Tj~SdWY3(faS%P`W&uXHeoQKzTWx|7Bo%H&@GuHuQsnfIL zjg)S9Mg5vMIWUi~jWe@%{Th!+=9n)u(B{q-Q zXC&*V*Ro&wP4^hoeIgvvQ5K`(W@Jnpe)HsMz+RAkz-c3ukIdLPulYA%66{K#_k zQTa=KLzC>BOzgWP%1XcI-A$%)*?T^JtqA8niI@(;S}l<{HV@L~Rx+O*8t&gkE=9z} zMFce^GbcfsYFmY)lO5b1D{}*^_uCTEWlfNc)c*M1Zn2e`uG`XNg{O~?#?zGuGu>h%<&4a1oUS8K`P)cc=oRLTq*wciAGzT)`ne-{HzQ4#y9?>n21JUEcjK`NGeBZ~?_n=5iNeQP-0o;v~la%9u zlUwq;`vj8$=5y1*BG)srGVh%2ye)BhyX)m2724^v9gNhv!m+iQNNsPoJ1|=y~PT7FNW^}RWw23?9wM6|cJ zw+ZrV&^;4`38)XZ(F0Z<@Y3M#nS97W_w&`WX0<6PDPcD-cqK_ZH;?y?7mOF+I|G`Ciwkj*n`#3odx7QF;{vqX zm+CcdML9wO-tN}$WnoF&7=VC?$Df(pWxf!h2!8RF83wjq5zYDjFzKeC6iAFrFrgiU z(LYi-Ub^iLTZPqjSe?hmIcQ2yf?Kn7B8q6tVl;EL3xddkgA*XA~z*X=LSFscwk;a0}` z&U;&UN=-upmDkt6VfsAVqKD}k`EwCSk%`EjAk#&~vLk6^J*hF1e=@D3noS-1x5(+eLuzb_ ztG1Cr(7yjXy_@4~oSrQ_du`Z#Uf&R3Z$oB~df6^Lq}3 zM1_Hdh~4hsKwZNxvw-|WM)8Fb)a_;|d3l*a8{e@Z-E2?Qr}}>5WVg7M7RW`GpFNjt z-7`Zoc?gGWVg@5f#uj^Gz7vmqLET(xmpK^E2iY6j4OEdK5PqAW*#OM>8o5V4 z?>*rqr`tOmUQ>c)L5;3A{HF1nyA%1vAawQ5lE-cd+aE|Ggyb|sxxKyhm!@Vg>12Pq z*N1_egP@@cF9poMR8Gb3LcM9=XQ+>RoFm%Y#IAfisr<*Yja(m}qg4d954hL7%Pa8aV&xeO}Uqz#X-lvkh@*)!A9 zu6DFuJgWn(=FOQ)Xu5^-Kp>mc0SM_pj+#|yZtphd-VnKRaQhP3rmB3 z=&SC+B*xVkoSk2fe+*OH_=pOu_#gY8mcr963tbbsD>;`!g z>7N4*`tq(?UxpV{R~Ow&Irz3#?d#>en@okIm{*3hbaYEd^d9bg5Z{K!KLb^wj=Y?hy1ab40lFzYIx7O9 zcs>ctXWxL2fgk+>eLwbeb$ME(=H#G7T8)=;HH1Qj57;d|MqQ8fe|6|AA`pWX16Onw zlqk z3Wxl?o#;f)_x>f}@ET>3k7x?`NTAj_M23aLM2BBxgeXu77ng&xd|CT}rEU;keFfQ8 z(7s-QCE6&rL&f1C9$q-1$=7@~NDboXVwjQU!#?PrUdw0EpS`0i=n*d$K%SMFsb^qd zsAp*M^7Xf%aJ&-~py|DQ`D-+EsEgsED=1|twH2D42X_0k6oYTt>Ho`6%_OZSCv6ZE zOHDIqP^F`#9qb?M8}4V1CG*tOWErl(Ib#yW88`=?P3%Y-1$&$ z*D!`=Py9kf<)*xFBUeCL#z=WOvvvWE;Cb!&ks!$U#l+n*@z815kX6&TG_;2Xj8an4 z+`J|5Wo9!Bw8x6m=$HgO zU1su`Rbys)ibiwyL{NB?R)2|ON4s896VJZF{dxj`_tezs^@8vESo)B3bVo;(>tdklz{j4ZB3LQXDr93Vs7Pfop$r8gXma4Rr7=jKr@gW zPZdas8X78&3o5jvy}#x*)}*+Jk9+TPpy33J{Gh zW)%GA`&)jYM$FsJ!&BpX3N%>jXXsBQss%N&uRkS5sloVJx^ijdaL%%E<^U$GBal0T z%x$pUo3$wyWdnlRkTR*{YY>t@CGNPD-`QI4!Roqm+klE(6%$#`aUpwJoJL#U1KjD1 zkm*FB9?Gkg)%K9SkP^2IUHOK+cR3N^@ik_{H(N@$S{x<5B98)O(L1v{6tj#!4kN?PE9XxUesuG`k**jGA(8pe0}k$ANcu z%IUx!sCOqy?)aC5H0{>$PjW^S8PGk+pGkd{Y38f#41B)Q(xC56|;$M z<{drHZ(BA$s(LA$D;*tftT#LST5y3bjE<`&l`hE*zeOeuZny5|H1F?TF~r$;I)eLg z{~LB5D}JtKRBY~xBB^f#pmQ|?Q^B*AK=&6`tqF}+A{G__+7lZj7}>R0Yuhk1GK3oU z!BwZqd$Hh|OZq!GrNT{X+@9v4qZ1W%^@?AKjm4cdZ22{6%iEcayssYn=cp*D5E3Oy z5)w>s{Cx{B0VR-iK1t5yn-d(FH<>-TDOk^)`~env+f@Ndz<{EIywGgkr=j7&dL%5w zdE>=J;!_=sKX>z#Wo(MsK9e|IPa^B+>`eX@T@sa*;qy5n6sgp`yX`Qx_rIvp^A1uZ%YXtb{5bmk`x(YQ&VZ} zevJoVZM&X6Gsf+_ay*%EwRKdB;?Sf2-SoqVn&R^{p65y$9Tuot>t1rf_E`4|JhRmO zBJ=R(KNZ7+U9ZUC^% zZxV8^68$X*n9a9o6-sc!gRu*2eqjt%i-CrBy3r19RV4A}*I()A;Nu0^D-ad!AyhBQ z7MO^dzrkk2%Fa<-Qou4`1g3sbQPEzWwH6vmtqMycFy@))L!cyt3L1AlX43`9zJ`!? zZAM1h`Lc{Xg_#co&%nec*l}}q-gI60YgY2w+{l+N)hGI*xK-3wvpLZ_v?}R3q*r4< zi&dl(y_}lQ;@FDoub&(mXnMp;PxFxLdDD2K9((S1F$Gn)cK#JxynhI*!XuZB1u+%` z9rkV>2UwKCemC5-?2X&alW!3BCGPXR`lO*P`%QDtl5CaN={ zxHM+2s1-t+7fNTVI+<$TqRuFBeoP`2DD1aZ#iw~k$a%4Q`15k<{M>hvt>Prz z+30s!`YbS@7znhebG0gUC9$arx$j%uh|06g0tWNz|UoQj?O zq6_cwvUmH}HtRoeR@d-6B3hob0@AUW+eR|l?s30PKaQa0D0Jx_ev`k~ChlWH#F0`5 zO4KDY#=MM?Y8y_|*Sj&hnsu67k;l4aoI3GjHwkD0h+TJ&7y~hxL^Him*rFNVYIa2* z-ep~=vN|0?+GrY650>~budELavZL2(wj9~_MXk=GCEvQWbM6#2HU=BQ*C(blMdQp_ zoVGiwKc?q&U7W<8-edhVy7qgJ2z2=Iho3b|*Vc&fnAFSFD|q$#d$9(3rp{TX_nVfj zJ5^6P`>c092>oipn)|S`Od!PEu<~s3$i2epyzs5Z3GG7IK|R9!>e1fk_l2U(+G(Kb6kSQub$W& ztM)zI*%)}gA&(Pvvi+qckR&D|nChhaF<#V3?Fo_Iu@QPi8|ldS&PJKU$3XSyjn9{B zmW>p+Mq@SuAYSE6|FpmUZ08Y)M+63f;nKx_C&?)(uPd6k(~Hjb4J8#WW%V}xG^np0 zO?2MMIeB6`JV}xLqkpQ$sAW7a|7L}WiOEx^ZK|WJ-}%K`?hn{R^}V^Q7pv*jV?)l! z&Ru(rNV|@G4##cAyH71@c!Y}W7qZm`@Rp_y!`tO?5s-e#go&IAC1_UKv*pln{XN;-In2mNm~dYzBDQR!Q2ef@MpTc3#n-v-yKH82 zi0!$=vZq&PDQ2yI{dhZ_ik3`?9@u9;(ErnBU3Kd5``DZVm(#*#oey^d<9RyAl$~98 zy6v5OA5LH=Imt+>o?jA71jThF=^a2%Vs&)&B=VWRT70UV9ecpTv*XXfij(~_Uv5D1 zS>VZuZ~$vjfh{*7yL6h2+A*47@E19eN8wS`Lp`)71-nSwhpji%OO?b)LUy;7P&8BjH=iiHv6USl3GS!T8?QrC&Re(n_2C z-nd=U(-p;q%bJc=E&Z6z)NLVSdCJ0DY;~rL*`KtXZ$M~+%lJv&kLf3bn$~&V7(IS> z&Io37jn)4E230o=+)f&De2#5lTnN<+&0(guMWHQU zX1KEEvnn=Z(sYFtvFpXHAadWBIok+cShk!7pQiE0cg3Xmem*|Dye&iq|^#6%RFy;Lu13t|dG?YBxhV zgWiFcSaBU45x;LajE$W-NfZhwz?u+Sx^7r{i)ccg#TPHB;@qp?B}J{pRE7L2Cx_Xk zU>54&6~d)Xd{P`~eRCDk`E{?H;5&j`UA`pAFU1NT1kaFXN6?10O1q#@y!TzB+gT!B zX)l}0**Xfd{N2iBof5Xe*=|C)qws!?7=>DVhu7nYU%D5Y?LhvSMV)}F;p*9sV{0R;_d=lO?c&<%Fz5is!T!m z*zQrG)wqdm{M;Wq(GQiSO&nKqxC}cp^saJa-UxrLuGr2K?m9*k%Emq)@4P%eLxM}j z@riA1>an@qMsv~cEvaAPzinbxiljuo-`HY}Of`%T(GIihtL*C??(XoDkK@r)*6NzC z#Ui18#7Qm3+Uie8cg_Sr8NKyIsrF3hR2TB}=gFya_F|0ZWU1A6@NKcFz=~>`GJ*B9 zCdbo5*p+YoaL=&&<0GlN?yT}2pdw|1A8hDT%B zR8aLz5WiQmm5EHzxla*1xADmDwm}TZ&Aho2C$Tn_J;$X^qxi^}$SDRy_F|FM`v+{z zt{aVqoZLN+x=~EOQOHgCRR4%%8#;aea)!$vQ(cTC)ymrDB@+~gH}jf4%2$gI53$mH zdMIyTplqViGB~37J|I4A8B2~x?o)Hio%y*rj`BN|S7UEJw>z`I-#WTlwjmI8+&s%b z;F%U}{&9mt$fH(610(EV3NiSaq8X#rF$9C2Q@6ZqJXP}y2(Q8^74JBm=3_Q7xhX2O zSNihtONV(ki9F5ULpLy##zfVm4_%Y5Gpb1#!&|c1&umIh_n7drvhYu}i%WIM9=Tp) zzwkhgJeEaO)=cYX*SK92W6guuu)I_w-_(NKS;4l{!qU4$qhY%i-%^+bK4s;Xi*`$^ zrK0V;wUJ`|a07kJZt0#i9Z}3Dw_07PS88STSC8{e+BmvF0n|kLSWPEyD{ubHQ{sE` z$;rFCI-TC>8uR9#225!wspxQdHk^K5Ii-~EH1mQgmDmYWyZR12w-K(4xd?Gvzk*b% zgt_*8b}#n5H9g_JfpZrYE8}yR8?mIP=vPpXoZjch)_P89frbCFODHTN7-6a}hHLbS zlH+z^@$>9UxmI=!Ly{GwaVqtqd$nUBWf1g(qIf6Oxvp2l(?XV@bxeS!C$7%&cpaYj ze8~!@0U~0 zc`GB>8-fi#D-RATxsgPwKF@DKmq}pyiVHb4{ZsZ*Rw~8)i%flfdGf8jjoM2~^G{>Q zXt~2H9=g4<^!$zPr^XnW+j0kGaNxw6$JsVukm--APg93!nQ z8rRblnYY>(lg<)6m5jnSo3`*WcL#?Du)= z%{k3VE53qFsV@HfI74WTY4P2BvgxnUjnlDki_#aZCyN8cM~OQ#4W3m#YG#5k+qXug3FViadcE}OH` z@;i}cr?QBawx02y)z?apD@V)kC*jyw$4Nf@*6~`7Dop^%*}GS~{GZAkjuVE{djxEM zXUBc%`C&!ok8*K#?&;D^{c1w}(yt<E{~sdSh--@5vO&GwOgO%NFpKge#p0 zp;V$CFrB*!n2tRNh8Ljw=#`M|x?7bw*j-or&F`~5o2PjpJ%y%8LFM{p%zZDPJ?!{Y z{QQzX5&zpsiStm6-wCd@Dkpo3v#D8h^jSBGUuQi3>mJG@O#R}&>L~TS$&uowTx@2_ z%TRcG7eQT{%f#A|IrHcwU9x(PetXgMTtE@#jg;s7WgX{Tht3PU7H^j)6yYj}*VwHK zo}9JnsL%Au{E`SUOv_8>MNE9~ICWg*fAhM>BRPb)R;@Vf40nHbIb+0;N_eg>ZJw-h z)?ywiPOvcv;ZjaWEuk8f;Nx>zO(h?X;G;(tPO`h{Y;5WK6wMBg#9HdOo>Cw%US~53 zIG?W_NJ#P8l2+T#^Q;VIuf}m(<1=b+ejF`Q94ADsVyjvkKE+s?6~Pt4A#LXrLkp{c zwNLtFy7=;%p6SD_V>vI^^ZsA9JLA(EKd(m*wsW^;@vt+B41|P-%EWwYYjdxNiVpu7CM!zzNUydc`n%HT*!T7lV@p{DRpv%hoQ?EzMSbijekdg=UHwc$rAvz!oVf}l`(RRj zHF*7Mrz7R@6l0X>tB0kJ5o*rMV;En^oQMioHH(V2C2{g6r@{tq^bm%eq_tac?|zw&X&zb>g%- zpVYG@bkJDdv9h#{Iv>5B-2%3iFJvllE~hPKOeJ;(mUh;9OnvUxcFMxa*x2~TFWZ_g4N?BkQ-zPpwHq@CQ*NG-%1W-W(bCU%8YT7r+`wsC zdi1@Mg>hRp#2f$kgE_imOcK)$W;2;Bwg^MnY-?8aC1&j*sg=8TnsETnar0GR!G)+*hz5@tP< zz*>0e>RMmlNb~WXv~=@=(c;P`ysT10czD>~V~m(rrm$2OpF)w0Xed$rGSa;L<)4jA z$+NDZhtqE!h!rVy>}z^b$`t+(w78~0ms0TO5!?(*_0CN79@!kBp!pK|43ET)&p-u`JtjNtZ->;?ke)p)anx~va6D+MPyKafAf^UV(jg3tCKU)N% z>A$aIwdtr1Jlk*W*obgG9Zj*Mh$CM6D2;m;Px-k@TV;69Vrbgd&a3OBD;(!Pm&i3L zD^G&VFMp3P<(%zKsFNjZPEWqBZ}e9l`~Rr=>aeE6^=}MBLL67XbRi1cLG@kG6cmIA3Ka?R52$RB#HuDs& zTa2-ocN7=sVqAMOG?ik0=wLcAi&j>@j%KaBoK(Xvg@U`HW#i7h00lAu zJch}tqDE9ltPj7eq&}y7B6kvx5Hj1ZV~f+8P&fbG4(NafdbXG%hERC^ zpg?v~4K06|dz;NxeqVyevTOA+lPyF|C+xWgVG8N9zCbG&y+#fC(Giy5^dto`)J3Uma zHNY`|;l7lL1LnHDG-n4)Oy2jE=dowILNU!YMq(>bD~KQ^*UaA?Z*%-JZBQQM-aMDX z{va15vN_gTc5K2Hb?C3QPGdB){t9*z0>9+BQG6P?pgPmyV$wT(J}y6mjDi2}3h1ri zuxwN4$?(Bi4;M`%2Sr|_36tRzvLVFhoQgp}`A}EQz@|#`_{0gb`s0qvq2Rw&_zCmT z@zJ4eQ%`5P>9flHrWM77pDpfwS{j-UWz+h^qoU%x1vss@!dq7CC7ziV6eIY}kZV|R z%)|;}9oh&yF0&b1fC3GY`+FF_r3@cV1Vl_o}KPu-Jm;skRt5!l;MSVT@>Y>wc0A_5g3eY!gY!FGk-N<@d zUQz-Ct0j$&r@KVY#T^)j_)V)BWLtbRTD;G`{K^h!85kU%ZM`xx^GCu`PLgl>I&q1g zu56Da;5C7Rn%kQ->7sNn{fy1nN6>l%S7=@ym`?yv4I15ur%`>n^`04-4rZXM#5mph zxh+0^wG=$6#X~XkKP(_Wn<i7n-j2;6(V$6m`(V^eXoZ6v(fFK1b3%qGky;fK2;s-8X9OE>pRZX=mTvUR`T?`0+e zhLfWMZ$07uC#8MIF9Dvl+j1UT%vFxMWbXU6J1B768N`O9ZDemuJ=%Dmo{M&~D^wTTh-s>QIf0$pTBm z@<=tflARBBmKS#4e|!&4XOlkNoZGMl&oEuo8K_WqHW$~0g?f&0<|h;A5G9kHo}%t= zVmQRfG@nuBRItSBJJ=tk$EZ0ToaD_X|Lf^zAMQ@&!r|fVQc0TJo!C}il9Hytg)=bn zZPa^n8{HjG7o4r#Jn3VzlhbSi2sR5x207RGGEJIhc^^i5@=Nih*{&rC?~9WNWV--H z_z;$t>M+S_P?w3i>oVsa{h4Y|2=UNpI=im}OMGHP zz2yqz&{EmL;^=i9&aA0*X`iUww{H&&B(OD_^oWbSj!p%R-#_zoD2#89keBbRCv}OK zeBnVCuIpz(+0V(erOVE^N3h;OC^VQ>mVGqnF+7*b5ixqvc`$rTVPWNXFi}_q+Iu|4 zCu9==4OK=)YXhm1y=`56?M3AUqUSKfNv}WaFM?V_fQy=KO>A5&`QuCGw?2oW_R*-z zi@^4FB~8sFayXaO7!8&>Uq9Tp<5Zyd72TGD#93H`P=b6?^w8gqE{HsZQ+PwmmyBb3 znr-pn5)#w>tWD_ZeOfB!I~)Y&cwS~4+M$s8eAaP&{jK|}emK5p$CE^g zX>Hs2_BWYI?~u7$65$f*0zM7Z)uDgSgKC=DM$}30Bd<(oOdR#lgz73Pj1PqN=Z!;Iznoe9Jxz_wbr}Z^CE-R!iw*Vgh1zPW1XeIhe;Kq(O|#;^DtZ8yysM4 zaE&oCE;FP2gSkI$BfhlteMkwM$B4gp5IM4Zq+r~QTJJ*53}VEpfDzj<|Ah>yoxzz& z9ZJf0lI%y8iC4nF!H`<;0Ut5%=et71CmE*C=jQZcpbp~;!DFX?Wk-fUmf7}#feE-V zQ=lJ&g=avKKbMMngvhL_6l> z;S|M-@^V^8?_B>YRw7x)}3 z(Wd2gm)O%f|6-WS-}%Y@QKWlue!z58nb1*Gf|*WVw5Qvmsd3MFg^0${T`8|8M}jDF zua~MG&7s236LyXob&hzWifZO`zDpgw+$RJQ9@Be--l)ZbSAN8eEC}dv;r{P^!4{mT2Ex>Pf(sdO{0JNx)?@#t#>sk!N#ZyhlAjmrXgz8te?X& z_~Mi58gQN^O2zE7w9=G(37>#HFfh#U7`>3v*Hm=0%A*r}w<{^5eBMarba`Eg@N{{F z{-;k8o<5W%v9towb|l-DUG8Efa>SCqLX6npUN?g>R-g%m-= z-X9hXgL`H$&I}uz#Ldwq(pelUgLN4etsZ8CHri5z5~dX%PGHN zrU)vpxVX4W(?s1iLqQ?};k+Hv^1OrAj;_%|IKJ=Q7w_4-O{QH8JMZV4(2tCv4c;=| zZx;sse13QB?k2qq;pI=&V4DjSblHd>-J+^n9wwz^jUlu7v&BEl0>3%@1*Tf+kEt0A z5zMF8)Tn?2SC_;*P5=^;@Yt>XE<94Y9GG=#ozR;V@m(jTwGePrjv!)r^X7-kboe#s zM`L(j$5}@+!six#>hQTY$moECr_Qo8@M~pwJfY5HiU)j-m9`=B4(>e=B={;!;~=@$Saa zLn*JeIE;o&E$HMWs_uM*l^3{9Ar;nRRfN2<%L0PXRW>)C#hpQX-phS-wbgLDSSjX8 z2m=V%dOFK4T~}9E2)JJ+hbSpTxiRiu+c~XXtrT^G*p#CD+p^Nq!lJ_BvV6qMdTCa% zj8Er&Z?D{JDj0us|2~>9L_mj>QpD?SNiVsNHQ@~|M8GqnNy|Xmwids}$ti+7f0b~+ zMEToIWNKxDR7`3?`L}Nv8Q(GvkFRNIs1KCnPfxHu%yI(YD0P1BFdYiz&aR%LFxS_v zsiU)?%+^Ck`xIaKQOE}M+u;vt`N=;&exhJzS!LD3oOJ^Fujr`gjXv>P6$wN^34J=8 zw=34+J`<3bir;zg?YLyHj+v{5nT@Jn#bNtDV7}Gor*+~|W*SQNwAV84*p%HL{~Gz7 zaI=9&7U2emTwkkdsKmxYzb7XvFpbu567qtMQZe2&%2kd(gHpqh?RbE6z>xIS^q( zqX$WjOk^+_dqVB*39pI($O9ho!W{H`Q~W6W!M4eFL2FpUuj}6GmA(d26!%xX;xaN0 z>sVe1W|lz6YsSeA#kZ#5BBU9*a^>Y$=*#c5hvpZDP_}%h&JGHK-ATsIYAOw@@y8B0 zBs^wDyq7?!gy!uVvzxQvyK-ERXFhn~?&!hk9?coOI`Ln`3607c8)7{KwPo0s(9TB8 zyG~6V2WCIT;r+V#D$KtYEX&N0 z9^)qQ`g{wa&-MsU)@ytyiLt(=Yz7CPI|XXaS3;EN*zuxFmP|&N(-W-%ai`i`(japu zr0G)l-HUk{65_V=cAU5vVZ_*Du$rEJmdMf$fh$=yqbqw zHEwRUyx$XIQo4(HM<^-$*nj@=%V9#?9K=c)U|_F{fp?ZMnx(jnk&g7yOtT;PlNOm- zSwv-MymEDNrfVEhm(O>N(cZZ&rfAJEJyg~o1A zT<~d`!QcOJq@tqJ;@D<>xcz3Y%*WExV|6B*CJO`}1N*X=m%YQ0%QnyXjRcg{GWCh*i*QVlz-W&de-q=cY2xxaH8SF0x=#wgft$9 zHdbFgiig!IoAfCpa%(DAEihF?o9HKmcni+u-AxRDU+^~X5qOy!e)^yWg=#A)ef%iv z^2&YrqQ06flhbJk!k_4d%k@ZRwPNMpPB zH&Aij{er3ZfI&_U&KMy~XQHQ91k-7q==}Uy`y5T*=iZ+;0&E^TsQjY=u&a?Ng8tQy z!LtqH^E$m-?0=(t*>@d~+RTgVN=DCsdUL#|&zRk=3Y1Os$C%ST5A?MJ3Y#uwYLP7SL&q4xx^q4r6J)UN|5^Iay})bi43%H|!lpf&o15VJ zWtjBl<^3jXL3y!P;y@DoT)Q4wnm5AnBmPaFFw|!M$K3F|XV9qku_*hIr#Sdarq4%4 zM&LL0@*h5@zdzfYX49WaW-|NpjPCEh-Mx~zJ5@OydeM{+p3bYLtN1RasJ`BC>e2y+ zTu3g%E#XnwB|Ll$1?oH$#b@edO53Ou*PfP=}iVmHErklCp7clyPN~@;&!>a({y%p_-G^i_?YJf z5rml7&Czyadxxrcj&XL4jNkLO<-ys>!a;SACBPwopgxFTp$N%%Qaxc*|-Y&&+V&Re%8oFnX4%jNBB ziMwBd3V*!5(WsOHqbIWS8jS(!tfVHkcd`~onsxa|Kvt4hq4ZH%4yt?Oe^|hgldCc` znS^XAw)1c2esO``y5)0I3uZ6Gx0?vjLwnub0dj+na%TGGy&IoNe*h-R% z+7be1c7bSc%ki`uI}>F;Ap27_7)<(g;-cFD2ocsGS!cB-pO#~$oic&<>bgz*S%)Wi zp^6aWRJjw4MLSO>0K1}0@V|wU@P1Gw)6`IDSiejc3js%oh4)2iM!D@|cTJYSmoBX= zYt~*p5Mp+Iwkx7E?z23Zu?Ol>KsI1nY_J*`8ZqfM22cJa5it!UaEaD?t`^qj6B+ho zQ(K3}P`e#aF^1!NO^X2+?3vW|sUZ!>nJ_*#5ATaE575C2zdL4n5@xjF|98i*vw<%E z)jORTZ4%JsuP+6%RKJEPkC;(3SP!3uhFUIAgcVde9ML0XSITm7s<-Pnk`oF`Undo^ z7;s4IXmV5lvI*N~XQpNEVPtW0mVMXZynKq_EsAbQbXcm(epB{wL_R~(tr{()WDf4( zv%Bp=tgY44F}yHnn@B{^p$!+Gr!4%XvbnXD-5=6or#LH;Y~WWG9{zOolb1ug#33IE zuluH>Bl53C2-`iDiGyM3NK8h{iD68J#HGdYI47YpeGzLG?S`C&@}4(s6GCA4c*lCj130RYSVrC{raK zo1ae$4Rkb$if9o!s1Pt&8Nyo5b@cfi&^#%?&uf3a0UXKTR|iUT)2(e;Le9&toX8YZc>N+ z&J9SA7nx=~Bb@Z|WLG|d8BiM&>K#iNgoW7q}Md%^wCwbazdqL|EIalo4 zgk@HFE)fJH_)@{5m}dP&XL|?c;BXW@JY<$ZKe;eolN{%D>3`Mob+xw8j3`UY2Wn$N zbGbd^xq$c3_&~dHk3r|aqv71p^f=>dM4cLjiwWutrc{@m%aX>=U7*KX&kxe*Rt?B! zGuVuYK%A?NM8%{uTHHnsE=o{y+D!MlD^?jd7hdi)n*-$TjFgwLaicS6J)Syzb*ntY zGElquX^-|}>?c3oy+ix(P|m=sS)kGZ>p$Om5jYHMrPNoIUT;rkNJ1o8eEA{cg02bc zuP8IK?Z9UzPGmTSv-9Ym?h)bDo_DAq-}wW19E43DH%v6qB?s@t(QE5NdLFOK6Dgg0 zo{3=~KBh;tRL@xD7`1yX@pqUcYEGE@v+gJ39s+eP^4hfDT(*lj+$tQ zF4^w!`?Tkcvkhkm^UULg<&Py+yr{8GNH6X3?jrkIOJSw8ikjNqFl9<&(R{rV4I)GG zlm74RzjqL;ChQ}6*kG>e_YacN=BRHB`q`lj49H&OeNiHicm>Eu;nig|02oiZ?A&^s zy)))Ys!Heff-CetWZZIeutNbHBP$G;0S2l=>05k;6*kwqs$hT8=2n(8hC{<{rqcjE zc3fq9i1g_oGj*3qAML@HVB1^YSP*wfBlWoPs7f4v{JbfbHqkO%3_vJxlRDQMr zVE1!Mm-W6*U~YgFH=~pKy6M8{^vUra(o(Ab8L^q`CKBD4UXm?4(FV~?lG2$|Sjb~^ zs184eeZT}xGs}6;lL{DakpfP|LiDlOLx+knfa`b9%Fz=DQ3)ClJ1s=13vMlc8jsP4 z0vmkXast&of}{RRvJhl);TqYxI7B%`hgAJq#-vPS0O?pA@+p+qZeHEoX){Fa&N~9g zBE|jHm;+w^x3PEDBSY~-9+aOVcfbgg#)Imb+q2VRBmpFWjN7xaw$%Efs0P^qu&g;HZmn$$TJ17rNDh^++I#JlYol3Z#utIQ%0 z9G$RO@3E^EjEIB~rQ)BDobV8D>1623`8;&#-++-)F%1oGBYQ#heU+exj03S+Jl8ur zFvHp9&BY%OWsMIXM5p|8BlVDN=K+{Rp}q2FUxpXg%Fouxr0cu7`x0+=6ojrTolnxW zysz9&XrP{#Q^!0u&6f*v_c~tloo#_M2EC!H#()PJT3XCnPl=NjtN<^~sTy~9sa;P& zU@9)=*9rV{{8o+f#yzQ*2T`-8&iwM87S8y|o?Y z5f69%_6C1nn*5=!zt*mvrd}fP3&572)lLx&0h*$P%rBQXskk*M+@$zZa@{=JZf^P~ zcaXA1V-GmE@UhBSUqtBY>i#Tp9cZgtz@OGhAxQ`UQ3p8{vT|XGMpj3U9>uCGR}bF5 z8x17bJfxs~k`#IW^$O5QX_HSK4vGqX?Edn5t(tguEKug6v~6y2sJG|W4vB zG3i9aMHE|Q<;uL+jW~7x>YhIK3F!zT{;Ek-{63H;+#=S#N6M*D@T5=dzw=^(1{uZS zeHu&2)lK8h^G&V_#dy_{-I{QXLnTKlGxHU#?B2cCjC&%Wx znQMMCWuf9=C=bDjq^1;smF8Aux2(JCy=r3pky~6L`Yv`AYI+`qff%^#4Omjxs$!NCuyi`sZU3A1$vcRwoR(Cw-^{0-^_QN}S6 zZBS6Ni^cj70?2PF%q@n3k4wSa?jE1kmKt@~wSmCp#J!eFJ{vf_5KnI1=^lQA^^(tU zNGi8OT5Nk7FWo6Yz+2W+qW^r*DNJSMrJ$v6$w@B`Ts3prglEAAm6r%n!E&;5{!YIo zg`c}W@Y1-aB6={YxRQWev)<&kLi*q$+|QJv_IAL|IE2#Rv!0aHTQgP*EQcZWq)kXp zSk?iFh0@yv*DfFwLe_uZ+}x8%`X-3$RUavG$#iZxcDM&u@_Htdm{cBcwz^8txd|&_ zusLMRe0;F63RDXZF;*928Py{hhd+A@s7IG5IvB%^J`L>Secjd5Pajx!h)?hxHrwta zfoyLqjD7Og{?AAGOmNo#KuZP1#nUe5_p5RxL<2|C017Ula25X}FVBcyTGH7GuXqfC z2l<^+AN0Tz_}0?dt?yf?@STzA^-AxpqM+B^Lo_QnUE4cWn34lSL))X2`tv_;=dzdb z`y)ts5_mqv@fW`U=&lTRRW&oT_B^DL#;CO}wk%tOsR$XZNiHL|J335TC$0)`3H_k9zQr%xmCR7!IAMB0mrhV3bw_M_A zgoVJhDvqc7{nB4vxSu^X$*VZt4`L20DJv7Aydem<@ASX>vE)A3GkoDGp9o130N2~v zdk{SstxNI~a&~sQ6+L&kPG>Ch7h9yjO)xbxmrl#>9bb@xFmbxZIVm|S*%zVM?#KKb zZ5y1L`sy}>0>v{+&ollK=79!EGkp7_iD+vMJ9Abntcte{uW3%AKB}p8Ozkk_JP)Jl z55|N|ma9~z!ufy?!~pbU5dQW*Yr~Nm*Gli5YG8a;s)XXJ6A)%|{-pU!W`X81*Hb76nyI*ouKPm!M06sJN26 z+PG>mFe1c+ku_R_mX$(Os~G+Hru8FLh>XEH^^>GpW zU`tc=_|UM<)H|5~Rxsz)@#x7G!|5teYkaxkT3zHYUUDd0!1~V{8!(lYG^pF-tzN9d zCg$8j_E>DN;8*MAx2>t$_$iy@z+V06H|ZKrZ)o7|LX<@|w#&l)hXrIVEM7SH-n~-a zVeAWs#eso#-{VT^bNs5w^j_(#%s^h9jg74|pZ|P-F`cK1&&jDjuJ5|f{O(4lRJ*B7 zJEut?y^M0w>epB*Xc>?MgeDcNFMaj|T`xAVhrUZ5 zB=ENE(^w*)?Mr|;QDv}lTk0Y#*#$^%4NLqU)ZDu~d5HPwJMUoh7kFAzvJ5}vvSyu~ zhcF@el)Prq-ASfebXGPFAQg87(ygxK`*-mpo|NXu1^UGNhM=rWEX0z;<>bP?<+R$^ zWQugid$`s2Y(mx4>MU;cj6(pRmvLW%bi4=%UFOekA^hko@ATyR$ou(}A&S<*Kdt9_kpE0cbFMcyB#a zBc+qRV#$-z;^3^uIBP&>l8m%Fpc z$y<6_-O6n>HR?I~LZl4t&c$!KV?e{T*fa77+Cxq|c=PFE%i8LTnP5kTx4Fb-u% zKWhWpz4JIHA>a+#^sD%ValC^Es%%hieETMDB$j(6v4KZKi<@q>0b~lmNK^=m5sYgF z>i}rI()PRg{KaS?+2$6MoZW{&W1iXsmfyoi$FWmn5E4nvh~s0+!W}Tc9Sg1_9>K*- z4EO*mBQyCVn2BLv&H4G5O&sjan;0dBRUk(p_kJ3pkxH^n(fTz3%+k?4m`-0=Ob;}( zf#)31cx9H*%B7&_2A#9{d{^4+%==oDF`|W z`h=w!xy;$S2`1*lTM^1brk1a@o1K>sZY_)C)TBaAs>&i&d(Rp#L$)=ji zU}%I55XC9PL}v?Zy#iQ6nLJsQ>jtgyQZ4CaUrNjQp`T(YqkP_70}c+3q)lT(qxld8 zb%a?HbN|%&c&V99vuIL!3HId-5xP8!XpXGrYQKI#GAZBoJIu{YBxvncIxwTzW zsojA!rFDz2Ogo>jkSxMnTw$>ejrEckKch0w2%jF$IlsV7R=t8nCaXT4a? zzJ8{Jx=)yHnn`1gwWVe}pW3TJjgEMj}=5eXL zmBr%%_AbGZ4=QeF^+%tQLbIt29 zOb1kYLO{kk7;Vy@(v*-IEnxsnQUjVA-iax|j%aEo9=SC~MOAKbS+mLLMdec(8a%*W zr@ByQ?0Yibck8fZuJfOR-_nakT>^Poz4%z_z<*_Q za(r-lwQ+et=dIHNi=%rF!qMxdujuUj=92u(+J^Rd6kxlJZkKVPt&0 zdj-Tm9X-RTqq^!Js~z@U8TSlB&|3`SzUI1%`M1sD(Y*xoq*7(n3`D->M3_3;R~x9* zJ>1CKuXp;-6F0SymNqlR&+i#$;llU3e-Rn?t^^KGWABYxvG6;3DVN9&93WrOs)g8P zQ2K*P#yw*~Y7g^&MCI1ja#FJK@oq{OQhZbtBzm!2lkZ$8eGnkhc_VZh{o^ z#sjQJA$}hI{)vUDB{fNqjt`oc-E<&TROwL&7}F)akp;N$ip(z|+4mH72R4kIc3Wfj z;;#Ws*{}~3QF3USPyavF2HDqts*UZtV*?LH0G2rCQeV58e#9Ia`uOs6Q$A_6^MFlE z?Dpf-;K0ZW-h%XRkMiT2As%$E7Aj0D)FK{!3BvngQf~KgdtgwRs{dvd5ME|ayfT^c zG||IpH}>j^%yo3&4^D|pIXA#;nQ@zJHSJVVWBK&=nh!$5&_CP-U9dHyJaVBFV8@4B zfMPNoh$+E~#Z5{E)scb(j{B<;xjnLloY>*9p{yYOmhWo;)O%6)KBboh`-Bi;IpN?? zfQJ=aP+F=lKVxnRe<&@qd;^acHH~POb!c%#j9LFQCosy!X2^};Nhl^i8UWkg_$0{3!9=q(e zg;?7=IB%|xCP2mQT*wDNZWjoMyvDhPIypIoff2uq&;ap%RAZyNdm}ret(|iY0*+ZN z12p%`Knp`hPrn!yzCuXALI~y`p=ERd=DojN+qm}rxjn5NsHq1`V^vg?Z_g^2sks5& z^=Ba(?8OAMYD}T4<4d3h@E-^KSdI1Q(|h+)=I#{PUhev)tCjS0_eC}bJwP1;+}nM> zbNH=#dpHUIvcI5`!hmN&LbdZR&MbcRhXlD5$HHx=IvAo{Cp*t4>)B7+OZbo{oqASQ z3wzUM!s%(8W88zyUO!pr(A>N=TJ+kt`w4IBX{k7L(pBBsX2uWr-GLHh^;+J{Ld zi*%IP@i2 zFAQkS&zzElE;soK6=6Auo4L!=+g05ryX0yoKm`MAD&gj4W-qQpTCV5ifb}O!`p>dJ z?GWDJ8mpUmn;Q>&G}(;-+>_J6;bg-;LG5#m4(AgabmP{ndoS<^{93ut*#F@sx|=X<#C%CLFY~SYnMHbVJHsHj&#QJ?!I1w znj8~Dyw9tmJ{(M17abQ^YR?lo4CI+LcO2Ca>KLIgF6lar>8Vkb5=8^+NMrG zi7y!+53N5u6Z*%g=Mx@oYd^5Cz(TEkA?u8Cp9hy6KALIF#r%!q*_^Nafo8>ffWPw3 z=zx}5m<Qh}K=UfpN~~rpqI>MF6wf33O@m z$$}sglea7*5Uu(ZI93g>LzLA=#K%ZC7WjziWbUGc9llwp7>P(r4jHzHhQ3B)=#QWq zi-{G;R3qq5|Fu?Z{e&FNRFHFw04Ua`Yt~*~M~mY8Qh;!2tHmo!RR95gp#C(72MOcK z3)QOfPTu!B02W&pOTFQ`_rwn1*aD4z6-1;Fg+n@11}f_Upko$%#j^4{(V?}mLKYyU z?b*H|VCb{$Ai^QP3~?pdN4Y!ak-*a%k0?7!h!j~q9=EtDb3u{Jxm#7Hks%t38o~G} zP`olB{%l{!j9dSfa@M_=Mghx|lu*(ADRyumeP=B4czC@B^lS?^3JUJFd$V=AyE9!O zXrGm^*QX5*>m&KoIPai+NK>PZNYZBG=d1gSCiz0Z^3V61mZ)J??Ue0<;ulj<8EA+f zjH&q1N+l)8=?lFFrq)IZ-98{vXY_ov7$2y84LJZU0b)Z2WFmH7fjYmBGrUe>1 z=SfKSwQC1G-V@J|6MB{e|~^bBqRcMtEaOV_Pk9E+}Ol3bpR*0G9VRh z$ZGWkC;ToTUchSq7;>}f;^IVsW)%+ly|9*t!fewOmoK++hU~HffC)?XIQ~Q}9WJys z?2mk>2rha-Y1i$|Mad}w3z?8Q-BfigHIKy+H{(md{O43ykpb76+d85Z@VD#!WMd&d z3ApL|_2uOS~@fnHK)0` zbtcpsj`ZYp1KNhCXoot3wEx2b0KGX@1~C0D1;oa_EAX*n8gOUy7Q+ho9Y4ahKc|+Q zbY|XzG*j-Jo)R;8AwXNWRH1A#tqYM!!+zww6&r+!*#%1OtoE4d$9Jtry>IW?X65&s zZEedj-4`a(^>HJ)V@4f8{8y)v761%!L>fw~rZtt-wyU=5EqQs=uijoLE-rz@0)Qoe zgPU6X8z-kKkN~BmZf_`9X9WVHecELg8rG5;4H8eG!sEtfde%mItBcziA9P?}TU%pB zLP4(H@`xh}3#zO$JC#)0g^bdr@14~qS>b0v)dOYf03Dc2hCBtho|$P;h?9yk*8b7p z*ie+Vl$V#lY>}B+V%J-Z>&bcjXH@A-OGxw$>2WZG=bOTB3*=aVDk*1b#-9M(aKcF4}k^ezsH3ceWA7L{Iyck)fod1pfu` zV9kZAhpUtA(#C$^OuYD>JlE<{3_B!11*jtSt9xb9cX#_r`{cu))MCTsfF>`3h$qPe zLru?r7|?4+ENUAxZ|G>sVB0}JY17c~V{p#-6Cy3gtKM^mx_F)3YX=Cdw0DW87(O8a zra>zv#t;yZnkR%mW%$J#NQXW%rUL1sxA!|E6%~90MTS4va$uo=gV}kH&mJ`+)^)o7 zz4Wikgbc~8V)^yDxx*LzL|0SBOZgL$32qGWM#u-haX z3lsm5_2vQ6OSCuFd7|ZGVOfJU9-j1nlK32#9n5 z*?z##D%G+FT0lQF-E`Jcn=(o4NutFEn!&-EjAk14R}--Y3f01E850LLsOC3OPYnwzWOdKVl`ME^D9ex}x_wGT_tVq>i& zWV{KY#v7jVl)ACa2XH32x31TQIV?r+aE%SO)bOAZTGlp4feqqx7JwWxUvJogDQtA) zYuJIBCTP*Lx|g=_Z4hdg^{rga9_d0J zNXj{=RH<9q?b?g`p1|+1+xqU>qlMc%Fh#0;-C4bQ*@TgA;wL4|#>z^N!MDfT4CdMh zw*cm1s{tDe&h%h7w?%t$t42co_o~C6_uQjTGiz((oZf)%>7kc^POHY}-wSk&Hr1o- zs)r=#@EMlbfd{q_swe+eHJY3q=jtIaD$yCOnLdwwjs?Hf@Sf@nN}8JW&=!$8w8Sk~ zRv)J>g25J}VuE0rEi4Ci!6{Fqr@8xo3rVAvLu?vwHnl6S>hSY(JHpvFqRHE&llZNd z1NK^G1J9ePw=u(Bu1Hi+z*G69@N-Ozz4-UE{O3e@?2jH9`!tpVsf^)#=F*-IKcYG7 z{bmWMXEP(K-}f^JP=+)}MiAik0bmMtal{m!x*;K@`b_4i#`>L(6) z>st*?4a{1LtAD0X(p3zR3nN8W++_Y|=U5OGtaGhcbhcp`hQdI>h=&A58 zf?>I@`?u%HjU-$p)$ZN%GR_79n2)Z^E%rx!UaLCm?h~W$f&Qh9X3o*h`61xZA-K|Y z5vi#qARl+B<82i2+F4EVnh&RpDyf!{70IFH$lTC#L{%D^m$l%WF6m4v2Rr*yFuP*6 zy{dKyNLdOyY+8;-CEb#t514N~h%C@{wt-xFbNU30q`?JA!L|GdD_GxVb_Il# zOT2{Z9#$EW;-KalxcQ2Ti(>`c4w+vCoeY`Y_D-KRvzk5L(tTUF0j1ZZy?Aqs?q1(C zF|83FBPb>5iKL%>iJKq`;rcr1cDzscI@+s|3U~;P4v(m~?2%vu`r__|D;J6Fyitm_ z!n{R#xUQ#Qgdl;=+Q7T(oH14F=yiCfIm>ffQf>OhZBz1Fm}AX{t6#5&#AlnBRL2r` zgM;HB7(Awxy2q^R6O+0-t-1YYD(+D84O~2l27Qq(l>kF z0A)~JhqGh4pX35X-3?MY1FWkHvr%5vf;ywiv|YW~*J~QQcAGPxhtddRuO;TQXRfOM zd42B3M0h>$XT1|HKv`=!&#=vDjV$jhf0@J)Vh%N4TQ0rwy65GdbyK}on|cXNaabxJl(f8`=41k0P>0&=vWqM`<3&JLCnZy!O2Mu8vl4^Ym8XoB+tJltN*i(@Y6 zCJ_;}Cdov8$D$E}6`u+<0A!yWyM9{%fFKHS{N>HNreiz_P5O9KYmF6F(&V($O78@D zwl2Z6tnpO9eQ4wX`8t?o`kJVQ$U{MKtW}d9i1D z`=Bhqj1wGf9TLFe_(cUA*ILeDQ&QZr@`XH zf?C=9;rgHVnrX(UMFv;SS7gzVk*^fLx0F10s2J?%(yUh=WPueu(Ow<9w`&TSPQAfn zXIVVZ99RI5*T04Ed9A;Fij{-G0E9!{!)#cl>=HO9%FSo%FVn?wzo(ZQZnV=eJTN^=97*;wredoFn?vq_ZP zbRq-5^q}4-vaJm>3$sCOC$KpZ!W`EO+dBXff;1Z_e2d?~oUPuCl-(b^RMt?~SU(US z^LIUe6V$+6;G;11*z&)LWhsfumuI{3fJNPD%Ho0f0f9xl=e35YIxbyEj@eR^GBgsw z3Be`DEAwSB72fkt?>82!cort%bLC8C(OB{vQD(wO#@8l4w2{9ZU?XTGR_OikF`jReGCBGlDhJfiB zvv-M01t$?M%N^vhbrqE{?sQ?Je)qnJnf9q`Y01QF&elOQ3**v48xl+}>jy>D``=Y2 z+jI4Swn@X+WIlyeH-IKrG&?B?GE1ZJZu2kDF&?cvoWM2MX)iqgrt4%N#bvu76DWC( z9f#hSFY1}*RriWHZB6-G1Gz}kDVbi4C&!tWvsJK7>kTCmrtL3x|(+41ht#Ua03uvVRSm#Mbk z*X|OJek+vrclA(`90xx-I@=M|4EnqB-!hmm4k=(LlBD+wRx0W0%1=E?`o(y&5YLm& zXjn6BB^DuzYr6G6kN=SgEi(bO*i6ORAM&lbv9Zy}v53_)_B{iRIk*GVF%^z%!T8-5 zlozC8`>Cq`bm@~xPw>RyYk7GaYgmw%($IwRW9`Vedu(mrj`J=0xUXD0-M-$ zY(hUI_IWJeJXo~yT3DoRj*Wm$5%z|5XTD_-Hi7El5dFl~FM&CL#7XI6MI<02PO4Cy27?W+0`kT4PKB|i(&8pG3D8y4rzvMM8IYHk zGBA==PRh5pHk*tF25_wSQc`~V!=cd$*fz=7=Nh0e9ZCTQu9>^5kl+_;MZ4@(B-519s`}@q7cyykd8F(o#rW zk@U8&wvMvMGqsPu8K24EeE+T*=p8gz_SesYLn27zaIpCogIg&9aWaHtL$)f`ruV+r z|08UKLw>AnHJ|la91+mtF%CgXczE1ylW&S(!#i4dHf2u$C5q1=D1Wi{`mE_}F;T$m zeRoQ2!3G%>|6HxZZApC-w!BSj{5ZXV0VtXnb;D+B&Lo>%IcZe%6f4jvn?CNgcSQy! zlWtor&x^4d^!777dy+p&Smvwok%H|LSoB6U>zkY(F4T$w%QL(J!dXmyXT9yu=lh}c zKP&)5BZz{OPX2P7;%}BqGtFKKS9rcBCyr7Evpz35mHFkS5jWmqzGqpNv<@s(JL#=Y z>=yY%>V$=?7^oiq026Zm?hz2Ea)BL&-M_-@I-}dXYgIfz0&RxPezVp>3HSwKudt)ufpIt|^N3ycCY zW^FD!n0i&YPs_^Gyv+hFxU6la8bgR)cB`q;osz^f*{M63g+0b5=BND2LOC0R_xJYo z;^$SY=IO8P(NN`fduorp(Zq5nj5R0D$t)GT4vQDOSkzK2_Xh2$JJ1$zyH8+|w#-Q3 zm#}ZxFRYLHkOh#gLQ(L6A45?Twspz=WT*sUhD<%@8 z!5G~NaM<2B9L+CZ6vB!?Gzs<@z53q@DO9$C6`dqo+=1;5kQ79g0S#5mnUN+E?E?-_RTp+`Qzm+y_YZ@zbGEpVsla7uMwoQ$=vCB3K(;tG;I_l~;A~Ar3bPD$# zjNkSDKWqKgy`FWwdDeP-!D5^l&U4N=&g0m}-rw(MdwE?pco6{Pf9=|1oe7pzD(m)TY`rZ`{&hmCv`c-O`e%K4gKY|!_UH-V#$&7%a zi~>zNGcM3@gfUx)&jqeFr;AEl1Wa6PJ6a<>+ZNl5|0q;rO(LKSmJad{J!4R`>NAjP z9C?V87Kx6OZA--Y`Pv3I(Ke_IF2vrp|f2>Wtqam-*BDPxw9|?38bVx5Gl&86Nlc zUPh`og9H~M#T!^xUdh6e&Xd6+K$u*a@Xg0VOrfj*GSN`XRRE)mS-mq*G^V%=i{Rp1 z=`M=qa%H0K@HdC1qLC3HAB`={s;E{o(z;)=@+A93zUssNP03}w!CV=cTH)<-j*23c zL22hqL0x0VkHvl}n~OgvvSja7%O?om;_7@YMr1^%(Rm^MO{Q@dE78GFo#D=Y;1=Zh zt~`r3=pVW`9LBItvTr(}EaS!`_12QZ&%;CB@nxG3z z(;ZLbN+5&7x0o5F+;#Hd!_zHwEtv}`Z_ePJ6%V^##!{EDVrh3!+e^Ok;=<&^iv*+O zdXk1PR9GFXEpxR(Zl*+5{Ychas>8x2Lw+)}u#m3hu|Y6^@a|}2=p)pGg*yYqiBc;o zH6a($0zC#D-;nW&Lp4FM02;*4kXy0S_K7$8wuQNaH`T0RRI6Cr*(AU+aUf#o^bB*k zqpwQb*KV$5c5-iu<}psZ8O=4*&PUnB#j-A{P2P*Ix2L2W%l9SqYLI)~LYbxj4U$g8 zgdb9li&|Q@5G zFt)vA_v)o^UGsLA1P92DlY|U=)ha*UJIEB7Xp5>~)mAsVvz07tjqOz<5#))aEPGL$ zcui6a>*$l3w1B3frEyzt|CXBPR&Od<q0ln3+iVU_ot7OroI-s%eolAU2+Y-d6bj2RJ(}ht;+3tv)=ak;w4l@ z`Sw;;-DEY<{fVge`x_n64uJF{>NY-ccMdP+ANUv%QQr0*r21^G1R^aOxQn7rf0k(K z2CXm)KBKBvR%SXm7A?@nf?tqu8OJG}r#}0`iLC+O3^>uUUnV9~VgBM834Rb;@tKz5 zG9j_LzPsZ*GohR1~2yYlqGMA*c>~!(_#7!n_CNv@iz94L`Wo7ZTD^mo(L5 z#U%Gg%Vu4@U2b!4NFphZ+Xk0TJt9`k{KpTGgt&k%X+V~ehI>97N`-K|C@+8g%J?z; zuc+DCwA0IvY7$D8M%oP*NUl9b4E?k5%mE48X#HaCcbHPtoOj*A)4+pl!oVOaCiqQ@ z5ofv~D1Tw#R>*8;Wg=?ZHL;$aL45AJ4jvURIH%PQW3aDue>IQKNo`PmDi_FgkOIjv zqtDryZxvZ5!kz%<>%kpfUX>N}4Xdvj0Ma#tHae~8hI!~XxE?V;7u@+WzmZS}5@V=k zP-J*Rmfa%Z*h@i-UhYm`U*8>t0~GLy5<qh!4)w44ht@@HZJS`EOyKbl65_V@ zW$tT2bJ*aPrBWgJRZ4c2^&@5F(ZLSi zhn4!W#^@COM<~n3BjetyX8pmMvHH}|@?BN&*w$|?j^C-aPqWZ^<-FNz*Ag<`XKHI< z8zf^DSDBzWVMiB6K|@V-1_>aNlW|h3t5`UsTxrS424;G^hVrgm%uwb%x>xt(atmUN78z%6ewX#J`Qk!baC59DFfTZPaKzNFQzJ4W- z2JUyiwV6Zn=^=@1F&FTT^N-nXW%MCdP?nY0NX0K{MJvDkP>!8A954P<+Vy_?!LIZSsC0 zNYjh6H0E@mve}+r$T#f`VC4V#H5pnpTv(7eAq)m(iHoi9 zffIP>T$K22>PK!hBtjlVo}=^bPs7XsT?^YMrBm;Hg8_G`8O&>NXLBxeylIGIHs*$c zVyMBwBU_7C!8L~ty9+`JdYJ&naQ^wz8)I_I7d(D3UKmF&$D~cq$8@Zk&gPl*#dWta zd-Nx$)xRbL{rxP*8~t}nE}4>UPkxco7Wu7T42ErUdK7ZCZq6$o+asf(>{v~XPbrY+ zs*f3;XOT&G%1WkvPz%V!gRhf2ferqawuNRR3)1voTvhN%tQ@xA9`KRy>TfGaDZGmg z3=In^clm=D{^sZ&o$K%of&_}&A~=x{cPlWQ$777- zvk?WKu*ZFDNCf=W9-22sCRULYtijzwbU4IjxQ&oH9UZD>KhgCp!b^-#)>70F==B9c zUPPV=lJ;E#m&fn?6zUO7e6L`I-x|i9{4#^3bx&VRL}YH-h$AFNv7vk!GIs^!@tpNv zzk!%9u#*Wa;1FgLgmclP8Ho7HJdk`(NI-`$iw3RM@=mcrj>-4zt3NEJZeKNUhhvn%0QI%+SpY>#l z+Z2a+E*aXCkH|t(3Tva2O!Su+RlQwWRjXVAH`0=jAC{aw=PDpxmKew1^-8oj3||YK zOzyQHWiO2dq=*A4kO`@c-{19l9TT&JG^-Lvl-~PYd}|LcFW)s{tdWa3CR(@?0ES$xN?0*N=MC%F<&rHT{Z;%fA%w_=neW`Xi{ z=BA7uV~5WV?(f7V3bV}yhQ$thq*CWE-XFKu^UIMGzN(d2_$JeSa7}i!;_z+t$)?p5 z)}a*HcFE$YAptX?-IZp!G*bMSL6~dk-e(b#i4QhhpKpr6SYGO;b0QOs;NdNkCS` zu{f%*CLirj%+EmVcE{$wXlvp1UDuMM!5#`7QvaVP*xoe|@p|*c=0S*%;`iR z{6ss;geuXt06;GZwv~ZWhi|$p7}q+bBmi0^??8bKxDB~;r-$-Pp>CfFx14mmA9maN*fp& zR&TX^XDtjE_oaNp@5ExZOT?HrWI#?cIQT)}@Y7amldGxM8^JSpGrFv<20w}*o3f`h zy~S#!!RxlVYAGuTb*P&xGFKyJclLJ7=ftg=82KE=2Oc#?poCuK>H4By%)Ei6N)i`6 z(=z8|QXZ`P7?pMZ>ZWZ(Y!}<>(}B%5!}r8=;&M2QW|Wp|_D;tRb6W56b+sL19=jol zQ$nHUTJQX&n`#Gk7A+wwTAt99djSbab-gpgN9VxZe7*`arWS*odRw zZ15(VId12S0TaFhj{`qI#R=FgA03LxX+j$_N*nGQ9QdiA6?5D;+Yh|8i}4m&YI->U z$1cFK!|J@9A@_7U?A=QalTjN8_zq*@KRa!K^Aai*6($YUHFjh{PPh@GII~x;UB-iq z%NWbNL2u`Vl5KOWj~9?-f9qXQi?4!KUaps|>}cL%GC0V0YLWw#Nq}RzS)42S^2@#R z-QgwUVVP!e7={{oXg<5&C12D`n+w&ZF0r$6TFz@MtRF?@tAL@8gv(Oai=}$_=?HI5VHOEfFfJy+n7w*c^aLyYN zw(ZYzYwKKJ2;(qtI!t>QRK@zU*P)ry-|{*waka1X zLqoneMPb5(xpD$8d$c8V-oI^bp07NfUF3^N&<(vIlgp%`LkZ^4$MAbT8^cyC1aDzr3P~?-=|?H|j{2aWpqZ%|^bA@~_Nj-*$#gnCrLDoWP)K&ok{A$D5LP{tkqHDu zML>Y+Zc>YY+IUCDhmUPT1C6h#vx~ErRwjG)U(_aiyHf4~%{PJdo%V@kIwLbZ;fyb7 zYRWTN*-rPi4+7(ZAN>yGRwu3m2As$87LDaOx4!mn;R7@t3xUp#Au(y6y7A76i%YyY zbzF<9^PDeQ7LGu8`J^pDehr<{il1g|`;DL_=Ks)5IN zw3s3gbpu24&Kq-Q;E*HBwfz%SOgc{Es-qyiEz~mH*w{#F0(<`&mN&+xDS%v?^oAzh zzzwWhL;u*}KB*WS5iztda{M@=LmsrsP<9|j+OY4AP;iaDxo}bV!rV9ut+-^UB)qo> z2B|#{LziaUZ`M0PKnt$&9wfiNtaGS7ek58=7W+0b6q*YuD@ajXx*i#MuBBCcuC90R zp3K#n>SKtnH2b{2&szTq1A_#P9lj&RdubVO`fHzudGa~!Z`k_LE1#VDd;Q^YdK`Rx z|C4~r4h?)UuUmX*-hXFf!-glOI0dhH>WB@g$CD-A7M%6oW7(N2SJB)xyR$ zjLR9zfZ%s-{pe|dYro5WN00vX=0Gp;mKgsToKu z!hY{IFA)Y`h~7yapeg)PqcZv#8aDxOOX{O}9h24_#!D?9e}^tkMOVsrN*=h*>7j+T8+TDqlk^vBr;=<2kK~z&`p%EGfC42(s+EH^ zw@KeG`po9y1bXBo~zGWOYe;Y9DeEgQyH1P*_S2fIKmamT=#eI8`@yuJYCZfsfC_QZBwgs ziq=BsC7v$hM#A^d-CRVGtfTBWKl@`0g=VX5oeWy{RkF2QARg&8`{r{W?qT>Ra6jm6 zy&nNzT;_s^jSXlcgRvzmJ|2LA#yq7R9eJI=d?7O{?ZX!)Hl}X_1?B19=JnlOGY9TA zJKXLp&b+7Cp~pC3L(IAy_f~I6NXV5o`ys`V{+4c2`}WIB9o^H_x;8f1kuNr$~B#0lGp~ zwlaQ=JFa*_>(c~VJo`uR@7 z2ln#1xkV+eMzX~=tf+)nj%&4GtgrvUxpgVqkND-It3tNyZXd$-QgRFL!u3NbWq5mI z`9&KLesON~=FwnT%3$M=QI`peq|2oNq%I~+YTK83jH`hygC>^-I7ipEEm`o*)e6GH z&uy_GxrTn&M;(`KZhS36D}lQoQ&Lj!?Z7>Yz6EzXkm|1N6l%;*YXMf&Hs_-Hhu%)@ z!>s*Vm0slIYj@fexNZ&`rB{!UM8L-#RSF*?8OpJfGJ{%=#-y>kWjM^<5YU7@_e zTsWFh`7*<mpjrE-`2|{4$YW!hJybQ9&;L82Onep?Bc>21A zf;m%DQ+0qH3u@^2@S(YO;bY>11RijI1Bmf$YQfpQg)!Gcz!6s_dcSALcrk9y4^t`jk^9c5`lQ73<`Y-p+!W+eq=0}uCdgRnd@S0kc`#3gDI2tY2B6IONag(c*Eolclr~S<-pwhHYF`9 zO@pJiKX6747^QQf3xL+G+1*?S^J%81JBH}+{=2}ccXvEJXp~^vtw|}vLm71jT!R=O z=^WH`@8UyKI0tT?t`?9m@2^I()TrivYc2xbd>qOBo}MZh1D&0ehyuw|9;dk3O}%M9j5GPn?F`aNvW0s~Xo&iBpvMrn~li$UBd*xp@}5 zs3ft@LncM++tmO+Vx-Jd<(7E|;q(1++e zKBHbSWF)QhYUtR!B^;xeT~{n;h)tydqqpI>un4lljX8qOS%rQ?obN}w-?OWL5A@@% z!=eBV=j-&AhzbF~6I#z1E;P+vPv8m2QLKKJ!2vAno4(QtQah~y*MOhZ@{q7jQAsq- z%^XS=C^F|>bzHiqs!}`9l`S-D|0M#!^D^(xCWt{%Q<^!W?y0&UE;#%dFK{S?CkieY zXPqeD);BUrP1gH7>ZO-8pR5fQ$q0p^roxIqPjSZNqx-WeS z?n8nDMd97tT&GfIgX~b zIfB7)r^5nm6f}f7gZw@;R9?CN0(>`_3|F0(r1hT&lw^Y?t`PNS*8b|y+}!41vP!;r~C7?TnX&&;c1F#T7 zQBhIFCJAQU&@EAuHCupl{UBiTGU8aAj^Ll2L%Ra9MV>1?J^R3BgR+lU7H$w--Qx*@ z>zDZ>Sw3~uMh)u6ADu}f7__okC^%S`?mt>s~c#tyaf51 zHZ~jhyBpfY+S5~)lN|w4sQ-e7pbo2Z6WnF5d5=A09f^bHCm!Qots~Rl7Vc^d;ijZwnDEQ+ zyWdpVPzA4jZD6k-8qx&h0f6>J#K;xy$0a04%WAkigfirHrvU#M z5|(N(0~YcC>8nL%J9ufxcNm&g05*AMpa$YK7th5C>__Y|zGXbNQ|eq&29HTm*=1sK z*RmfdsHogW#5w=-C$9h)sH}p^ZhqiqoEZAFWR+l?HJl%Dw+{wj3NJzpY3tQ5&9SttRe9e!x3E(MQ z>3Y_$X_hwx`a0yEy&`z(u*5+`aGNp})C>=TCcsMx6zpY?+HW+y7VTySCqB)w=bL}J zTEhrTiO<$hkCS9Pc~AsUF4w~L6F^S=@u9i&yv4n0#56N6=^fJLMj16e)@ z5}A}B9Ruyi`=6?d2Wc-feuN!*5{~uENHUBVSF#ez(N~KQpwJGg-MD+%CNbY~m6h#s zI^ub4&#z4+1pipQJ$=o~<{N|+0v}VY#-3^7BB9S@M14T&q>0U#qYe353Itj8V{Za0 zebPR-mxaZ=6G9+x{{~fvr|G3h4_rJOXwq8hfLVgCcP8O>{KsP;=)J^n+IHpK9Nve8 z-2HxjI*RnqwyX0WFwOBEOGsP>Cuw0;>8|DgP)Vokus6;u&33FQ$HnK2+h zPD?Xr2}z%)O}HWBwvx1x7{&mIG*PFhTH}>0YyhjDP>5DFPYwH2??&r%F*d5 znKm%;C*pOt$q^d=Nqk5xQcv5IRf<|}izr#ii9U(=s`h8OLc}=?4#^iN;?b+(S?xr; zUpE9NPsk!_se$S&KFQ)@STsP&q3c*SVDqHJR9z-O=Ekp}CXD_h$a#yF;xZ)9?*C@&rx1r7f0&nuRuOeg0M~qeceOJCFCqby<@lhbtgS39mA{^DsBh?4%1;tEzo0$d zG`t3}kkld6Mo4)qYbz2xtFr-3dyasBS?nYrpv&c+mSS`+Zvi*<`sRt*6JClgN)=@p zH93PdI(UVJto%-9Lz$9zxYzJmmP?nv<~g?;HWCsL=9^wotk4(+Ek6|^P7ykWO~k7} zrXo|5rX9cjPj*e90Q!J%TBXpx14(x0c3)4S>#HKLBuTnzF=tRM+5$rzqOaTp`94Df z(%tEPv5fQ#uWt`=h^Q~Ut}6whc&&2M&1H0K3)H(q9PQGi=!If!MMMaWA$k)GjJI;* zT&l={PWi|OkaZBgWBx@%ySW_z(>jo_(fc;#PY#O^kFr4wM{PXT`@;84X#ba3`!d`Y zF8cKJ^aBeXlCj|&NzG#AazY}j9m0aT&aurskJxHX!?P3hb%PUoLa~I`XCW{BCw&>G z$zx(NlMoVeoNe9OUUM+2I0Ua`uk*~c?@f~ift1~s)y{plX^Ky&KVZsEOTPEiV}qxN z01}Tk1p-n^LK{FzxN1RL-uuo-4{*a2FN`?9?)6+M)OvV9F@LuEZT11o*wXz# z^B=2`>;Yw*bBTI(K_c%lg#AN_`6TzFt7D@2C-aU7rlVmH6MZZp)}G{5ETFDws7Zh6 z`kIZk_68#ZDggT01%$){2qG>=&sckQVd@6KRVc6UiHhH$Wk}1)zBo6N6Ca=O1_TK7 z6l}cEEv26R^ee~HySsQZDR{amClQJWnX9)*{lCW}ul-J`F5(`|9?#ixl9TiP^q|F@ zRvZ@AQ}j`stPciHOK&VOsIY+Pe5aiI2;&yF^Jnuh`*orlPyn5Dzop4XAF356_?H&% z=+VDsY5#nSm{+Dh|2t?PIrpDQ*%y?5k|MCfSNeYnn*Q#*G0B*e{Bl|@w&=pU|M{8z zP8)En|NE1Q!`RrN@I?j*t$xjh`FTS=HP55}djDHosqcS!-4_IeM>qq_Gw-ozuN}SO z*USD_(%D7OcjqWO&~X5pGhH;^5o%4p;!o}1ZwLp+{O?rw|LsNopC|fcC%On02zyK} z0P*biJh#}sL1ZW0UC`>@YnzB}8#_gJJ$Y#jkCx07}~gcKEc7Z45Us zuSKN4_+P&c69xvzfkAx(gP~!CK)m}Hw$jm|3i;XCihcz-Fl_H&ZhA?d!t4JV_riaE zeXK_jkiZ@WqmRP50)a1RnYNu`qGEu|YsP3-D`6@6P}&lz9xb_yL}zYkzA?{gKe2%zT!9$5XS>SG%=~p=XWt`(t*A)Zan-)k)UDb* zvT{G6v|@|Pwz#Co);1qKumhclKw3|XJhN@n)!jWkz_q(S4R_ZaUTDX1OvULe$6%QQ z)Dpi93^sin`#2V%g)XgF0iYnf5F+=~ zd@e0J{&L$6s=73Z9wZJ8ZG9m#OQvnaNPdXVLfSS^)mBuJTDR`|MKdawt3Fd{O-=>5 zHWc*SBcZ;oWC3TeXq>4WMZ#(8aw#?}Jo;0f6~3C;31?ov==M70w5*KDuE3ncZniiU zgO7g(_JhDvyTe|pB9A3P$Gcm5y-gZex#q@ZAzXp7xrATtl##neiKi|!zwi9s3H^~BoU$|bdvRpHl)UvW# zEi=Q4z@iDL=fikCwe%uJ&Be7c($h=%HHe7l;52k~*|*rbsUNtpZuV83uJIHqx1DQY z@_nW+!-?dgep{cSXQ@`;_3q{t>n6yz6%@*dk)Ef{80wpIkCs(dzSz6Ex$w1~*Vj2J zO7sM?Uo=B&YiVj8^kPr4xCw(aA_C46w9ZSBwfzk~aB1F#iC@+Bi-trsuBcXQkQFx` z+$M0980hI|s%d7A-8|Jb;CT)P%;Z4$!Bw$%){x0#j~ty&{hCk(=u$p)7w2CxUoehq zr-~vVnzK?X0LIJ2n>|2Tar%~Cme!Y=u}KDuuK>jvJGAbCv$DFZ7q=2u3V5MG%;6$TA%w~ig{tlboI<*65b0J zN?$qZrcjRbkCf=y+elmtjd3C&S;{CXyo;`;0S;SfPMH|m4{t`f;bnJp;9kU;O`mV_ zX&(gC{Pzq^jOk${+R0HZ8DXz)<&{iwzmb+RpoC8LyO<}Ef)&y#r9tQdwqSiHIGz|I=8ceS`U z|LcJ}%pkbitO|6wGw)xU68o%mjQH*uo}#QD;qZ7#z8qZ=T9JLH-#EhMqNSSEGO0%J zvnap(ANE5da$>P3wj?ILX}3GKR`aPIHx>48?6!O0W)^?oTyB;5G%ht`xa*r79-E)* zpNu9fE^#!9)=w`StumM&9lHM`>_YsPqToN*ioe-jc-y~L*#BkO{r?a4pE2?O%jo6* t1DCP(5TgCVLkZ3|SukEsV`9(~2(PHP 8 path) — THIS PR"] + DDClient["DDTrace FeatureFlags Client
(PHP 7 and PHP 8 share this) — THIS PR"] + DDProvider["DDTrace OpenFeature DataDogProvider
(PHP 8 only) — THIS PR"] + Evaluator["NativeEvaluator (FFI → libdatadog) — THIS PR"] + RC["Remote Config client (FFE_FLAGS product) — THIS PR"] + Hook["EvaluationCompletedHook composite (future)"] + OFHook["OpenFeature SDK Hook layer
(EvalMetricsHook on DataDogProvider) (future)"] + ExposureWriter["ExposureWriter (future)"] + MetricWriter["EvaluationMetricWriter
(aggregate, OTLP protobuf encode) (future)"] + SidecarFFI["ddog_sidecar_send_ffe_{exposures,metrics} FFI (future)"] + end + + subgraph Host["Host"] + Sidecar["libdatadog sidecar (Rust, async) (future)"] + end + + subgraph Backend["Backend"] + Agent["Datadog Agent (future)"] + EVP["EVP proxy (future)"] + OTLP["OTLP HTTP intake (future)"] + end + + UserCode -- "PHP 7" --> DDClient + UserCode -- "PHP 8" --> OFClient --> DDProvider --> DDClient + DDClient --> Evaluator + Evaluator <--> RC + DDClient --> Hook + Hook --> ExposureWriter + Hook --> MetricWriter + DDProvider -. registers .-> OFHook + OFHook --> MetricWriter + OFHook --> ExposureWriter + ExposureWriter --> SidecarFFI + MetricWriter -- "OTLP protobuf" --> SidecarFFI + SidecarFFI --> Sidecar + Sidecar --> Agent --> EVP + Sidecar -- "OTLP HTTP" --> OTLP + + classDef pr1 fill:#ddd6fe,stroke:#6d28d9,color:#1e1b4b,stroke-width:3px + classDef future fill:#e5e7eb,stroke:#9ca3af,color:#374151,stroke-dasharray: 5 5 + + class UserCode,OFClient,DDClient,DDProvider,Evaluator,RC pr1 + class Hook,OFHook,ExposureWriter,MetricWriter,SidecarFFI,Sidecar,Agent,EVP,OTLP future diff --git a/docs/php-ffe-stack/system-pr3906.png b/docs/php-ffe-stack/system-pr3906.png new file mode 100644 index 0000000000000000000000000000000000000000..fc50625e22e8ab35edf7c56f2d1d1a99f5ddfd9d GIT binary patch literal 500812 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

l}>l=X=vH5Kdv^OxMr}HJ|A#Uzl)LWJPHtE~n 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