-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(flags): support groups and payloads correctly in new /flags
service
#26817
Changes from all commits
029061f
8ed5d01
f111c7d
172a674
574d4a6
386ff7b
9068c0b
ef78f6b
ffe10d0
6f47ee7
0156612
f255203
21a9e25
94d15e8
d791a03
c209683
048c4db
897acd2
bec7fa9
53994b2
1da6644
61033cb
464cca8
6ef7d9d
f2c6f5e
e835697
ad5bad6
0bb7a5c
66ab412
15df5b2
90bfab2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,7 +46,6 @@ pub async fn flags( | |
let context = RequestContext { | ||
state, | ||
ip, | ||
meta: meta.0, | ||
headers, | ||
body, | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,7 +58,6 @@ pub struct FlagsQueryParams { | |
pub struct RequestContext { | ||
pub state: State<router::State>, | ||
pub ip: IpAddr, | ||
pub meta: FlagsQueryParams, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see above comment. |
||
pub headers: HeaderMap, | ||
pub body: Bytes, | ||
} | ||
|
@@ -82,11 +81,24 @@ pub struct FeatureFlagEvaluationContext { | |
hash_key_override: Option<String>, | ||
} | ||
|
||
/// Process a feature flag request and return the evaluated flags | ||
/// | ||
/// ## Flow | ||
/// 1. Decodes and validates the request | ||
/// 2. Extracts and verifies the authentication token | ||
/// 3. Retrieves team information | ||
/// 4. Processes person and group properties | ||
/// 5. Retrieves feature flags | ||
/// 6. Evaluates flags based on the context | ||
/// | ||
/// ## Error Handling | ||
/// - Returns early if any step fails | ||
/// - Maintains error context through the FlagError enum | ||
/// - Individual flag evaluation failures don't fail the entire request | ||
pub async fn process_request(context: RequestContext) -> Result<FlagsResponse, FlagError> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this needed a comment since it's an entrypoint method |
||
let RequestContext { | ||
state, | ||
ip, | ||
meta: _, // TODO use this | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lol I don't actually use this anywhere outside of recording request metadata, so bye bye |
||
headers, | ||
body, | ||
} = context; | ||
|
@@ -95,20 +107,24 @@ pub async fn process_request(context: RequestContext) -> Result<FlagsResponse, F | |
let token = request | ||
.extract_and_verify_token(state.redis.clone(), state.reader.clone()) | ||
.await?; | ||
|
||
let team = request | ||
.get_team_from_cache_or_pg(&token, state.redis.clone(), state.reader.clone()) | ||
.await?; | ||
|
||
let distinct_id = request.extract_distinct_id()?; | ||
let groups = request.groups.clone(); | ||
let team_id = team.id; | ||
let person_property_overrides = get_person_property_overrides( | ||
!request.geoip_disable.unwrap_or(false), | ||
request.person_properties.clone(), | ||
&ip, | ||
&state.geoip, | ||
); | ||
let group_property_overrides = request.group_properties.clone(); | ||
|
||
let groups = request.groups.clone(); | ||
let group_property_overrides = | ||
process_group_property_overrides(groups.clone(), request.group_properties.clone()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note, this means every time we have a flags request with groups, there will be a
|
||
|
||
let hash_key_override = request.anon_distinct_id.clone(); | ||
|
||
let feature_flags_from_cache_or_pg = request | ||
|
@@ -170,6 +186,39 @@ pub fn get_person_property_overrides( | |
} | ||
} | ||
|
||
/// Processes group property overrides by combining existing overrides with group key overrides | ||
/// | ||
/// When groups are provided in the format {"group_type": "group_key"}, we need to ensure these | ||
/// are included in the group property overrides with the special "$group_key" property. | ||
fn process_group_property_overrides( | ||
groups: Option<HashMap<String, Value>>, | ||
existing_overrides: Option<HashMap<String, HashMap<String, Value>>>, | ||
) -> Option<HashMap<String, HashMap<String, Value>>> { | ||
match groups { | ||
Some(groups) => { | ||
let group_key_overrides: HashMap<String, HashMap<String, Value>> = groups | ||
.into_iter() | ||
.map(|(group_type, group_key)| { | ||
let mut properties = existing_overrides | ||
.as_ref() | ||
.and_then(|g| g.get(&group_type)) | ||
.cloned() | ||
.unwrap_or_default(); | ||
|
||
properties.insert("$group_key".to_string(), group_key); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm replicating the work done here: https://github.com/PostHog/posthog/blob/master/posthog/models/feature_flag/flag_matching.py#L1108-L1119 |
||
|
||
(group_type, properties) | ||
}) | ||
.collect(); | ||
|
||
let mut result = existing_overrides.unwrap_or_default(); | ||
result.extend(group_key_overrides); | ||
Some(result) | ||
} | ||
None => existing_overrides, | ||
} | ||
} | ||
|
||
/// Decode a request into a `FlagRequest` | ||
/// - Currently only supports JSON requests | ||
// TODO support all supported content types | ||
|
@@ -738,4 +787,61 @@ mod tests { | |
assert!(!result.error_while_computing_flags); | ||
assert_eq!(result.feature_flags["test_flag"], FlagValue::Boolean(true)); | ||
} | ||
|
||
#[test] | ||
fn test_process_group_property_overrides() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wanted to make sure I'm correctly overriding the group values with any passed in property overrides. |
||
// Test case 1: Both groups and existing overrides | ||
let groups = HashMap::from([ | ||
("project".to_string(), json!("project_123")), | ||
("organization".to_string(), json!("org_456")), | ||
]); | ||
|
||
let mut existing_overrides = HashMap::new(); | ||
let mut project_props = HashMap::new(); | ||
project_props.insert("industry".to_string(), json!("tech")); | ||
existing_overrides.insert("project".to_string(), project_props); | ||
|
||
let result = | ||
process_group_property_overrides(Some(groups.clone()), Some(existing_overrides)); | ||
|
||
assert!(result.is_some()); | ||
let result = result.unwrap(); | ||
|
||
// Check project properties | ||
let project_props = result.get("project").expect("Project properties missing"); | ||
assert_eq!(project_props.get("industry"), Some(&json!("tech"))); | ||
assert_eq!(project_props.get("$group_key"), Some(&json!("project_123"))); | ||
|
||
// Check organization properties | ||
let org_props = result | ||
.get("organization") | ||
.expect("Organization properties missing"); | ||
assert_eq!(org_props.get("$group_key"), Some(&json!("org_456"))); | ||
|
||
// Test case 2: Only groups, no existing overrides | ||
let result = process_group_property_overrides(Some(groups.clone()), None); | ||
|
||
assert!(result.is_some()); | ||
let result = result.unwrap(); | ||
assert_eq!(result.len(), 2); | ||
assert_eq!( | ||
result.get("project").unwrap().get("$group_key"), | ||
Some(&json!("project_123")) | ||
); | ||
|
||
// Test case 3: No groups, only existing overrides | ||
let mut existing_overrides = HashMap::new(); | ||
let mut project_props = HashMap::new(); | ||
project_props.insert("industry".to_string(), json!("tech")); | ||
existing_overrides.insert("project".to_string(), project_props); | ||
|
||
let result = process_group_property_overrides(None, Some(existing_overrides.clone())); | ||
|
||
assert!(result.is_some()); | ||
assert_eq!(result.unwrap(), existing_overrides); | ||
|
||
// Test case 4: Neither groups nor existing overrides | ||
let result = process_group_property_overrides(None, None); | ||
assert!(result.is_none()); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I never use this value in the request logic, so I can omit it.