@@ -44,8 +44,8 @@ def _hash(key: str, distinct_id: str, salt: str = "") -> float:
4444 return hash_val / __LONG_SCALE__
4545
4646
47- def get_matching_variant (flag , distinct_id ):
48- hash_value = _hash (flag ["key" ], distinct_id , salt = "variant" )
47+ def get_matching_variant (flag , hashing_identifier ):
48+ hash_value = _hash (flag ["key" ], hashing_identifier , salt = "variant" )
4949 for variant in variant_lookup_table (flag ):
5050 if hash_value >= variant ["value_min" ] and hash_value < variant ["value_max" ]:
5151 return variant ["key" ]
@@ -68,7 +68,13 @@ def variant_lookup_table(feature_flag):
6868
6969
7070def evaluate_flag_dependency (
71- property , flags_by_key , evaluation_cache , distinct_id , properties , cohort_properties
71+ property ,
72+ flags_by_key ,
73+ evaluation_cache ,
74+ distinct_id ,
75+ properties ,
76+ cohort_properties ,
77+ device_id = None ,
7278):
7379 """
7480 Evaluate a flag dependency property according to the dependency chain algorithm.
@@ -80,6 +86,7 @@ def evaluate_flag_dependency(
8086 distinct_id: The distinct ID being evaluated
8187 properties: Person properties for evaluation
8288 cohort_properties: Cohort properties for evaluation
89+ device_id: The device ID for bucketing (optional)
8390
8491 Returns:
8592 bool: True if all dependencies in the chain evaluate to True, False otherwise
@@ -131,6 +138,7 @@ def evaluate_flag_dependency(
131138 cohort_properties ,
132139 flags_by_key ,
133140 evaluation_cache ,
141+ device_id = device_id ,
134142 )
135143 evaluation_cache [dep_flag_key ] = dep_result
136144 except InconclusiveMatchError as e :
@@ -222,16 +230,32 @@ def match_feature_flag_properties(
222230 cohort_properties = None ,
223231 flags_by_key = None ,
224232 evaluation_cache = None ,
233+ device_id = None ,
234+ skip_bucketing_identifier = False ,
225235) -> FlagValue :
226- flag_conditions = (flag .get ("filters" ) or {}).get ("groups" ) or []
236+ flag_filters = flag .get ("filters" ) or {}
237+ flag_conditions = flag_filters .get ("groups" ) or []
227238 is_inconclusive = False
228239 cohort_properties = cohort_properties or {}
229240 # Some filters can be explicitly set to null, which require accessing variants like so
230- flag_variants = ((flag .get ("filters" ) or {}).get ("multivariate" ) or {}).get (
231- "variants"
232- ) or []
241+ flag_variants = (flag_filters .get ("multivariate" ) or {}).get ("variants" ) or []
233242 valid_variant_keys = [variant ["key" ] for variant in flag_variants ]
234243
244+ # Determine the hashing identifier based on bucketing_identifier setting
245+ # For group flags, skip_bucketing_identifier is True and we always use the passed identifier
246+ if skip_bucketing_identifier :
247+ hashing_identifier = distinct_id
248+ else :
249+ bucketing_identifier = flag_filters .get ("bucketing_identifier" )
250+ if bucketing_identifier == "device_id" :
251+ if not device_id :
252+ raise InconclusiveMatchError (
253+ "Flag requires device_id for bucketing but none was provided"
254+ )
255+ hashing_identifier = device_id
256+ else :
257+ hashing_identifier = distinct_id
258+
235259 for condition in flag_conditions :
236260 try :
237261 # if any one condition resolves to True, we can shortcircuit and return
@@ -244,12 +268,14 @@ def match_feature_flag_properties(
244268 cohort_properties ,
245269 flags_by_key ,
246270 evaluation_cache ,
271+ hashing_identifier = hashing_identifier ,
272+ device_id = device_id ,
247273 ):
248274 variant_override = condition .get ("variant" )
249275 if variant_override and variant_override in valid_variant_keys :
250276 variant = variant_override
251277 else :
252- variant = get_matching_variant (flag , distinct_id )
278+ variant = get_matching_variant (flag , hashing_identifier )
253279 return variant or True
254280 except RequiresServerEvaluation :
255281 # Static cohort or other missing server-side data - must fallback to API
@@ -277,7 +303,13 @@ def is_condition_match(
277303 cohort_properties ,
278304 flags_by_key = None ,
279305 evaluation_cache = None ,
306+ hashing_identifier = None ,
307+ device_id = None ,
280308) -> bool :
309+ # Use hashing_identifier if provided, otherwise fall back to distinct_id
310+ if hashing_identifier is None :
311+ hashing_identifier = distinct_id
312+
281313 rollout_percentage = condition .get ("rollout_percentage" )
282314 if len (condition .get ("properties" ) or []) > 0 :
283315 for prop in condition .get ("properties" ):
@@ -290,6 +322,7 @@ def is_condition_match(
290322 flags_by_key ,
291323 evaluation_cache ,
292324 distinct_id ,
325+ device_id = device_id ,
293326 )
294327 elif property_type == "flag" :
295328 matches = evaluate_flag_dependency (
@@ -299,6 +332,7 @@ def is_condition_match(
299332 distinct_id ,
300333 properties ,
301334 cohort_properties ,
335+ device_id = device_id ,
302336 )
303337 else :
304338 matches = match_property (prop , properties )
@@ -308,9 +342,9 @@ def is_condition_match(
308342 if rollout_percentage is None :
309343 return True
310344
311- if rollout_percentage is not None and _hash (feature_flag [ "key" ], distinct_id ) > (
312- rollout_percentage / 100
313- ):
345+ if rollout_percentage is not None and _hash (
346+ feature_flag [ "key" ], hashing_identifier
347+ ) > ( rollout_percentage / 100 ) :
314348 return False
315349
316350 return True
@@ -454,6 +488,7 @@ def match_cohort(
454488 flags_by_key = None ,
455489 evaluation_cache = None ,
456490 distinct_id = None ,
491+ device_id = None ,
457492) -> bool :
458493 # Cohort properties are in the form of property groups like this:
459494 # {
@@ -478,6 +513,7 @@ def match_cohort(
478513 flags_by_key ,
479514 evaluation_cache ,
480515 distinct_id ,
516+ device_id = device_id ,
481517 )
482518
483519
@@ -488,6 +524,7 @@ def match_property_group(
488524 flags_by_key = None ,
489525 evaluation_cache = None ,
490526 distinct_id = None ,
527+ device_id = None ,
491528) -> bool :
492529 if not property_group :
493530 return True
@@ -512,6 +549,7 @@ def match_property_group(
512549 flags_by_key ,
513550 evaluation_cache ,
514551 distinct_id ,
552+ device_id = device_id ,
515553 )
516554 if property_group_type == "AND" :
517555 if not matches :
@@ -545,6 +583,7 @@ def match_property_group(
545583 flags_by_key ,
546584 evaluation_cache ,
547585 distinct_id ,
586+ device_id = device_id ,
548587 )
549588 elif prop .get ("type" ) == "flag" :
550589 matches = evaluate_flag_dependency (
@@ -554,6 +593,7 @@ def match_property_group(
554593 distinct_id ,
555594 property_values ,
556595 cohort_properties ,
596+ device_id = device_id ,
557597 )
558598 else :
559599 matches = match_property (prop , property_values )
0 commit comments