Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .sampo/changesets/group-and-mixed-targeting-local-eval.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
cargo/posthog-rs: minor
---

feat(flags): support group-targeted and mixed-targeting feature flags in local evaluation

Adds local evaluation support for pure group flags (where `aggregation_group_type_index` is set at the flag level) and mixed-targeting flags (where individual conditions can override the aggregation per condition). `LocalEvaluator::evaluate_flag`, `evaluate_flag_simple`, and `evaluate_all_flags` now take `groups` and `group_properties` parameters; `match_feature_flag` and `match_feature_flag_with_context` have updated signatures. Backwards-incompatible at the public-API level — see PR description for migration notes.
41 changes: 35 additions & 6 deletions src/client/async_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,20 @@ impl Client {

// Try local evaluation first if available
if let Some(ref evaluator) = self.local_evaluator {
let empty = HashMap::new();
let props = person_properties.as_ref().unwrap_or(&empty);
match evaluator.evaluate_flag(&key_str, &distinct_id_str, props) {
let empty_props = HashMap::new();
let empty_groups: HashMap<String, String> = HashMap::new();
let empty_group_props: HashMap<String, HashMap<String, serde_json::Value>> =
HashMap::new();
let props = person_properties.as_ref().unwrap_or(&empty_props);
let groups_ref = groups.as_ref().unwrap_or(&empty_groups);
let group_props_ref = group_properties.as_ref().unwrap_or(&empty_group_props);
match evaluator.evaluate_flag(
&key_str,
&distinct_id_str,
props,
groups_ref,
group_props_ref,
) {
Ok(Some(value)) => {
debug!(flag = %key_str, ?value, "Flag evaluated locally");
return Ok(Some(value));
Expand Down Expand Up @@ -339,14 +350,32 @@ impl Client {
Ok(payloads.get(&key_str).cloned())
}

/// Evaluate a feature flag locally (requires feature flags to be loaded)
/// Evaluate a feature flag locally (requires feature flags to be loaded).
///
/// `groups` and `group_properties` are only consulted when the flag (or one
/// of its conditions) targets a group; pass empty maps for person flags.
#[allow(clippy::too_many_arguments)]
pub fn evaluate_feature_flag_locally(
&self,
flag: &FeatureFlag,
distinct_id: &str,
person_properties: &HashMap<String, serde_json::Value>,
groups: &HashMap<String, String>,
group_properties: &HashMap<String, HashMap<String, serde_json::Value>>,
) -> Result<FlagValue, Error> {
match_feature_flag(flag, distinct_id, person_properties)
.map_err(|e| Error::InconclusiveMatch(e.message))
let group_type_mapping = self
.local_evaluator
.as_ref()
.map(|ev| ev.cache().get_group_type_mapping())
.unwrap_or_default();
match_feature_flag(
flag,
distinct_id,
person_properties,
groups,
group_properties,
&group_type_mapping,
)
.map_err(|e| Error::InconclusiveMatch(e.message))
}
}
41 changes: 35 additions & 6 deletions src/client/blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,20 @@ impl Client {

// Try local evaluation first if available
if let Some(ref evaluator) = self.local_evaluator {
let empty = HashMap::new();
let props = person_properties.as_ref().unwrap_or(&empty);
match evaluator.evaluate_flag(&key_str, &distinct_id_str, props) {
let empty_props = HashMap::new();
let empty_groups: HashMap<String, String> = HashMap::new();
let empty_group_props: HashMap<String, HashMap<String, serde_json::Value>> =
HashMap::new();
let props = person_properties.as_ref().unwrap_or(&empty_props);
let groups_ref = groups.as_ref().unwrap_or(&empty_groups);
let group_props_ref = group_properties.as_ref().unwrap_or(&empty_group_props);
match evaluator.evaluate_flag(
&key_str,
&distinct_id_str,
props,
groups_ref,
group_props_ref,
) {
Ok(Some(value)) => {
debug!(flag = %key_str, ?value, "Flag evaluated locally");
return Ok(Some(value));
Expand Down Expand Up @@ -330,14 +341,32 @@ impl Client {
Ok(payloads.get(&key_str).cloned())
}

/// Evaluate a feature flag locally (requires feature flags to be loaded)
/// Evaluate a feature flag locally (requires feature flags to be loaded).
///
/// `groups` and `group_properties` are only consulted when the flag (or one
/// of its conditions) targets a group; pass empty maps for person flags.
#[allow(clippy::too_many_arguments)]
pub fn evaluate_feature_flag_locally(
&self,
flag: &FeatureFlag,
distinct_id: &str,
person_properties: &HashMap<String, serde_json::Value>,
groups: &HashMap<String, String>,
group_properties: &HashMap<String, HashMap<String, serde_json::Value>>,
) -> Result<FlagValue, Error> {
match_feature_flag(flag, distinct_id, person_properties)
.map_err(|e| Error::InconclusiveMatch(e.message))
let group_type_mapping = self
.local_evaluator
.as_ref()
.map(|ev| ev.cache().get_group_type_mapping())
.unwrap_or_default();
match_feature_flag(
flag,
distinct_id,
person_properties,
groups,
group_properties,
&group_type_mapping,
)
.map_err(|e| Error::InconclusiveMatch(e.message))
}
}
Loading
Loading