From a139d25d2393fdeb64deddafcae0dd9859766a4b Mon Sep 17 00:00:00 2001 From: datron Date: Fri, 21 Nov 2025 12:46:12 +0530 Subject: [PATCH 1/3] feat: expose get_satisfied_experiments and refactor code for resolution - Add variants chosen to the Resolution Details so users can validate and debug --- crates/superposition_provider/src/client.rs | 44 ++- crates/superposition_provider/src/lib.rs | 4 +- crates/superposition_provider/src/provider.rs | 309 ++++++++++-------- 3 files changed, 205 insertions(+), 152 deletions(-) diff --git a/crates/superposition_provider/src/client.rs b/crates/superposition_provider/src/client.rs index 6eadd2477..b6d65ba1e 100644 --- a/crates/superposition_provider/src/client.rs +++ b/crates/superposition_provider/src/client.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::sync::Arc; use log::{debug, error, info, warn}; -use serde_json::Value; +use serde_json::{Map, Value}; use superposition_core::experiment::ExperimentGroups; use superposition_core::{ eval_config, get_applicable_variants, Experiments, MergeStrategy, @@ -22,17 +22,17 @@ pub use open_feature::{ }; #[derive(Debug)] -pub struct CacConfig { +pub struct CacClient { superposition_options: SuperpositionOptions, options: ConfigurationOptions, fallback_config: Option>, cached_config: Arc>>, - last_updated: Arc>>>, + pub last_updated: Arc>>>, evaluation_cache: RwLock>>, polling_task: RwLock>>, } -impl CacConfig { +impl CacClient { pub fn new( superposition_options: SuperpositionOptions, options: ConfigurationOptions, @@ -217,7 +217,7 @@ impl CacConfig { pub async fn evaluate_config( &self, query_data: &serde_json::Map, - prefix_filter: Option<&[String]>, + prefix_filter: Option>, ) -> Result> { let cached_config = self.cached_config.read().await; match cached_config.as_ref() { @@ -264,17 +264,17 @@ impl CacConfig { /// Experimentation Configuration client #[derive(Debug)] -pub struct ExperimentationConfig { +pub struct ExperimentationClient { superposition_options: SuperpositionOptions, options: ExperimentationOptions, cached_experiments: Arc>>, cached_experiment_groups: Arc>>, - last_updated: Arc>>>, + pub last_updated: Arc>>>, evaluation_cache: RwLock>>, polling_task: RwLock>>, } -impl ExperimentationConfig { +impl ExperimentationClient { pub fn new( superposition_options: SuperpositionOptions, options: ExperimentationOptions, @@ -543,6 +543,34 @@ impl ExperimentationConfig { cached_experiments.clone() } + pub async fn get_satisfied_experiments( + &self, + context: &Map, + filter_prefixes: Option>, + ) -> Result { + let cached_experiments = self.cached_experiments.read().await; + let experiments = match cached_experiments.as_ref() { + Some(experiments) => experiments, + None => { + return Err(SuperpositionError::ConfigError( + "No cached experiments available, please check if the experimentation settings are configured correctly".into(), + )) + } + }; + + superposition_core::experiment::get_satisfied_experiments( + experiments, + context, + filter_prefixes, + ) + .map_err(|e| { + SuperpositionError::ConfigError(format!( + "Failed to get satisfied experiments: {}", + e + )) + }) + } + pub async fn get_cached_experiment_groups(&self) -> Option { let cached_experiment_groups = self.cached_experiment_groups.read().await; cached_experiment_groups.clone() diff --git a/crates/superposition_provider/src/lib.rs b/crates/superposition_provider/src/lib.rs index 059e47cf1..67efcf70d 100644 --- a/crates/superposition_provider/src/lib.rs +++ b/crates/superposition_provider/src/lib.rs @@ -31,7 +31,7 @@ mod tests { None, ); - let cac_config = CacConfig::new(superposition_options, config_options); + let cac_config = CacClient::new(superposition_options, config_options); assert!(cac_config.get_cached_config().await.is_none()); } @@ -49,7 +49,7 @@ mod tests { OnDemandStrategy::default(), )); - let exp_config = ExperimentationConfig::new(superposition_options, exp_options); + let exp_config = ExperimentationClient::new(superposition_options, exp_options); // Test that we can get None for cached experiments initially assert!(exp_config.get_cached_experiments().await.is_none()); diff --git a/crates/superposition_provider/src/provider.rs b/crates/superposition_provider/src/provider.rs index b431da674..40ebd20a3 100644 --- a/crates/superposition_provider/src/provider.rs +++ b/crates/superposition_provider/src/provider.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use async_trait::async_trait; +use chrono::{DateTime, Utc}; use log::{error, info}; use open_feature::{ provider::FeatureProvider, @@ -8,20 +9,23 @@ use open_feature::{ EvaluationContext, EvaluationError, EvaluationErrorCode, EvaluationResult, StructValue, }; -use serde_json::Value; +use serde_json::{json, Value}; +use superposition_core::Experiments; use superposition_types::DimensionInfo; use tokio::sync::RwLock; -use crate::client::{CacConfig, ExperimentationConfig}; +use crate::client::{CacClient, ExperimentationClient}; use crate::types::*; use crate::utils::ConversionUtils; +pub type ResolutionResponse = (serde_json::Map, Vec); + #[derive(Debug)] pub struct SuperpositionProvider { metadata: ProviderMetadata, status: RwLock, - cac_config: Option, - exp_config: Option, + cac_client: Option, + exp_client: Option, } impl SuperpositionProvider { pub fn new(provider_options: SuperpositionProviderOptions) -> Self { @@ -38,15 +42,15 @@ impl SuperpositionProvider { provider_options.fallback_config.clone(), ); - let cac_config = - CacConfig::new(superposition_options.clone(), cac_options.clone()); + let cac_client = + CacClient::new(superposition_options.clone(), cac_options.clone()); - let exp_config = + let exp_client = provider_options .experimentation_options .as_ref() .map(|exp_opts| { - ExperimentationConfig::new( + ExperimentationClient::new( superposition_options.clone(), exp_opts.clone(), ) @@ -57,8 +61,8 @@ impl SuperpositionProvider { name: "SuperpositionProvider".to_string(), }, status: RwLock::new(ProviderStatus::NotReady), - cac_config: Some(cac_config), - exp_config, + cac_client: Some(cac_client), + exp_client, } } @@ -81,8 +85,8 @@ impl SuperpositionProvider { } async fn get_dimensions_info(&self) -> HashMap { - match &self.cac_config { - Some(cac_config) => cac_config + match &self.cac_client { + Some(client) => client .get_cached_config() .await .map(|c| c.dimensions.clone()) @@ -91,10 +95,43 @@ impl SuperpositionProvider { } } + async fn resolve_value( + &self, + flag_key: &str, + evaluation_context: &EvaluationContext, + converter: fn(&Value) -> EvaluationResult, + ) -> EvaluationResult> { + let (config, variants) = self + .eval_config(evaluation_context, None) + .await + .map_err(|e| { + error!("Error evaluating flag {}: {}", flag_key, e); + EvaluationError { + code: EvaluationErrorCode::General("EVALUATION_ERROR".to_string()), + message: Some(format!( + "could not evaluate config for the given context: {}", + e + )), + } + })?; + let value = config + .get(flag_key) + .ok_or(EvaluationError { + code: EvaluationErrorCode::FlagNotFound, + message: Some("Flag not found in configuration".to_string()), + }) + .and_then(converter)?; + let mut resolution_details = ResolutionDetails::new(value); + if !variants.is_empty() { + resolution_details.variant = Some(variants.join(",")) + } + Ok(resolution_details) + } + pub async fn init(&self) -> Result<()> { // Initialize CAC config - if let Some(cac_config) = &self.cac_config { - match cac_config.create_config().await { + if let Some(client) = &self.cac_client { + match client.create_config().await { Ok(_) => info!("CAC configuration initialized successfully"), Err(e) => { error!("Failed to initialize CAC configuration: {}", e); @@ -107,8 +144,8 @@ impl SuperpositionProvider { } // Initialize experimentation config if available - if let Some(exp_config) = &self.exp_config { - match exp_config.create_config().await { + if let Some(client) = &self.exp_client { + match client.create_config().await { Ok(_) => info!("Experimentation configuration initialized successfully"), Err(e) => { error!("Failed to initialize experimentation configuration: {}", e); @@ -125,20 +162,76 @@ impl SuperpositionProvider { pub async fn resolve_full_config( &self, evaluation_context: &EvaluationContext, - ) -> Result> { - self.eval_config(evaluation_context).await + prefix_filters: Option>, + ) -> Result { + self.eval_config(evaluation_context, prefix_filters).await + } + + pub async fn get_satisfied_experiments( + &self, + context: &EvaluationContext, + filter_prefixes: Option>, + ) -> Result { + let Some(ref exp_client) = self.exp_client else { + return Err(SuperpositionError::ProviderError( + "Experimentation config not initialized".into(), + )); + }; + let (context_map, _) = self.get_context_from_evaluation_context(context); + exp_client + .get_satisfied_experiments(&context_map, filter_prefixes) + .await + } + + pub async fn get_running_experiments_from_provider(&self) -> Result { + let Some(exp_client) = &self.exp_client else { + return Err(SuperpositionError::ProviderError( + "Experimentation config not initialized".into(), + )); + }; + exp_client.get_cached_experiments().await.ok_or( + SuperpositionError::ProviderError( + "Could not retrieve running experiments".into(), + ), + ) + } + + pub async fn get_last_modified_time(&self) -> Result> { + let Some(cac_client) = &self.cac_client else { + return Err(SuperpositionError::ConfigError( + "CAC client not initialized".into(), + )); + }; + let cac_last_modified = cac_client.last_updated.read().await.ok_or( + SuperpositionError::ConfigError( + "Could not retrieve last modified time".into(), + ), + )?; + if let Some(exp_client) = &self.exp_client { + let exp_last_modified = exp_client.last_updated.read().await; + match *exp_last_modified { + Some(exp_time) if exp_time > cac_last_modified => { + return Ok(exp_time); + } + _ => { + return Ok(cac_last_modified); + } + } + } + Ok(cac_last_modified) } async fn eval_config( &self, evaluation_context: &EvaluationContext, - ) -> Result> { + prefix_filters: Option>, + ) -> Result { // Get cached config from CAC let (mut context, targeting_key) = self.get_context_from_evaluation_context(evaluation_context); let dimensions_info = self.get_dimensions_info().await; - let variant_ids = if let Some(exp_config) = &self.exp_config { + let variant_ids = if let Some(exp_config) = &self.exp_client { exp_config .get_applicable_variants(&dimensions_info, &context, targeting_key) .await? @@ -146,19 +239,18 @@ impl SuperpositionProvider { vec![] }; - context.insert( - "variantIds".to_string(), - Value::Array(variant_ids.into_iter().map(Value::String).collect()), - ); + context.insert("variantIds".to_string(), json!(variant_ids)); - match &self.cac_config { - Some(cac_config) => cac_config.evaluate_config(&context, None).await, - None => Err(SuperpositionError::ConfigError( + let Some(ref client) = self.cac_client else { + return Err(SuperpositionError::ConfigError( "CAC config not initialized".into(), - )), - } + )); + }; + let config = client.evaluate_config(&context, prefix_filters).await?; + Ok((config, variant_ids)) } } + #[async_trait] impl FeatureProvider for SuperpositionProvider { async fn initialize(&mut self, _context: &EvaluationContext) { @@ -184,26 +276,15 @@ impl FeatureProvider for SuperpositionProvider { flag_key: &str, evaluation_context: &EvaluationContext, ) -> EvaluationResult> { - match self.eval_config(evaluation_context).await { - Ok(config) => { - if let Some(value) = config.get(flag_key) { - if let Some(bool_val) = value.as_bool() { - return Ok(ResolutionDetails::new(bool_val)); - } - } - Err(EvaluationError { - code: EvaluationErrorCode::FlagNotFound, - message: Some("Flag not found in configuration".to_string()), - }) - } - Err(e) => { - error!("Error evaluating boolean flag {}: {}", flag_key, e); - Err(EvaluationError { - code: EvaluationErrorCode::FlagNotFound, - message: Some("Flag not found in configuration".to_string()), - }) - } - } + self.resolve_value(flag_key, evaluation_context, |v| { + v.as_bool().ok_or(EvaluationError { + code: EvaluationErrorCode::TypeMismatch, + message: Some( + "The value could not be parsed into the desired type".to_string(), + ), + }) + }) + .await } async fn resolve_string_value( @@ -211,26 +292,15 @@ impl FeatureProvider for SuperpositionProvider { flag_key: &str, evaluation_context: &EvaluationContext, ) -> EvaluationResult> { - match self.eval_config(evaluation_context).await { - Ok(config) => { - if let Some(value) = config.get(flag_key) { - if let Some(str_val) = value.as_str() { - return Ok(ResolutionDetails::new(str_val.to_owned())); - } - } - Err(EvaluationError { - code: EvaluationErrorCode::FlagNotFound, - message: Some("Flag not found in configuration".to_string()), - }) - } - Err(e) => { - error!("Error evaluating String flag {}: {}", flag_key, e); - Err(EvaluationError { - code: EvaluationErrorCode::FlagNotFound, - message: Some("Flag not found in configuration".to_string()), - }) - } - } + self.resolve_value(flag_key, evaluation_context, |v| { + v.as_str().map(|s| s.to_string()).ok_or(EvaluationError { + code: EvaluationErrorCode::TypeMismatch, + message: Some( + "The value could not be parsed into the desired type".to_string(), + ), + }) + }) + .await } async fn resolve_int_value( @@ -238,26 +308,15 @@ impl FeatureProvider for SuperpositionProvider { flag_key: &str, evaluation_context: &EvaluationContext, ) -> EvaluationResult> { - match self.eval_config(evaluation_context).await { - Ok(config) => { - if let Some(value) = config.get(flag_key) { - if let Some(int_val) = value.as_i64() { - return Ok(ResolutionDetails::new(int_val)); - } - } - Err(EvaluationError { - code: EvaluationErrorCode::FlagNotFound, - message: Some("Flag not found in configuration".to_string()), - }) - } - Err(e) => { - error!("Error evaluating integer flag {}: {}", flag_key, e); - Err(EvaluationError { - code: EvaluationErrorCode::FlagNotFound, - message: Some("Flag not found in configuration".to_string()), - }) - } - } + self.resolve_value(flag_key, evaluation_context, |v| { + v.as_i64().ok_or(EvaluationError { + code: EvaluationErrorCode::TypeMismatch, + message: Some( + "The value could not be parsed into the desired type".to_string(), + ), + }) + }) + .await } async fn resolve_float_value( @@ -265,26 +324,15 @@ impl FeatureProvider for SuperpositionProvider { flag_key: &str, evaluation_context: &EvaluationContext, ) -> EvaluationResult> { - match self.eval_config(evaluation_context).await { - Ok(config) => { - if let Some(value) = config.get(flag_key) { - if let Some(int_val) = value.as_f64() { - return Ok(ResolutionDetails::new(int_val)); - } - } - Err(EvaluationError { - code: EvaluationErrorCode::FlagNotFound, - message: Some("Flag not found in configuration".to_string()), - }) - } - Err(e) => { - error!("Error evaluating float flag {}: {}", flag_key, e); - Err(EvaluationError { - code: EvaluationErrorCode::FlagNotFound, - message: Some("Flag not found in configuration".to_string()), - }) - } - } + self.resolve_value(flag_key, evaluation_context, |v| { + v.as_f64().ok_or(EvaluationError { + code: EvaluationErrorCode::TypeMismatch, + message: Some( + "The value could not be parsed into the desired type".to_string(), + ), + }) + }) + .await } async fn resolve_struct_value( @@ -292,39 +340,16 @@ impl FeatureProvider for SuperpositionProvider { flag_key: &str, evaluation_context: &EvaluationContext, ) -> EvaluationResult> { - match self.eval_config(evaluation_context).await { - Ok(config) => { - if let Some(value) = config.get(flag_key) { - // Use the conversion utility we added earlier - match ConversionUtils::serde_value_to_struct_value(value) { - Ok(struct_value) => { - return Ok(ResolutionDetails::new(struct_value)); - } - Err(e) => { - error!("Error converting value to StructValue: {}", e); - return Err(EvaluationError { - code: EvaluationErrorCode::ParseError, - message: Some(format!( - "Failed to parse struct value: {}", - e - )), - }); - } - } - } - Err(EvaluationError { - code: EvaluationErrorCode::FlagNotFound, - message: Some("Flag not found in configuration".to_string()), - }) - } - Err(e) => { - error!("Error evaluating Object flag {}: {}", flag_key, e); - Err(EvaluationError { - code: EvaluationErrorCode::FlagNotFound, - message: Some("Flag not found in configuration".to_string()), - }) - } - } + self.resolve_value(flag_key, evaluation_context, |v| { + ConversionUtils::serde_value_to_struct_value(v).map_err(|e| EvaluationError { + code: EvaluationErrorCode::TypeMismatch, + message: Some(format!( + "The value could not be parsed into the desired type: {}", + e + )), + }) + }) + .await } fn metadata(&self) -> &ProviderMetadata { From 08425097472eafc6c594677b92fa397c5da6c3e7 Mon Sep 17 00:00:00 2001 From: datron Date: Mon, 1 Dec 2025 16:13:34 +0530 Subject: [PATCH 2/3] fix: update error message to better debug issues --- crates/superposition_provider/src/client.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/superposition_provider/src/client.rs b/crates/superposition_provider/src/client.rs index b6d65ba1e..fa232c916 100644 --- a/crates/superposition_provider/src/client.rs +++ b/crates/superposition_provider/src/client.rs @@ -483,8 +483,8 @@ impl ExperimentationClient { .await .map_err(|e| { SuperpositionError::NetworkError(format!( - "Failed to list experiments: {}", - e + "Failed to list experiments: {:?}", + e.raw_response() )) })?; @@ -523,8 +523,8 @@ impl ExperimentationClient { .await .map_err(|e| { SuperpositionError::NetworkError(format!( - "Failed to list experiment groups: {}", - e + "Failed to list experiment groups: {:?}", + e.raw_response() )) })?; From 128469a3540c11fe5cf589cb9234479f47d8f910 Mon Sep 17 00:00:00 2001 From: datron Date: Mon, 15 Dec 2025 16:00:00 +0530 Subject: [PATCH 3/3] fix: Do not set variantIds if variants are empty --- crates/superposition_provider/src/provider.rs | 21 +++++++++++-------- crates/superposition_provider/src/utils.rs | 10 ++++----- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/crates/superposition_provider/src/provider.rs b/crates/superposition_provider/src/provider.rs index 40ebd20a3..821862490 100644 --- a/crates/superposition_provider/src/provider.rs +++ b/crates/superposition_provider/src/provider.rs @@ -231,16 +231,19 @@ impl SuperpositionProvider { self.get_context_from_evaluation_context(evaluation_context); let dimensions_info = self.get_dimensions_info().await; - let variant_ids = if let Some(exp_config) = &self.exp_client { - exp_config - .get_applicable_variants(&dimensions_info, &context, targeting_key) - .await? - } else { - vec![] - }; - - context.insert("variantIds".to_string(), json!(variant_ids)); + let mut variant_ids = Vec::new(); + if targeting_key.is_some() { + if let Some(exp_config) = &self.exp_client { + let applicable_variant_ids = exp_config + .get_applicable_variants(&dimensions_info, &context, targeting_key) + .await?; + context.insert("variantIds".to_string(), json!(applicable_variant_ids)); + variant_ids = applicable_variant_ids; + } else { + log::warn!("Targeting key is set, but experiments have not been defined in the superposition provider builder options") + } + } let Some(ref client) = self.cac_client else { return Err(SuperpositionError::ConfigError( "CAC config not initialized".into(), diff --git a/crates/superposition_provider/src/utils.rs b/crates/superposition_provider/src/utils.rs index 9012a896e..b53fd219d 100644 --- a/crates/superposition_provider/src/utils.rs +++ b/crates/superposition_provider/src/utils.rs @@ -115,7 +115,7 @@ impl ConversionUtils { dimensions, }; - debug!("Successfully converted config with {} contexts, {} overrides, {} default configs", + debug!("Successfully converted config with {} contexts, {} overrides, {} default configs", config.contexts.len(), config.overrides.len(), config.default_configs.len()); Ok(config) @@ -525,12 +525,10 @@ impl ConversionUtils { } open_feature::EvaluationContextFieldValue::Struct(s) => { // Convert struct to serde_json::Value - let struct_map: Map = s - .as_ref() - .downcast_ref::>() + s.as_ref() + .downcast_ref::() .cloned() - .unwrap_or_default(); - Value::Object(struct_map) + .unwrap_or_default() } } }