This repository has been archived by the owner on Jun 21, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d040b94
commit 278fab7
Showing
6 changed files
with
2,339 additions
and
10 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
use sha1::{Digest, Sha1}; | ||
|
||
use crate::flag_definitions::{FeatureFlag, FlagGroupType}; | ||
|
||
#[derive(Debug, PartialEq, Eq)] | ||
pub struct FeatureFlagMatch { | ||
pub matches: bool, | ||
pub variant: Option<String>, | ||
//reason | ||
//condition_index | ||
//payload | ||
} | ||
|
||
#[derive(Debug)] | ||
pub struct FeatureFlagMatcher { | ||
// pub flags: Vec<FeatureFlag>, | ||
pub distinct_id: String, | ||
} | ||
|
||
const LONG_SCALE: u64 = 0xfffffffffffffff; | ||
|
||
impl FeatureFlagMatcher { | ||
|
||
pub fn new(distinct_id: String) -> Self { | ||
FeatureFlagMatcher { | ||
// flags, | ||
distinct_id, | ||
} | ||
} | ||
|
||
pub fn get_match(&self, feature_flag: &FeatureFlag) -> FeatureFlagMatch { | ||
|
||
if self.hashed_identifier(feature_flag).is_none() { | ||
return FeatureFlagMatch { | ||
matches: false, | ||
variant: None, | ||
}; | ||
} | ||
|
||
// TODO: super groups | ||
// TODO: Variant overrides condition sort | ||
|
||
for (index, condition) in feature_flag.get_conditions().iter().enumerate() { | ||
let (is_match, evaluation_reason) = self.is_condition_match(feature_flag, condition, index); | ||
|
||
if is_match { | ||
|
||
let variant = match condition.variant.clone() { | ||
Some(variant_override) => { | ||
if feature_flag.get_variants().iter().any(|v| v.key == variant_override) { | ||
Some(variant_override) | ||
} else { | ||
self.get_matching_variant(feature_flag) | ||
} | ||
} | ||
None => { | ||
self.get_matching_variant(feature_flag) | ||
} | ||
}; | ||
|
||
// let payload = self.get_matching_payload(is_match, variant, feature_flag); | ||
return FeatureFlagMatch { | ||
matches: true, | ||
variant, | ||
}; | ||
} | ||
} | ||
FeatureFlagMatch { | ||
matches: false, | ||
variant: None, | ||
} | ||
} | ||
|
||
pub fn is_condition_match(&self, feature_flag: &FeatureFlag, condition: &FlagGroupType, _index: usize) -> (bool, String) { | ||
let rollout_percentage = condition.rollout_percentage.unwrap_or(100.0); | ||
let mut condition_match = true; | ||
if condition.properties.is_some() { | ||
// TODO: Handle matching conditions | ||
if !condition.properties.as_ref().unwrap().is_empty() { | ||
condition_match = false; | ||
} | ||
} | ||
|
||
if !condition_match { | ||
return (false, "NO_CONDITION_MATCH".to_string()); | ||
} else if rollout_percentage == 100.0 { | ||
// TODO: Check floating point schenanigans if any | ||
return (true, "CONDITION_MATCH".to_string()); | ||
} | ||
|
||
if self.get_hash(feature_flag, "") > (rollout_percentage / 100.0) { | ||
return (false, "OUT_OF_ROLLOUT_BOUND".to_string()); | ||
} | ||
|
||
(true, "CONDITION_MATCH".to_string()) | ||
|
||
} | ||
|
||
pub fn hashed_identifier(&self, feature_flag: &FeatureFlag) -> Option<String> { | ||
if feature_flag.get_group_type_index().is_none() { | ||
// TODO: Use hash key overrides for experience continuity | ||
Some(self.distinct_id.clone()) | ||
} else { | ||
// TODO: Handle getting group key | ||
Some("".to_string()) | ||
} | ||
} | ||
|
||
/// This function takes a identifier and a feature flag key and returns a float between 0 and 1. | ||
/// Given the same identifier and key, it'll always return the same float. These floats are | ||
/// uniformly distributed between 0 and 1, so if we want to show this feature to 20% of traffic | ||
/// we can do _hash(key, identifier) < 0.2 | ||
pub fn get_hash(&self, feature_flag: &FeatureFlag, salt: &str) -> f64 { | ||
// check if hashed_identifier is None | ||
let hashed_identifier = self.hashed_identifier(feature_flag).expect("hashed_identifier is None when computing hash"); | ||
let hash_key = format!("{}.{}{}", feature_flag.key, hashed_identifier, salt); | ||
let mut hasher = Sha1::new(); | ||
hasher.update(hash_key.as_bytes()); | ||
let result = hasher.finalize(); | ||
// :TRICKY: Convert the first 15 characters of the digest to a hexadecimal string | ||
// not sure if this is correct, padding each byte as 2 characters | ||
let hex_str: String = result.iter().map(|byte| format!("{:02x}", byte)).collect::<String>()[..15].to_string(); | ||
let hash_val = u64::from_str_radix(&hex_str, 16).unwrap(); | ||
|
||
hash_val as f64 / LONG_SCALE as f64 | ||
} | ||
|
||
pub fn get_matching_variant(&self, feature_flag: &FeatureFlag) -> Option<String> { | ||
let hash = self.get_hash(feature_flag, "variant"); | ||
let mut total_percentage = 0.0; | ||
|
||
for variant in feature_flag.get_variants() { | ||
total_percentage += variant.rollout_percentage / 100.0; | ||
if hash < total_percentage { | ||
return Some(variant.key.clone()); | ||
} | ||
} | ||
None | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,7 @@ use serde_json::json; | |
use std::sync::Arc; | ||
|
||
use crate::{ | ||
flag_definitions, redis::{Client, RedisClient}, team::{self, Team} | ||
flag_definitions::{self, FeatureFlag}, redis::{Client, RedisClient}, team::{self, Team} | ||
}; | ||
use rand::{distributions::Alphanumeric, Rng}; | ||
|
||
|
@@ -89,3 +89,35 @@ pub fn setup_redis_client(url: Option<String>) -> Arc<RedisClient> { | |
let client = RedisClient::new(redis_url).expect("Failed to create redis client"); | ||
Arc::new(client) | ||
} | ||
|
||
pub fn create_flag_from_json(json_value: Option<String>) -> Vec<FeatureFlag> { | ||
|
||
let payload = match json_value { | ||
Some(value) => value, | ||
None => json!([{ | ||
"id": 1, | ||
"key": "flag1", | ||
"name": "flag1 description", | ||
"active": true, | ||
"deleted": false, | ||
"team_id": 1, | ||
"filters": { | ||
"groups": [ | ||
{ | ||
"properties": [ | ||
{ | ||
"key": "email", | ||
"value": "[email protected]", | ||
"type": "person", | ||
}, | ||
], | ||
"rollout_percentage": 50, | ||
}, | ||
], | ||
}, | ||
}]).to_string(), | ||
}; | ||
|
||
let flags: Vec<FeatureFlag> = serde_json::from_str(&payload).expect("Failed to parse data to flags list"); | ||
flags | ||
} |
Oops, something went wrong.