From f28466a0f9699441109060ba1ba4e7baf3705a8f Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Mon, 10 Jun 2024 10:12:32 +0100 Subject: [PATCH] feat(flags): Match flags on rollout percentage (#45) --- Cargo.lock | 1 + feature-flags/Cargo.toml | 1 + feature-flags/src/flag_definitions.rs | 81 +- feature-flags/src/flag_matching.rs | 161 +++ feature-flags/src/lib.rs | 1 + feature-flags/src/team.rs | 1 + feature-flags/src/test_utils.rs | 35 +- .../tests/test_flag_matching_consistency.rs | 1209 +++++++++++++++++ 8 files changed, 1454 insertions(+), 36 deletions(-) create mode 100644 feature-flags/src/flag_matching.rs create mode 100644 feature-flags/tests/test_flag_matching_consistency.rs diff --git a/Cargo.lock b/Cargo.lock index 8642ade..b9f226b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -709,6 +709,7 @@ dependencies = [ "serde", "serde-pickle", "serde_json", + "sha1", "thiserror", "tokio", "tracing", diff --git a/feature-flags/Cargo.toml b/feature-flags/Cargo.toml index 1e0c111..4993930 100644 --- a/feature-flags/Cargo.toml +++ b/feature-flags/Cargo.toml @@ -25,6 +25,7 @@ serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } serde-pickle = { version = "1.1.1"} +sha1 = "0.10.6" [lints] workspace = true diff --git a/feature-flags/src/flag_definitions.rs b/feature-flags/src/flag_definitions.rs index 29ec8d8..1f4582c 100644 --- a/feature-flags/src/flag_definitions.rs +++ b/feature-flags/src/flag_definitions.rs @@ -1,4 +1,4 @@ -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use std::sync::Arc; use tracing::instrument; @@ -13,44 +13,30 @@ pub const TEAM_FLAGS_CACHE_PREFIX: &str = "posthog:1:team_feature_flags_"; // TODO: Hmm, revisit when dealing with groups, but seems like // ideal to just treat it as a u8 and do our own validation on top -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize)] pub enum GroupTypeIndex {} -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum OperatorType { - #[serde(rename = "exact")] Exact, - #[serde(rename = "is_not")] IsNot, - #[serde(rename = "icontains")] Icontains, - #[serde(rename = "not_icontains")] NotIcontains, - #[serde(rename = "regex")] Regex, - #[serde(rename = "not_regex")] NotRegex, - #[serde(rename = "gt")] Gt, - #[serde(rename = "lt")] Lt, - #[serde(rename = "gte")] Gte, - #[serde(rename = "lte")] Lte, - #[serde(rename = "is_set")] IsSet, - #[serde(rename = "is_not_set")] IsNotSet, - #[serde(rename = "is_date_exact")] IsDateExact, - #[serde(rename = "is_date_after")] IsDateAfter, - #[serde(rename = "is_date_before")] IsDateBefore, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize)] pub struct PropertyFilter { pub key: String, pub value: serde_json::Value, @@ -60,28 +46,28 @@ pub struct PropertyFilter { pub group_type_index: Option, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize)] pub struct FlagGroupType { pub properties: Option>, - pub rollout_percentage: Option, + pub rollout_percentage: Option, pub variant: Option, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize)] pub struct MultivariateFlagVariant { pub key: String, pub name: Option, - pub rollout_percentage: f32, + pub rollout_percentage: f64, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize)] pub struct MultivariateFlagOptions { pub variants: Vec, } // TODO: test name with https://www.fileformat.info/info/charset/UTF-16/list.htm values, like '𝖕𝖗𝖔𝖕𝖊𝖗𝖙𝖞': `𝓿𝓪𝓵𝓾𝓮` -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize)] pub struct FlagFilters { pub groups: Vec, pub multivariate: Option, @@ -90,7 +76,7 @@ pub struct FlagFilters { pub super_groups: Option>, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize)] pub struct FeatureFlag { pub id: i64, pub team_id: i64, @@ -105,15 +91,31 @@ pub struct FeatureFlag { pub ensure_experience_continuity: bool, } -#[derive(Debug, Deserialize, Serialize)] +impl FeatureFlag { + pub fn get_group_type_index(&self) -> Option { + self.filters.aggregation_group_type_index + } + + pub fn get_conditions(&self) -> &Vec { + &self.filters.groups + } + + pub fn get_variants(&self) -> Vec { + self.filters + .multivariate + .clone() + .map_or(vec![], |m| m.variants) + } +} + +#[derive(Debug, Deserialize)] pub struct FeatureFlagList { pub flags: Vec, } impl FeatureFlagList { - /// Returns feature flags given a team_id - + /// Returns feature flags from redis given a team_id #[instrument(skip_all)] pub async fn from_redis( client: Arc, @@ -126,6 +128,8 @@ impl FeatureFlagList { .map_err(|e| match e { CustomRedisError::NotFound => FlagError::TokenValidationError, CustomRedisError::PickleError(_) => { + // TODO: Implement From trait for FlagError so we don't need to map + // CustomRedisError ourselves tracing::error!("failed to fetch data: {}", e); println!("failed to fetch data: {}", e); FlagError::DataParsingError @@ -150,8 +154,6 @@ impl FeatureFlagList { #[cfg(test)] mod tests { - use rand::Rng; - use super::*; use crate::test_utils::{ insert_flags_for_team_in_redis, insert_new_team_in_redis, setup_redis_client, @@ -161,7 +163,9 @@ mod tests { async fn test_fetch_flags_from_redis() { let client = setup_redis_client(None); - let team = insert_new_team_in_redis(client.clone()).await.unwrap(); + let team = insert_new_team_in_redis(client.clone()) + .await + .expect("Failed to insert team"); insert_flags_for_team_in_redis(client.clone(), team.id, None) .await @@ -169,13 +173,20 @@ mod tests { let flags_from_redis = FeatureFlagList::from_redis(client.clone(), team.id) .await - .unwrap(); + .expect("Failed to fetch flags from redis"); assert_eq!(flags_from_redis.flags.len(), 1); - let flag = flags_from_redis.flags.get(0).unwrap(); + let flag = flags_from_redis.flags.get(0).expect("Empty flags in redis"); assert_eq!(flag.key, "flag1"); assert_eq!(flag.team_id, team.id); assert_eq!(flag.filters.groups.len(), 1); - assert_eq!(flag.filters.groups[0].properties.as_ref().unwrap().len(), 1); + assert_eq!( + flag.filters.groups[0] + .properties + .as_ref() + .expect("Properties don't exist on flag") + .len(), + 1 + ); } #[tokio::test] diff --git a/feature-flags/src/flag_matching.rs b/feature-flags/src/flag_matching.rs new file mode 100644 index 0000000..c59b594 --- /dev/null +++ b/feature-flags/src/flag_matching.rs @@ -0,0 +1,161 @@ +use crate::flag_definitions::{FeatureFlag, FlagGroupType}; +use sha1::{Digest, Sha1}; +use std::fmt::Write; + +#[derive(Debug, PartialEq, Eq)] +pub struct FeatureFlagMatch { + pub matches: bool, + pub variant: Option, + //reason + //condition_index + //payload +} + +// TODO: Rework FeatureFlagMatcher - python has a pretty awkward interface, where we pass in all flags, and then again +// the flag to match. I don't think there's any reason anymore to store the flags in the matcher, since we can just +// pass the flag to match directly to the get_match method. This will also make the matcher more stateless. +// Potentially, we could also make the matcher a long-lived object, with caching for group keys and such. +// It just takes in the flag and distinct_id and returns the match... +// Or, make this fully stateless +// and have a separate cache struct for caching group keys, cohort definitions, etc. - and check size, if we can keep it in memory +// for all teams. If not, we can have a LRU cache, or a cache that stores only the most recent N keys. +// But, this can be a future refactor, for now just focusing on getting the basic matcher working, write lots and lots of tests +// and then we can easily refactor stuff around. +#[derive(Debug)] +pub struct FeatureFlagMatcher { + // pub flags: Vec, + 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 for early access + // 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 { + // TODO: This is a bit awkward, we should handle overrides only when variants exist. + 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 { + 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().fold(String::new(), |mut acc, byte| { + let _ = write!(acc, "{:02x}", byte); + acc + })[..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 { + 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 + } +} diff --git a/feature-flags/src/lib.rs b/feature-flags/src/lib.rs index 0352c21..edc2a29 100644 --- a/feature-flags/src/lib.rs +++ b/feature-flags/src/lib.rs @@ -1,6 +1,7 @@ pub mod api; pub mod config; pub mod flag_definitions; +pub mod flag_matching; pub mod redis; pub mod router; pub mod server; diff --git a/feature-flags/src/team.rs b/feature-flags/src/team.rs index ac62ea9..e872aa4 100644 --- a/feature-flags/src/team.rs +++ b/feature-flags/src/team.rs @@ -42,6 +42,7 @@ impl Team { } })?; + // TODO: Consider an LRU cache for teams as well, with small TTL to skip redis/pg lookups let team: Team = serde_json::from_str(&serialized_team).map_err(|e| { tracing::error!("failed to parse data to team: {}", e); FlagError::DataParsingError diff --git a/feature-flags/src/test_utils.rs b/feature-flags/src/test_utils.rs index 0cefb7e..92bc8a4 100644 --- a/feature-flags/src/test_utils.rs +++ b/feature-flags/src/test_utils.rs @@ -3,7 +3,7 @@ use serde_json::json; use std::sync::Arc; use crate::{ - flag_definitions, + flag_definitions::{self, FeatureFlag}, redis::{Client, RedisClient}, team::{self, Team}, }; @@ -91,3 +91,36 @@ pub fn setup_redis_client(url: Option) -> Arc { let client = RedisClient::new(redis_url).expect("Failed to create redis client"); Arc::new(client) } + +pub fn create_flag_from_json(json_value: Option) -> Vec { + 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": "a@b.com", + "type": "person", + }, + ], + "rollout_percentage": 50, + }, + ], + }, + }]) + .to_string(), + }; + + let flags: Vec = + serde_json::from_str(&payload).expect("Failed to parse data to flags list"); + flags +} diff --git a/feature-flags/tests/test_flag_matching_consistency.rs b/feature-flags/tests/test_flag_matching_consistency.rs new file mode 100644 index 0000000..4a24b0e --- /dev/null +++ b/feature-flags/tests/test_flag_matching_consistency.rs @@ -0,0 +1,1209 @@ +/// These tests are common between all libraries doing local evaluation of feature flags. +/// This ensures there are no mismatches between implementations. +use feature_flags::flag_matching::{FeatureFlagMatch, FeatureFlagMatcher}; + +use feature_flags::test_utils::create_flag_from_json; +use serde_json::json; + +#[test] +fn it_is_consistent_with_rollout_calculation_for_simple_flags() { + let flags = create_flag_from_json(Some( + json!([{ + "id": 1, + "key": "simple-flag", + "name": "Simple flag", + "active": true, + "deleted": false, + "team_id": 1, + "filters": { + "groups": [ + { + "properties": [], + "rollout_percentage": 45, + }, + ], + }, + }]) + .to_string(), + )); + + let results = vec![ + false, true, true, false, true, false, false, true, false, true, false, true, true, false, + true, false, false, false, true, true, false, true, false, false, true, false, true, true, + false, false, false, true, true, true, true, false, false, false, false, false, false, + true, true, false, true, true, false, false, false, true, true, false, false, false, false, + true, false, true, false, true, false, true, true, false, true, false, true, false, true, + true, false, false, true, false, false, true, false, true, false, false, true, false, + false, false, true, true, false, true, true, false, true, true, true, true, true, false, + true, true, false, false, true, true, true, true, false, false, true, false, true, true, + true, false, false, false, false, false, true, false, false, true, true, true, false, + false, true, false, true, false, false, true, false, false, false, false, false, false, + false, false, true, true, false, false, true, false, false, true, true, false, false, true, + false, true, false, true, true, true, false, false, false, true, false, false, false, + false, true, true, false, true, true, false, true, false, true, true, false, true, false, + true, true, true, false, true, false, false, true, true, false, true, false, true, true, + false, false, true, true, true, true, false, true, true, false, false, true, false, true, + false, false, true, true, false, true, false, true, false, false, false, false, false, + false, false, true, false, true, true, false, false, true, false, true, false, false, + false, true, false, true, false, false, false, true, false, false, true, false, true, true, + false, false, false, false, true, false, false, false, false, false, false, false, false, + false, false, false, false, false, true, true, false, true, false, true, true, false, true, + false, true, false, false, false, true, true, true, true, false, false, false, false, + false, true, true, true, false, false, true, true, false, false, false, false, false, true, + false, true, true, true, true, false, true, true, true, false, false, true, false, true, + false, false, true, true, true, false, true, false, false, false, true, true, false, true, + false, true, false, true, true, true, true, true, false, false, true, false, true, false, + true, true, true, false, true, false, true, true, false, true, true, true, true, true, + false, false, false, false, false, true, false, true, false, false, true, true, false, + false, false, true, false, true, true, true, true, false, false, false, false, true, true, + false, false, true, true, false, true, true, true, true, false, true, true, true, false, + false, true, true, false, false, true, false, false, true, false, false, false, false, + false, false, false, false, false, false, true, true, false, false, true, false, false, + true, false, true, false, false, true, false, false, false, false, false, false, true, + false, false, false, false, false, false, false, false, false, true, true, true, false, + false, false, true, false, true, false, false, false, true, false, false, false, false, + false, false, false, true, false, false, false, false, false, false, false, false, true, + false, true, false, true, true, true, false, false, false, true, true, true, false, true, + false, true, true, false, false, false, true, false, false, false, false, true, false, + true, false, true, true, false, true, false, false, false, true, false, false, true, true, + false, true, false, false, false, false, false, false, true, true, false, false, true, + false, false, true, true, true, false, false, false, true, false, false, false, false, + true, false, true, false, false, false, true, false, true, true, false, true, false, true, + false, true, false, false, true, false, false, true, false, true, false, true, false, true, + false, false, true, true, true, true, false, true, false, false, false, false, false, true, + false, false, true, false, false, true, true, false, false, false, false, true, true, true, + false, false, true, false, false, true, true, true, true, false, false, false, true, false, + false, false, true, false, false, true, true, true, true, false, false, true, true, false, + true, false, true, false, false, true, true, false, true, true, true, true, false, false, + true, false, false, true, true, false, true, false, true, false, false, true, false, false, + false, false, true, true, true, false, true, false, false, true, false, false, true, false, + false, false, false, true, false, true, false, true, true, false, false, true, false, true, + true, true, false, false, false, false, true, true, false, true, false, false, false, true, + false, false, false, false, true, true, true, false, false, false, true, true, true, true, + false, true, true, false, true, true, true, false, true, false, false, true, false, true, + true, true, true, false, true, false, true, false, true, false, false, true, true, false, + false, true, false, true, false, false, false, false, true, false, true, false, false, + false, true, true, true, false, false, false, true, false, true, true, false, false, false, + false, false, true, false, true, false, false, true, true, false, true, true, true, true, + false, false, true, false, false, true, false, true, false, true, true, false, false, + false, true, false, true, true, false, false, false, true, false, true, false, true, true, + false, true, false, false, true, false, false, false, true, true, true, false, false, + false, false, false, true, false, false, true, true, true, true, true, false, false, false, + false, false, false, false, false, true, true, true, false, false, true, true, false, true, + true, false, true, false, true, false, false, false, true, false, false, true, false, + false, true, true, true, true, false, false, true, false, true, true, false, false, true, + false, false, true, true, false, true, false, false, true, true, true, false, false, false, + false, false, true, false, true, false, false, false, false, false, true, true, false, + true, true, true, false, false, false, false, true, true, true, true, false, true, true, + false, true, false, true, false, true, false, false, false, false, true, true, true, true, + false, false, true, false, true, true, false, false, false, false, false, false, true, + false, true, false, true, true, false, false, true, true, true, true, false, false, true, + false, true, true, false, false, true, true, true, false, true, false, false, true, true, + false, false, false, true, false, false, true, false, false, false, true, true, true, true, + false, true, false, true, false, true, false, true, false, false, true, false, false, true, + false, true, true, + ]; + + for i in 0..1000 { + let distinct_id = format!("distinct_id_{}", i); + + let feature_flag_match = FeatureFlagMatcher::new(distinct_id).get_match(&flags[0]); + + if results[i] { + assert_eq!( + feature_flag_match, + FeatureFlagMatch { + matches: true, + variant: None, + } + ); + } else { + assert_eq!( + feature_flag_match, + FeatureFlagMatch { + matches: false, + variant: None, + } + ); + } + } +} + +#[test] +fn it_is_consistent_with_rollout_calculation_for_multivariate_flags() { + let flags = create_flag_from_json(Some( + json!([{ + "id": 1, + "key": "multivariate-flag", + "name": "Multivariate flag", + "active": true, + "deleted": false, + "team_id": 1, + "filters": { + "groups": [ + { + "properties": [], + "rollout_percentage": 55, + }, + ], + "multivariate": { + "variants": [ + { + "key": "first-variant", + "name": "First Variant", + "rollout_percentage": 50, + }, + { + "key": "second-variant", + "name": "Second Variant", + "rollout_percentage": 20, + }, + { + "key": "third-variant", + "name": "Third Variant", + "rollout_percentage": 20, + }, + { + "key": "fourth-variant", + "name": "Fourth Variant", + "rollout_percentage": 5, + }, + { + "key": "fifth-variant", + "name": "Fifth Variant", + "rollout_percentage": 5, + }, + ], + }, + }, + }]) + .to_string(), + )); + + let results = vec![ + Some("second-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("fourth-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + None, + None, + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + Some("fifth-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("fourth-variant".to_string()), + None, + None, + None, + None, + Some("third-variant".to_string()), + None, + None, + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("second-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + None, + None, + Some("second-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("second-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("fifth-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + Some("third-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + None, + Some("third-variant".to_string()), + None, + None, + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + Some("fourth-variant".to_string()), + Some("fourth-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("fifth-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("second-variant".to_string()), + Some("fifth-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + None, + Some("third-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("fifth-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("fourth-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("fifth-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("fourth-variant".to_string()), + Some("fifth-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + None, + Some("third-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + None, + None, + Some("third-variant".to_string()), + Some("fourth-variant".to_string()), + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + None, + Some("fourth-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("fourth-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("fourth-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("fourth-variant".to_string()), + None, + None, + None, + Some("fourth-variant".to_string()), + None, + None, + Some("third-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + None, + None, + Some("fifth-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + None, + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("third-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + None, + None, + None, + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + None, + None, + None, + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + Some("second-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + None, + None, + None, + None, + None, + Some("fourth-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("fifth-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("fifth-variant".to_string()), + None, + None, + Some("third-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("fifth-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("fifth-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + None, + None, + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("second-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("second-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + None, + None, + None, + Some("second-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + None, + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + ]; + + for i in 0..1000 { + let distinct_id = format!("distinct_id_{}", i); + + let feature_flag_match = FeatureFlagMatcher::new(distinct_id).get_match(&flags[0]); + + if results[i].is_some() { + assert_eq!( + feature_flag_match, + FeatureFlagMatch { + matches: true, + variant: results[i].clone(), + } + ); + } else { + assert_eq!( + feature_flag_match, + FeatureFlagMatch { + matches: false, + variant: None, + } + ); + } + } +}