Skip to content

Commit

Permalink
feat: Add get_applicable_variants as expt endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
ayushjain17 committed Sep 12, 2024
1 parent 606099a commit 99c74ff
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 4 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/experimentation_platform/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
63 changes: 59 additions & 4 deletions crates/experimentation_platform/src/api/experiments/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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::ApplicableVariantsRequest,
db::{
models::{EventLog, Experiment, ExperimentStatusType},
schema::{event_log::dsl as event_log, experiments::dsl as experiments},
},
};

use serde_json::{json, Map, Value};
Expand All @@ -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(
Expand Down Expand Up @@ -442,6 +446,57 @@ pub async fn conclude(
Ok((updated_experiment, config_version_id))
}

#[post("/applicable_variants")]
async fn get_applicable_variants(
db_conn: DbConnection,
req: Json<ApplicableVariantsRequest>,
) -> superposition::Result<HttpResponse> {
let DbConnection(mut conn) = db_conn;
let payload = req.into_inner();

let experiments = experiments::experiments
.filter(experiments::status.ne(ExperimentStatusType::CONCLUDED))
.load::<Experiment>(&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))
}

#[get("")]
async fn list_experiments(
req: HttpRequest,
Expand Down
28 changes: 28 additions & 0 deletions crates/experimentation_platform/src/api/experiments/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,31 @@ pub fn add_variant_dimension_to_ctx(
pub fn extract_override_keys(overrides: &Map<String, Value>) -> HashSet<String> {
overrides.keys().map(String::from).collect()
}

pub fn decide_variant(
traffic: u8,
applicable_variants: Vec<Variant>,
toss: i8,
) -> Result<Option<Variant>, 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::<Vec<i8>>();
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())
}
7 changes: 7 additions & 0 deletions crates/experimentation_platform/src/api/experiments/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,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)]
Expand Down

0 comments on commit 99c74ff

Please sign in to comment.