0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-21 13:39:22 +01:00

feat(flags): rust implementation of a bunch of things (#24957)

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Dylan Martin 2024-09-24 19:06:22 -04:00 committed by GitHub
parent d4c86a981b
commit c940fd5229
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1283 additions and 336 deletions

4
.gitignore vendored
View File

@ -66,4 +66,6 @@ plugin-transpiler/dist
.dlt
*.db
# Ignore any log files that happen to be present
*.log
*.log
# pyright config (keep this until we have a standardized one)
pyrightconfig.json

33
rust/Cargo.lock generated
View File

@ -471,7 +471,7 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa"
dependencies = [
"heck",
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.48",
@ -1184,6 +1184,7 @@ dependencies = [
"serde_json",
"sha1",
"sqlx",
"strum",
"thiserror",
"tokio",
"tower",
@ -1553,6 +1554,12 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.9"
@ -3718,7 +3725,7 @@ dependencies = [
"atomic-write-file",
"dotenvy",
"either",
"heck",
"heck 0.4.1",
"hex",
"once_cell",
"proc-macro2",
@ -3870,6 +3877,28 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.48",
]
[[package]]
name = "subtle"
version = "2.5.0"

View File

@ -34,6 +34,7 @@ uuid = { workspace = true }
base64.workspace = true
flate2.workspace = true
common-alloc = { path = "../common/alloc" }
strum = { version = "0.26", features = ["derive"] }
health = { path = "../common/health" }
common-metrics = { path = "../common/metrics" }
tower = { workspace = true }

View File

@ -0,0 +1,101 @@
use std::cmp::Ordering;
use strum::EnumString;
#[derive(Debug, Clone, PartialEq, Eq, EnumString)]
pub enum FeatureFlagMatchReason {
#[strum(serialize = "super_condition_value")]
SuperConditionValue,
#[strum(serialize = "condition_match")]
ConditionMatch,
#[strum(serialize = "no_condition_match")]
NoConditionMatch,
#[strum(serialize = "out_of_rollout_bound")]
OutOfRolloutBound,
#[strum(serialize = "no_group_type")]
NoGroupType,
}
impl FeatureFlagMatchReason {
pub fn score(&self) -> i32 {
match self {
FeatureFlagMatchReason::SuperConditionValue => 4,
FeatureFlagMatchReason::ConditionMatch => 3,
FeatureFlagMatchReason::NoGroupType => 2,
FeatureFlagMatchReason::OutOfRolloutBound => 1,
FeatureFlagMatchReason::NoConditionMatch => 0,
}
}
}
impl PartialOrd for FeatureFlagMatchReason {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for FeatureFlagMatchReason {
fn cmp(&self, other: &Self) -> Ordering {
self.score().cmp(&other.score())
}
}
impl std::fmt::Display for FeatureFlagMatchReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
FeatureFlagMatchReason::SuperConditionValue => "super_condition_value",
FeatureFlagMatchReason::ConditionMatch => "condition_match",
FeatureFlagMatchReason::NoConditionMatch => "no_condition_match",
FeatureFlagMatchReason::OutOfRolloutBound => "out_of_rollout_bound",
FeatureFlagMatchReason::NoGroupType => "no_group_type",
}
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ordering() {
let reasons = vec![
FeatureFlagMatchReason::NoConditionMatch,
FeatureFlagMatchReason::OutOfRolloutBound,
FeatureFlagMatchReason::NoGroupType,
FeatureFlagMatchReason::ConditionMatch,
FeatureFlagMatchReason::SuperConditionValue,
];
let mut sorted_reasons = reasons.clone();
sorted_reasons.sort();
assert_eq!(sorted_reasons, reasons);
}
#[test]
fn test_display() {
assert_eq!(
FeatureFlagMatchReason::SuperConditionValue.to_string(),
"super_condition_value"
);
assert_eq!(
FeatureFlagMatchReason::ConditionMatch.to_string(),
"condition_match"
);
assert_eq!(
FeatureFlagMatchReason::NoConditionMatch.to_string(),
"no_condition_match"
);
assert_eq!(
FeatureFlagMatchReason::OutOfRolloutBound.to_string(),
"out_of_rollout_bound"
);
assert_eq!(
FeatureFlagMatchReason::NoGroupType.to_string(),
"no_group_type"
);
}
}

View File

@ -110,6 +110,14 @@ impl FeatureFlag {
.clone()
.map_or(vec![], |m| m.variants)
}
pub fn get_payload(&self, match_val: &str) -> Option<serde_json::Value> {
self.filters.payloads.as_ref().and_then(|payloads| {
payloads
.as_object()
.and_then(|obj| obj.get(match_val).cloned())
})
}
}
#[derive(Debug, Deserialize, Serialize)]

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
pub mod api;
pub mod config;
pub mod database;
pub mod feature_flag_match_reason;
pub mod flag_definitions;
pub mod flag_matching;
pub mod flag_request;

View File

@ -53,9 +53,12 @@ pub fn match_property(
let compute_exact_match = |value: &Value, override_value: &Value| -> bool {
if is_truthy_or_falsy_property_value(value) {
// Do boolean handling, such that passing in "true" or "True" or "false" or "False" as matching value is equivalent
let truthy = is_truthy_property_value(value);
return override_value.to_string().to_lowercase()
== truthy.to_string().to_lowercase();
let (truthy_value, truthy_override_value) = (
is_truthy_property_value(value),
is_truthy_property_value(override_value),
);
return truthy_override_value.to_string().to_lowercase()
== truthy_value.to_string().to_lowercase();
}
if value.is_array() {

View File

@ -1,3 +1,4 @@
use feature_flags::feature_flag_match_reason::FeatureFlagMatchReason;
/// 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};
@ -121,6 +122,9 @@ async fn it_is_consistent_with_rollout_calculation_for_simple_flags() {
FeatureFlagMatch {
matches: true,
variant: None,
reason: FeatureFlagMatchReason::ConditionMatch,
condition_index: Some(0),
payload: None,
}
);
} else {
@ -129,6 +133,9 @@ async fn it_is_consistent_with_rollout_calculation_for_simple_flags() {
FeatureFlagMatch {
matches: false,
variant: None,
reason: FeatureFlagMatchReason::OutOfRolloutBound,
condition_index: Some(0),
payload: None,
}
);
}
@ -1199,12 +1206,15 @@ async fn it_is_consistent_with_rollout_calculation_for_multivariate_flags() {
.await
.unwrap();
if result.is_some() {
if let Some(variant) = &result {
assert_eq!(
feature_flag_match,
FeatureFlagMatch {
matches: true,
variant: results[i].clone(),
variant: Some(variant.clone()),
reason: FeatureFlagMatchReason::ConditionMatch,
condition_index: Some(0),
payload: None,
}
);
} else {
@ -1213,6 +1223,9 @@ async fn it_is_consistent_with_rollout_calculation_for_multivariate_flags() {
FeatureFlagMatch {
matches: false,
variant: None,
reason: FeatureFlagMatchReason::OutOfRolloutBound,
condition_index: Some(0),
payload: None,
}
);
}