Skip to content

Commit

Permalink
Refactor event dispatchers
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkBiesheuvel committed Aug 7, 2024
1 parent 81d6ea1 commit 6e15cb0
Show file tree
Hide file tree
Showing 22 changed files with 347 additions and 490 deletions.
8 changes: 0 additions & 8 deletions examples/results-demo/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,6 @@ const FLAG_KEY: &str = "results_demo";
const ADD_TO_CART_EVENT_KEY: &str = "add_to_cart";
const PURCHASE_EVENT_KEY: &str = "purchase";

enum ProductCategory {
Games,
Controllers,
Headsets,
Mouses,
Keyboards,
}

/// Whether a random event does or doesn't happen
fn random_event_does_happen(chance: f32) -> bool {
random::<f32>() < chance
Expand Down
16 changes: 8 additions & 8 deletions optimizely/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
[package]
name = "optimizely"
version = "0.2.0"
version = "0.3.0"
edition = "2021"

[dependencies]
serde_json = "1.0.107"
serde_json = "1.0"
thiserror = "1.0"
error-stack = "0.3.1"
fasthash = "0.4.0"
log = "0.4.17"
error-stack = "0.5"
fasthash = "0.4"
log = "0.4"

[dependencies.serde]
version = "1.0.188"
version = "1.0"
features = ["derive"]

[dependencies.ureq]
version = "2.5.0"
version = "2.10"
optional = true

[dependencies.uuid]
version = "1.3.0"
version = "1.10"
features = ["v4", "fast-rng"]

[features]
Expand Down
9 changes: 2 additions & 7 deletions optimizely/src/client/initialization.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// External imports
use error_stack::{IntoReport, Result, ResultExt};
use error_stack::{Result, ResultExt};
use std::fs::File;
use std::io::Read;

Expand Down Expand Up @@ -44,13 +44,11 @@ impl Client {
// TODO: implement polling mechanism
let response = ureq::get(&url)
.call()
.into_report()
.change_context(ClientError::FailedRequest)?;

// Get response body
let content = response
.into_string()
.into_report()
.change_context(ClientError::FailedResponse)?;

// Use response to build Client
Expand All @@ -63,13 +61,10 @@ impl Client {
let mut content = String::new();

// Open file
let mut file = File::open(file_path)
.into_report()
.change_context(ClientError::FailedFileOpen)?;
let mut file = File::open(file_path).change_context(ClientError::FailedFileOpen)?;

// Read file content into String
file.read_to_string(&mut content)
.into_report()
.change_context(ClientError::FailedFileRead)?;

// Use file content to build Client
Expand Down
71 changes: 34 additions & 37 deletions optimizely/src/client/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ use fasthash::murmur3::hash32_with_seed as murmur3_hash;
use std::collections::HashMap;

// Imports from crate
use crate::conversion::Conversion;
use crate::datafile::{Experiment, FeatureFlag, Variation};
use crate::decision::{DecideOptions, Decision};

#[cfg(feature = "online")]
use crate::event_api;

// Imports from super
use super::Client;

Expand Down Expand Up @@ -75,6 +73,11 @@ impl UserContext<'_> {
self.attributes.insert(key, value);
}

/// Get the client instance
pub fn client(&self) -> &Client {
self.client
}

/// Get the id of a user
pub fn user_id(&self) -> &str {
self.user_id
Expand Down Expand Up @@ -106,21 +109,18 @@ impl UserContext<'_> {
pub fn track_event_with_properties_and_tags(
&self, event_key: &str, properties: HashMap<String, String>, tags: HashMap<String, String>,
) {
// Find the event key in the datafile
match self.client.datafile().event(event_key) {
Some(event) => {
log::debug!("Logging conversion event");

// Send out a decision event as a side effect
let user_id = self.user_id();
let account_id = self.client.datafile().account_id();
let event_id = event.id();

// Create event_api::Event to send to dispatcher
let conversion_event =
event_api::Event::conversion(account_id, user_id, event_id, event_key, properties, tags);
// Create conversion to send to dispatcher
let conversion = Conversion::new(event_key, event.id(), properties, tags);

// Ignore result of the send_decision function
self.client.event_dispatcher().send_event(conversion_event);
self.client
.event_dispatcher()
.send_conversion_event(self, conversion);
}
None => {
log::warn!("Event key does not exist in datafile");
Expand All @@ -129,13 +129,13 @@ impl UserContext<'_> {
}

/// Decide which variation to show to a user
pub fn decide<'b>(&self, flag_key: &'b str) -> Decision<'b> {
pub fn decide(&self, flag_key: &str) -> Decision {
let options = DecideOptions::default();
self.decide_with_options(flag_key, &options)
}

/// Decide which variation to show to a user
pub fn decide_with_options<'b>(&self, flag_key: &'b str, options: &DecideOptions) -> Decision<'b> {
pub fn decide_with_options(&self, flag_key: &str, options: &DecideOptions) -> Decision {
// Retrieve Flag object
let flag = match self.client.datafile().flag(flag_key) {
Some(flag) => flag,
Expand All @@ -147,10 +147,10 @@ impl UserContext<'_> {
};

// Only send decision events if the disable_decision_event option is false
let send_decision = !options.disable_decision_event;
let mut send_decision = !options.disable_decision_event;

// Get the selected variation for the given flag
match self.decide_variation_for_flag(flag, send_decision) {
let decision = match self.decide_variation_for_flag(flag, &mut send_decision) {
Some(variation) => {
// Unpack the variation and create Decision struct
Decision::new(flag_key, variation.is_feature_enabled(), variation.key())
Expand All @@ -159,40 +159,53 @@ impl UserContext<'_> {
// No experiment or rollout found, or user does not qualify for any
Decision::off(flag_key)
}
};

#[cfg(feature = "online")]
if send_decision {
self.client.event_dispatcher().send_decision_event(&self, decision.clone());
}

// Return
decision
}

fn decide_variation_for_flag<'a>(&'a self, flag: &'a FeatureFlag, send_decision: bool) -> Option<&Variation> {
fn decide_variation_for_flag(&self, flag: &FeatureFlag, send_decision: &mut bool) -> Option<&Variation> {
// Find first Experiment for which this user qualifies
let result = flag.experiments_ids().iter().find_map(|experiment_id| {
let experiment = self.client.datafile().experiment(experiment_id);

match experiment {
Some(experiment) => self.decide_variation_for_experiment(experiment, send_decision),
Some(experiment) => self.decide_variation_for_experiment(experiment),
None => None,
}
});

match result {
Some(_) => {
// A matching A/B test was found, send out any decisions
// Send out a decision event for an A/B Test
*send_decision &= true;

result
}
None => {
// Do not send any decision for a Rollout (Targeted Delivery)
*send_decision = false;

// No direct experiment found, let's look at the Rollout
let rollout = self.client.datafile().rollout(flag.rollout_id()).unwrap(); // TODO: remove unwrap

// Find the first experiment within the Rollout for which this user qualifies
rollout
.experiments()
.iter()
.find_map(|experiment| self.decide_variation_for_experiment(experiment, false))
.find_map(|experiment| self.decide_variation_for_experiment(experiment))
}
}
}

fn decide_variation_for_experiment<'a>(
&'a self, experiment: &'a Experiment, send_decision: bool,
&'a self, experiment: &'a Experiment
) -> Option<&Variation> {
// Use references for the ids
let user_id = self.user_id();
Expand All @@ -213,22 +226,6 @@ impl UserContext<'_> {

match result {
Some(variation_id) => {
if send_decision {
#[cfg(feature = "online")]
{
// Send out a decision event as a side effect
let account_id = self.client.datafile().account_id();
let campaign_id = experiment.campaign_id();

// Create event_api::Event to send to dispatcher
let decision_event =
event_api::Event::decision(account_id, user_id, campaign_id, experiment_id, variation_id);

// Ignore result of the send_decision function
self.client.event_dispatcher().send_event(decision_event);
}
}

// Find the variation belonging to this variation ID
experiment.variation(variation_id)
}
Expand Down
47 changes: 47 additions & 0 deletions optimizely/src/conversion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! A conversion event
use std::collections::HashMap;

/// A conversion event
#[derive(Debug)]
pub struct Conversion {
event_key: String,
event_id: String,
properties: HashMap<String, String>,
tags: HashMap<String, String>,
}

impl Conversion {
pub(crate) fn new<T: Into<String>>(
event_key: T, event_id: T, properties: HashMap<String, String>, tags: HashMap<String, String>,
) -> Conversion {
Conversion {
event_key: event_key.into(),
event_id: event_id.into(),
properties,
tags,
}
}
}

impl Conversion {
/// Get key
pub fn event_key(&self) -> &str {
&self.event_key
}

/// Get id
pub fn event_id(&self) -> &str {
&self.event_id
}

/// Get properties
pub fn properties(&self) -> &HashMap<String, String> {
&self.properties
}

/// Get tags
pub fn tags(&self) -> &HashMap<String, String> {
&self.tags
}
}
8 changes: 3 additions & 5 deletions optimizely/src/datafile.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
//! Parsing the Optimizely datafile
// External imports
use error_stack::{IntoReport, Result, ResultExt};
use error_stack::{Result, ResultExt};

// Relative imports of sub modules
use environment::Environment;
pub use error::DatafileError;
use event::Event;
pub(crate) use event::Event;
pub(crate) use experiment::Experiment;
pub(crate) use feature_flag::FeatureFlag;
use rollout::Rollout;
Expand Down Expand Up @@ -39,9 +39,7 @@ impl Datafile {
/// Construct a new Datafile from a string containing a JSON document
pub fn build(content: &str) -> Result<Datafile, DatafileError> {
// Parse the JSON content via Serde into Rust structs
let environment: Environment = serde_json::from_str(content)
.into_report()
.change_context(DatafileError::InvalidJson)?;
let environment: Environment = serde_json::from_str(content).change_context(DatafileError::InvalidJson)?;

Ok(Datafile(environment))
}
Expand Down
1 change: 0 additions & 1 deletion optimizely/src/datafile/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ impl Event {
}

/// Getter for `id` field
#[allow(dead_code)]
pub fn id(&self) -> &str {
&self.id
}
Expand Down
39 changes: 30 additions & 9 deletions optimizely/src/decision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,26 @@
pub use decide_options::DecideOptions;
mod decide_options;

/// Decision for a specfic user and feature flag
#[derive(Debug)]
pub struct Decision<'a> {
flag_key: &'a str,
enabled: bool,
/// Decision for a specific user and feature flag
#[derive(Debug, Clone)]
pub struct Decision {
flag_key: String,
campaign_id: String,
experiment_id: String,
variation_id: String,
variation_key: String,
enabled: bool,
}

impl Decision<'_> {
pub(crate) fn new<T: Into<String>>(flag_key: &str, enabled: bool, variation_key: T) -> Decision {
impl Decision {
pub(crate) fn new<T: Into<String>>(flag_key: T, enabled: bool, variation_key: T) -> Decision {
Decision {
flag_key,
flag_key: flag_key.into(),
enabled,
variation_key: variation_key.into(),
campaign_id: "".into(),
experiment_id: "".into(),
variation_id: "".into(),
}
}

Expand All @@ -27,7 +33,7 @@ impl Decision<'_> {

/// Get the flag key for which this decision was made
pub fn flag_key(&self) -> &str {
self.flag_key
&self.flag_key
}

/// Get whether the flag should be enabled or disable
Expand All @@ -39,4 +45,19 @@ impl Decision<'_> {
pub fn variation_key(&self) -> &str {
&self.variation_key
}

/// Get the campaign ID
pub fn campaign_id(&self) -> &str {
&self.campaign_id
}

/// Get the experiment ID
pub fn experiment_id(&self) -> &str {
&self.experiment_id
}

/// Get the variation ID that was decided
pub fn variation_id(&self) -> &str {
&self.variation_id
}
}
Loading

0 comments on commit 6e15cb0

Please sign in to comment.