Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add get_applicable_variants as expt endpoint #210

Merged
merged 1 commit into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
55 changes: 51 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::ApplicableVariantsQuery,
db::{
models::{EventLog, Experiment, ExperimentStatusType},
schema::{event_log::dsl as event_log, experiments::dsl as experiments},
},
};

use serde_json::{json, Map, Value};
Expand All @@ -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)
Expand Down Expand Up @@ -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<ApplicableVariantsQuery>,
) -> superposition::Result<HttpResponse> {
let DbConnection(mut conn) = db_conn;
let query_data = query_data.into_inner();

let experiments = experiments::experiments
.filter(experiments::status.ne(ExperimentStatusType::CONCLUDED))
.load::<Experiment>(&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,
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())
}
42 changes: 42 additions & 0 deletions crates/experimentation_platform/src/api/experiments/types.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashMap;

use chrono::{DateTime, NaiveDateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
Expand Down Expand Up @@ -129,6 +131,46 @@ pub enum ContextBulkResponse {
MOVE(ContextPutResp),
}

/********** Applicable Variants API Type *************/
#[derive(Debug, Deserialize)]
#[serde(try_from = "HashMap<String,String>")]
pub struct ApplicableVariantsQuery {
pub context: Map<String, Value>,
pub toss: i8,
}

impl TryFrom<HashMap<String, String>> for ApplicableVariantsQuery {
type Error = String;
fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
let mut value = value
.into_iter()
.map(|(key, value)| {
(key, value.parse().unwrap_or_else(|_| Value::String(value)))
})
.collect::<Map<_, _>>();

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(|| {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't toss be optional and default should be -1 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_applicable_variant is based on toss itself, toss is mandatory here

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)]
Expand Down
Loading