From d86191c5f1e2f2796072a354f1b85fbd413d8933 Mon Sep 17 00:00:00 2001 From: "ayush.jain@juspay.in" Date: Fri, 23 Aug 2024 16:10:22 +0530 Subject: [PATCH] feat: Add get_applicable_variants as expt endpoint --- Cargo.lock | 1 + crates/experimentation_platform/Cargo.toml | 1 + .../src/api/experiments/handlers.rs | 55 +++++++++++++++++-- .../src/api/experiments/helpers.rs | 28 ++++++++++ .../src/api/experiments/types.rs | 42 ++++++++++++++ 5 files changed, 123 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a902e28..51638364 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1928,6 +1928,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..11bc7b54 100644 --- a/crates/experimentation_platform/src/api/experiments/handlers.rs +++ b/crates/experimentation_platform/src/api/experiments/handlers.rs @@ -27,8 +27,8 @@ use superposition_types::{ use super::{ helpers::{ add_variant_dimension_to_ctx, check_variant_types, - check_variants_override_coverage, extract_override_keys, validate_experiment, - validate_override_keys, + check_variants_override_coverage, decide_variant, extract_override_keys, + validate_experiment, validate_override_keys, }, types::{ AuditQueryFilters, ConcludeExperimentRequest, ContextAction, ContextBulkResponse, @@ -39,8 +39,11 @@ use super::{ }; use crate::{ - db::models::{EventLog, Experiment, ExperimentStatusType}, - db::schema::{event_log::dsl as event_log, experiments::dsl as experiments}, + api::experiments::types::ApplicableVariantsQuery, + db::{ + models::{EventLog, Experiment, ExperimentStatusType}, + schema::{event_log::dsl as event_log, experiments::dsl as experiments}, + }, }; use serde_json::{json, Map, Value}; @@ -51,6 +54,7 @@ pub fn endpoints(scope: Scope) -> Scope { .service(create) .service(conclude_handler) .service(list_experiments) + .service(get_applicable_variants) .service(get_experiment_handler) .service(ramp) .service(update_overrides) @@ -442,6 +446,49 @@ pub async fn conclude( Ok((updated_experiment, config_version_id)) } +#[get("/applicable-variants")] +async fn get_applicable_variants( + db_conn: DbConnection, + query_data: Query, +) -> superposition::Result { + let DbConnection(mut conn) = db_conn; + let query_data = query_data.into_inner(); + + let experiments = experiments::experiments + .filter(experiments::status.ne(ExperimentStatusType::CONCLUDED)) + .load::(&mut conn)?; + + let experiments = experiments.into_iter().filter(|exp| { + let is_empty = exp + .context + .as_object() + .map_or(false, |context| context.is_empty()); + is_empty + || jsonlogic::apply(&exp.context, &Value::Object(query_data.context.clone())) + == Ok(Value::Bool(true)) + }); + + 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.") + })?, + query_data.toss, + ) + .map_err(|e| { + log::error!("Unable to decide variant {e}"); + unexpected_error!("Something went wrong.") + })? { + variants.push(v) + } + } + + Ok(HttpResponse::Ok().json(variants)) +} + #[get("")] async fn list_experiments( req: HttpRequest, diff --git a/crates/experimentation_platform/src/api/experiments/helpers.rs b/crates/experimentation_platform/src/api/experiments/helpers.rs index 42cf962c..9658f89d 100644 --- a/crates/experimentation_platform/src/api/experiments/helpers.rs +++ b/crates/experimentation_platform/src/api/experiments/helpers.rs @@ -240,3 +240,31 @@ pub fn add_variant_dimension_to_ctx( pub fn extract_override_keys(overrides: &Map) -> HashSet { overrides.keys().map(String::from).collect() } + +pub 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()) +} diff --git a/crates/experimentation_platform/src/api/experiments/types.rs b/crates/experimentation_platform/src/api/experiments/types.rs index fdf41ba4..c968c03c 100644 --- a/crates/experimentation_platform/src/api/experiments/types.rs +++ b/crates/experimentation_platform/src/api/experiments/types.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use chrono::{DateTime, NaiveDateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; @@ -129,6 +131,46 @@ pub enum ContextBulkResponse { MOVE(ContextPutResp), } +/********** Applicable Variants API Type *************/ +#[derive(Debug, Deserialize)] +#[serde(try_from = "HashMap")] +pub struct ApplicableVariantsQuery { + pub context: Map, + pub toss: i8, +} + +impl TryFrom> for ApplicableVariantsQuery { + type Error = String; + fn try_from(value: HashMap) -> Result { + let mut value = value + .into_iter() + .map(|(key, value)| { + (key, value.parse().unwrap_or_else(|_| Value::String(value))) + }) + .collect::>(); + + let toss = value + .remove("toss") + .and_then(|toss| toss.as_i64()) + .and_then(|toss| { + if -1 <= toss && toss <= 100 { + Some(toss as i8) + } else { + None + } + }) + .ok_or_else(|| { + log::error!("toss should be a an interger between -1 and 100 (included)"); + String::from("toss should be a an interger between -1 and 100 (included)") + })?; + + Ok(Self { + toss, + context: value, + }) + } +} + /********** List API Filter Type *************/ #[derive(Deserialize, Debug, Clone)]