diff --git a/Cargo.lock b/Cargo.lock index 4a694975..394ef9a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1875,6 +1875,7 @@ dependencies = [ "diesel-derive-enum", "dotenv", "env_logger", + "jsonlogic", "log", "reqwest", "rs-snowflake", diff --git a/crates/experimentation_platform/Cargo.toml b/crates/experimentation_platform/Cargo.toml index 383e149d..d87969bd 100644 --- a/crates/experimentation_platform/Cargo.toml +++ b/crates/experimentation_platform/Cargo.toml @@ -34,6 +34,7 @@ superposition_types = { path = "../superposition_types" } reqwest = { workspace = true } anyhow = { workspace = true } superposition_macros = { path = "../superposition_macros" } +jsonlogic = { workspace = true } [features] disable_db_data_validation = ["superposition_types/disable_db_data_validation"] diff --git a/crates/experimentation_platform/src/api/experiments/handlers.rs b/crates/experimentation_platform/src/api/experiments/handlers.rs index 8fca5078..31f76c9b 100644 --- a/crates/experimentation_platform/src/api/experiments/handlers.rs +++ b/crates/experimentation_platform/src/api/experiments/handlers.rs @@ -34,13 +34,16 @@ use super::{ AuditQueryFilters, ConcludeExperimentRequest, ContextAction, ContextBulkResponse, ContextMoveReq, ContextPutReq, ExperimentCreateRequest, ExperimentCreateResponse, ExperimentResponse, ExperimentsResponse, ListFilters, OverrideKeysUpdateRequest, - RampRequest, Variant, + RampRequest, Variant, VariantType, }, }; use crate::{ - db::models::{EventLog, Experiment, ExperimentStatusType}, - db::schema::{event_log::dsl as event_log, experiments::dsl as experiments}, + api::experiments::types::ApplicableVariantsRequest, + db::{ + models::{EventLog, Experiment, ExperimentStatusType}, + schema::{event_log::dsl as event_log, experiments::dsl as experiments}, + }, }; use serde_json::{json, Map, Value}; @@ -54,6 +57,7 @@ pub fn endpoints(scope: Scope) -> Scope { .service(get_experiment_handler) .service(ramp) .service(update_overrides) + .service(get_applicable_variants) } fn construct_header_map( @@ -442,6 +446,85 @@ pub async fn conclude( Ok((updated_experiment, config_version_id)) } +#[post("/applicable_variants")] +async fn get_applicable_variants( + db_conn: DbConnection, + req: Json, +) -> superposition::Result { + let DbConnection(mut conn) = db_conn; + let payload = req.into_inner(); + + let experiments = experiments::experiments + .filter(experiments::status.ne(ExperimentStatusType::CONCLUDED)) + .load::(&mut conn)?; + + let experiment_list = experiments.into_iter(); + + let experiments = experiment_list.filter_map(|exp| { + let is_empty = exp + .context + .as_object() + .map_or(false, |context| context.is_empty()); + if is_empty { + Some(exp.clone()) + } else { + match jsonlogic::partial_apply(&exp.context, &payload.context) { + Ok(jsonlogic::PartialApplyOutcome::Resolved(Value::Bool(true))) + | Ok(jsonlogic::PartialApplyOutcome::Ambiguous) => Some(exp.clone()), + _ => None, + } + } + }); + + let mut variants = Vec::new(); + for exp in experiments { + if let Some(v) = decide_variant( + exp.traffic_percentage as u8, + serde_json::from_value(exp.variants).map_err(|e| { + log::error!("Unable to parse variants from DB {e}"); + unexpected_error!("Something went wrong.") + })?, + payload.toss, + ) + .map_err(|e| { + log::error!("Unable to decide variant {e}"); + unexpected_error!("Something went wrong.") + })? { + variants.push(v) + } + } + + Ok(HttpResponse::Ok().json(variants)) +} + +fn decide_variant( + traffic: u8, + applicable_variants: Vec, + toss: i8, +) -> Result, String> { + if toss < 0 { + for variant in applicable_variants.iter() { + if variant.variant_type == VariantType::EXPERIMENTAL { + return Ok(Some(variant.clone())); + } + } + } + let variant_count = applicable_variants.len() as u8; + let range = (traffic * variant_count) as i32; + if (toss as i32) >= range { + return Ok(None); + } + let buckets = (1..=variant_count) + .map(|i| (traffic * i) as i8) + .collect::>(); + let index = buckets + .into_iter() + .position(|x| toss < x) + .ok_or_else(|| "Unable to fetch variant's index".to_string())?; + + Ok(applicable_variants.get(index).cloned()) +} + #[get("")] async fn list_experiments( req: HttpRequest, diff --git a/crates/experimentation_platform/src/api/experiments/types.rs b/crates/experimentation_platform/src/api/experiments/types.rs index 9061b5a4..24df4eea 100644 --- a/crates/experimentation_platform/src/api/experiments/types.rs +++ b/crates/experimentation_platform/src/api/experiments/types.rs @@ -127,6 +127,13 @@ pub enum ContextBulkResponse { MOVE(ContextPutResp), } +/********** Applicable Variants API Type *************/ +#[derive(Deserialize, Debug)] +pub struct ApplicableVariantsRequest { + pub context: Value, + pub toss: i8, +} + /********** List API Filter Type *************/ #[derive(Deserialize, Debug, Clone)]