use murmur3::murmur3_32 as murmur3_hash;
#[cfg(feature = "online")]
use std::collections::HashMap;
use crate::datafile::{Experiment, FeatureFlag};
#[cfg(feature = "online")]
use crate::Conversion;
use crate::{AttributeValue, DecideOptions, Decision, UserAttribute, UserAttributeMap};
use super::{Client, DatafileReadGuard};
const HASH_SEED: u32 = 1;
const MAX_HASH_VALUE: f64 = 4_294_967_296_f64;
const MAX_RANGE_VALUE: f64 = 10_000_f64;
pub struct UserContext<'a> {
client: &'a Client,
user_id: &'a str,
user_attributes: UserAttributeMap,
}
impl<'a> UserContext<'a> {
pub(crate) fn new(client: &'a Client, user_id: &'a str) -> UserContext<'a> {
UserContext {
client,
user_id,
user_attributes: UserAttributeMap::default(),
}
}
pub fn set_attribute(&mut self, key: impl Into<String>, value: AttributeValue) {
let key = key.into();
if let Some(datafile_attribute) = self.client.datafile().attribute(&key) {
let user_attribute = UserAttribute::from_attribute_and_value(datafile_attribute, value);
self.user_attributes.insert(key, user_attribute);
}
}
}
impl UserContext<'_> {
pub fn client(&self) -> &Client {
self.client
}
pub fn user_id(&self) -> &str {
self.user_id
}
pub fn user_attributes(&self) -> Vec<&UserAttribute> {
self.user_attributes.values().collect()
}
#[cfg(feature = "online")]
pub fn track_event(&self, event_key: &str) {
let properties = HashMap::default();
let tags = HashMap::default();
self.track_event_with_properties_and_tags(event_key, properties, tags)
}
#[cfg(feature = "online")]
pub fn track_event_with_properties(&self, event_key: &str, properties: HashMap<String, String>) {
let tags = HashMap::default();
self.track_event_with_properties_and_tags(event_key, properties, tags)
}
#[cfg(feature = "online")]
pub fn track_event_with_properties_and_tags(
&self, event_key: &str, properties: HashMap<String, String>, tags: HashMap<String, String>,
) {
if let Some(event) = self.client.datafile().event(event_key) {
log::debug!("Logging conversion event");
let conversion = Conversion::new(event_key, event.id(), properties, tags);
self.client
.event_dispatcher()
.send_conversion_event(self, conversion);
}
}
pub fn decide(&self, flag_key: &str) -> Decision {
let options = self.client().default_decide_options();
self.decide_with_options(flag_key, options)
}
pub fn decide_with_options(&self, flag_key: &str, options: &DecideOptions) -> Decision {
let datafile = self.client.datafile();
let flag = match datafile.flag(flag_key) {
Some(flag) => flag,
None => {
return Decision::off(flag_key);
}
};
let mut send_decision = !options.disable_decision_event;
let decision = self
.decide_for_flag(&datafile, flag, &mut send_decision)
.unwrap_or_else(|| Decision::off(flag_key));
#[cfg(feature = "online")]
if send_decision {
self.client
.event_dispatcher()
.send_decision_event(self, decision.clone());
}
decision
}
fn decide_for_flag(
&self, datafile: &DatafileReadGuard<'_>, flag: &FeatureFlag, send_decision: &mut bool,
) -> Option<Decision> {
let decision = flag
.experiments_ids()
.iter()
.filter_map(|experiment_id| datafile.experiment(experiment_id))
.find(|experiment| self.is_in_target_audience(datafile, experiment))
.and_then(|experiment| self.decide_for_experiment(flag, experiment));
match decision {
Some(_) => {
*send_decision &= true;
decision
}
None => {
*send_decision = false;
let rollout = datafile.rollout(flag.rollout_id())?;
rollout
.experiments()
.iter()
.find(|experiment| self.is_in_target_audience(datafile, experiment))
.and_then(|experiment| self.decide_for_experiment(flag, experiment))
}
}
}
fn decide_for_experiment(&self, flag: &FeatureFlag, experiment: &Experiment) -> Option<Decision> {
let user_id = self.user_id();
let experiment_id = experiment.id();
let bucketing_key = format!("{user_id}{experiment_id}");
let mut bytes = bucketing_key.as_bytes();
let hash_value = murmur3_hash(&mut bytes, HASH_SEED).unwrap();
let bucket_value = ((hash_value as f64) / MAX_HASH_VALUE * MAX_RANGE_VALUE) as u64;
experiment
.traffic_allocation()
.variation(bucket_value)
.and_then(|variation_id| experiment.variation(variation_id))
.map(|variation| Decision::from(flag, experiment, variation))
}
fn is_in_target_audience(&self, datafile: &DatafileReadGuard<'_>, experiment: &Experiment) -> bool {
let audience_ids = experiment.audience_ids();
if audience_ids.is_empty() {
return true;
}
audience_ids.iter().any(|audience_id| {
let audience = match datafile.audience(audience_id.as_ref()) {
Some(audience) => audience,
None => {
return false;
}
};
audience.condition().does_match(&self.user_attributes)
})
}
}