use std::collections::HashMap;
use std::hash::{DefaultHasher, Hash, Hasher};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use superposition_types::database::models::experimentation::{
Bucket, Buckets, Experiment, ExperimentGroup, ExperimentStatusType, GroupType,
Variant, Variants,
};
use superposition_types::experimental::{Experimental, ExperimentalVariants};
use superposition_types::PrefixList;
use superposition_types::{logic::evaluate_local_cohorts, Condition, DimensionInfo};
use std::fmt;
pub trait MapError<T> {
fn map_err_to_string(self) -> Result<T, String>;
}
impl<T, E> MapError<T> for Result<T, E>
where
E: fmt::Display,
{
fn map_err_to_string(self) -> Result<T, String> {
self.map_err(|e| e.to_string())
}
}
#[derive(Serialize, Deserialize, Clone, Debug, uniffi::Record)]
pub struct FfiExperiment {
pub id: String,
pub traffic_percentage: u8,
pub variants: Variants,
pub context: Condition,
pub status: ExperimentStatusType,
}
impl Experimental for FfiExperiment {
fn get_condition(&self) -> &Condition {
&self.context
}
}
impl ExperimentalVariants for FfiExperiment {
fn get_variants_mut(&mut self) -> &mut Vec<Variant> {
&mut self.variants
}
}
impl From<Experiment> for FfiExperiment {
fn from(experiment: Experiment) -> Self {
Self {
id: experiment.id.to_string(),
traffic_percentage: *experiment.traffic_percentage,
variants: experiment.variants,
context: experiment.context,
status: experiment.status,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, uniffi::Record)]
pub struct FfiExperimentGroup {
pub id: String,
pub context: Condition,
pub traffic_percentage: u8,
pub member_experiment_ids: Vec<String>,
pub group_type: GroupType,
pub buckets: Buckets,
}
impl Experimental for FfiExperimentGroup {
fn get_condition(&self) -> &Condition {
&self.context
}
}
impl From<ExperimentGroup> for FfiExperimentGroup {
fn from(experiment_group: ExperimentGroup) -> Self {
Self {
id: experiment_group.id.to_string(),
context: experiment_group.context,
traffic_percentage: *experiment_group.traffic_percentage,
member_experiment_ids: experiment_group
.member_experiment_ids
.iter()
.map(|id| id.to_string())
.collect(),
group_type: experiment_group.group_type,
buckets: experiment_group.buckets,
}
}
}
#[derive(Serialize, Deserialize, Debug, uniffi::Record)]
pub struct ExperimentationArgs {
pub experiments: Vec<FfiExperiment>,
pub experiment_groups: Vec<FfiExperimentGroup>,
pub targeting_key: String,
}
pub type Experiments = Vec<FfiExperiment>;
pub type ExperimentGroups = Vec<FfiExperimentGroup>;
#[derive(Debug, Clone, uniffi::Record)]
pub struct ExperimentConfig {
pub experiments: Experiments,
pub experiment_groups: ExperimentGroups,
}
pub fn get_applicable_variants(
dimensions_info: &HashMap<String, DimensionInfo>,
experiments: Experiments,
experiment_groups: &ExperimentGroups,
query_data: &Map<String, Value>,
identifier: &str,
prefix: Option<Vec<String>>,
exclude_prefix: Option<Vec<String>>,
) -> Vec<String> {
let context = evaluate_local_cohorts(dimensions_info, query_data);
let buckets =
get_applicable_buckets_from_group(experiment_groups, &context, identifier);
let experiments: HashMap<String, FfiExperiment> =
get_satisfied_experiments(experiments, &context, prefix, exclude_prefix)
.into_iter()
.map(|exp| (exp.id.clone(), exp))
.collect();
get_applicable_variants_from_group_response(&experiments, &context, &buckets)
}
pub fn get_applicable_buckets_from_group(
experiment_groups: &ExperimentGroups,
context: &Map<String, Value>,
identifier: &str,
) -> Vec<(usize, Bucket)> {
if identifier.is_empty() {
return vec![];
}
experiment_groups
.iter()
.filter_map(|exp_group| {
let hashed_percentage = calculate_bucket_index(identifier, &exp_group.id);
log::info!(
"Identifier: {}, Experiment Group ID: {}, Hashed Percentage: {}",
identifier,
exp_group.id,
hashed_percentage
);
let exp_context = &exp_group.context;
let valid_context = superposition_types::apply(exp_context, context);
let res =
valid_context && exp_group.traffic_percentage >= hashed_percentage as u8;
res.then_some(
exp_group
.buckets
.get(hashed_percentage)
.and_then(Clone::clone),
)
.flatten()
.and_then(|b| {
if exp_group.group_type == GroupType::SystemGenerated {
Some((hashed_percentage, b))
} else if exp_group.traffic_percentage > 0 {
Some((
(hashed_percentage * 100) / exp_group.traffic_percentage as usize,
b,
))
} else {
None
}
})
})
.collect()
}
pub fn get_applicable_variants_from_group_response(
experiments: &HashMap<String, FfiExperiment>,
context: &Map<String, Value>,
bucket_response: &[(usize, Bucket)],
) -> Vec<String> {
bucket_response
.iter()
.filter_map(|(toss, bucket)| {
experiments.get(&bucket.experiment_id).and_then(|exp| {
let valid_context = superposition_types::apply(&exp.context, context);
let res = valid_context
&& (exp.traffic_percentage as usize * exp.variants.len()) >= *toss;
res.then_some(bucket.variant_id.clone())
})
})
.collect()
}
#[inline]
pub fn calculate_bucket_index(identifier: &str, group_id: &str) -> usize {
let mut hasher = DefaultHasher::new();
(identifier, group_id).hash(&mut hasher);
(hasher.finish() % 100) as usize
}
pub fn get_satisfied_experiments(
mut experiments: Experiments,
context: &Map<String, Value>,
filter_prefixes: Option<Vec<String>>,
filter_exclude_prefixes: Option<Vec<String>>,
) -> Experiments {
let prefix_list = PrefixList::from(filter_prefixes);
let exclude_prefix_list = PrefixList::from(filter_exclude_prefixes);
if !prefix_list.is_empty() || !exclude_prefix_list.is_empty() {
experiments = FfiExperiment::filter_keys_by_prefix(
experiments,
&prefix_list,
&exclude_prefix_list,
);
}
if !context.is_empty() {
experiments = FfiExperiment::get_satisfied(experiments, context);
}
experiments
}
pub fn filter_experiments_by_context(
mut experiments: Experiments,
context: &Map<String, Value>,
filter_prefixes: Option<Vec<String>>,
filter_exclude_prefixes: Option<Vec<String>>,
) -> Experiments {
let prefix_list = PrefixList::from(filter_prefixes);
let exclude_prefix_list = PrefixList::from(filter_exclude_prefixes);
if !prefix_list.is_empty() || !exclude_prefix_list.is_empty() {
experiments = FfiExperiment::filter_keys_by_prefix(
experiments,
&prefix_list,
&exclude_prefix_list,
);
}
if !context.is_empty() {
experiments = FfiExperiment::filter_by_eval(experiments, context);
}
experiments
}