@@ -55,8 +55,100 @@ def variant_lookup_table(feature_flag):
5555 return lookup_table
5656
5757
58+ def evaluate_flag_dependency (
59+ property , flags_by_key , evaluation_cache , distinct_id , properties , cohort_properties
60+ ):
61+ """
62+ Evaluate a flag dependency property according to the dependency chain algorithm.
63+
64+ Args:
65+ property: Flag property with type="flag" and dependency_chain
66+ flags_by_key: Dictionary of all flags by their key
67+ evaluation_cache: Cache for storing evaluation results
68+ distinct_id: The distinct ID being evaluated
69+ properties: Person properties for evaluation
70+ cohort_properties: Cohort properties for evaluation
71+
72+ Returns:
73+ bool: True if all dependencies in the chain evaluate to True, False otherwise
74+ """
75+ if flags_by_key is None or evaluation_cache is None :
76+ # Cannot evaluate flag dependencies without required context
77+ raise InconclusiveMatchError (
78+ f"Cannot evaluate flag dependency on '{ property .get ('key' , 'unknown' )} ' without flags_by_key and evaluation_cache"
79+ )
80+
81+ # Check if dependency_chain is present - it should always be provided for flag dependencies
82+ if "dependency_chain" not in property :
83+ # Missing dependency_chain indicates malformed server data
84+ raise InconclusiveMatchError (
85+ f"Flag dependency property for '{ property .get ('key' , 'unknown' )} ' is missing required 'dependency_chain' field"
86+ )
87+
88+ dependency_chain = property .get ("dependency_chain" , [])
89+
90+ # Handle circular dependency (empty chain means circular)
91+ if len (dependency_chain ) == 0 :
92+ log .debug (f"Circular dependency detected for flag: { property .get ('key' )} " )
93+ raise InconclusiveMatchError (
94+ f"Circular dependency detected for flag '{ property .get ('key' , 'unknown' )} '"
95+ )
96+
97+ # Evaluate all dependencies in the chain order
98+ for dep_flag_key in dependency_chain :
99+ if dep_flag_key not in evaluation_cache :
100+ # Need to evaluate this dependency first
101+ dep_flag = flags_by_key .get (dep_flag_key )
102+ if not dep_flag :
103+ # Missing flag dependency - cannot evaluate locally
104+ evaluation_cache [dep_flag_key ] = None
105+ raise InconclusiveMatchError (
106+ f"Cannot evaluate flag dependency '{ dep_flag_key } ' - flag not found in local flags"
107+ )
108+ else :
109+ # Check if the flag is active (same check as in client._compute_flag_locally)
110+ if not dep_flag .get ("active" ):
111+ evaluation_cache [dep_flag_key ] = False
112+ else :
113+ # Recursively evaluate the dependency
114+ try :
115+ dep_result = match_feature_flag_properties (
116+ dep_flag ,
117+ distinct_id ,
118+ properties ,
119+ cohort_properties ,
120+ flags_by_key ,
121+ evaluation_cache ,
122+ )
123+ evaluation_cache [dep_flag_key ] = dep_result
124+ except InconclusiveMatchError as e :
125+ # If we can't evaluate a dependency, store None and propagate the error
126+ evaluation_cache [dep_flag_key ] = None
127+ raise InconclusiveMatchError (
128+ f"Cannot evaluate flag dependency '{ dep_flag_key } ': { e } "
129+ ) from e
130+
131+ # Check the cached result
132+ cached_result = evaluation_cache [dep_flag_key ]
133+ if cached_result is None :
134+ # Previously inconclusive - raise error again
135+ raise InconclusiveMatchError (
136+ f"Flag dependency '{ dep_flag_key } ' was previously inconclusive"
137+ )
138+ elif not cached_result :
139+ # Definitive False result - dependency failed
140+ return False
141+
142+ return True
143+
144+
58145def match_feature_flag_properties (
59- flag , distinct_id , properties , cohort_properties = None
146+ flag ,
147+ distinct_id ,
148+ properties ,
149+ cohort_properties = None ,
150+ flags_by_key = None ,
151+ evaluation_cache = None ,
60152) -> FlagValue :
61153 flag_conditions = (flag .get ("filters" ) or {}).get ("groups" ) or []
62154 is_inconclusive = False
@@ -79,7 +171,13 @@ def match_feature_flag_properties(
79171 # if any one condition resolves to True, we can shortcircuit and return
80172 # the matching variant
81173 if is_condition_match (
82- flag , distinct_id , condition , properties , cohort_properties
174+ flag ,
175+ distinct_id ,
176+ condition ,
177+ properties ,
178+ cohort_properties ,
179+ flags_by_key ,
180+ evaluation_cache ,
83181 ):
84182 variant_override = condition .get ("variant" )
85183 if variant_override and variant_override in valid_variant_keys :
@@ -101,22 +199,36 @@ def match_feature_flag_properties(
101199
102200
103201def is_condition_match (
104- feature_flag , distinct_id , condition , properties , cohort_properties
202+ feature_flag ,
203+ distinct_id ,
204+ condition ,
205+ properties ,
206+ cohort_properties ,
207+ flags_by_key = None ,
208+ evaluation_cache = None ,
105209) -> bool :
106210 rollout_percentage = condition .get ("rollout_percentage" )
107211 if len (condition .get ("properties" ) or []) > 0 :
108212 for prop in condition .get ("properties" ):
109213 property_type = prop .get ("type" )
110214 if property_type == "cohort" :
111- matches = match_cohort (prop , properties , cohort_properties )
215+ matches = match_cohort (
216+ prop ,
217+ properties ,
218+ cohort_properties ,
219+ flags_by_key ,
220+ evaluation_cache ,
221+ distinct_id ,
222+ )
112223 elif property_type == "flag" :
113- log .warning (
114- "Flag dependency filters are not supported in local evaluation. "
115- "Skipping condition for flag '%s' with dependency on flag '%s'" ,
116- feature_flag .get ("key" , "unknown" ),
117- prop .get ("key" , "unknown" ),
224+ matches = evaluate_flag_dependency (
225+ prop ,
226+ flags_by_key ,
227+ evaluation_cache ,
228+ distinct_id ,
229+ properties ,
230+ cohort_properties ,
118231 )
119- continue
120232 else :
121233 matches = match_property (prop , properties )
122234 if not matches :
@@ -264,7 +376,14 @@ def compare(lhs, rhs, operator):
264376 raise InconclusiveMatchError (f"Unknown operator { operator } " )
265377
266378
267- def match_cohort (property , property_values , cohort_properties ) -> bool :
379+ def match_cohort (
380+ property ,
381+ property_values ,
382+ cohort_properties ,
383+ flags_by_key = None ,
384+ evaluation_cache = None ,
385+ distinct_id = None ,
386+ ) -> bool :
268387 # Cohort properties are in the form of property groups like this:
269388 # {
270389 # "cohort_id": {
@@ -281,10 +400,24 @@ def match_cohort(property, property_values, cohort_properties) -> bool:
281400 )
282401
283402 property_group = cohort_properties [cohort_id ]
284- return match_property_group (property_group , property_values , cohort_properties )
403+ return match_property_group (
404+ property_group ,
405+ property_values ,
406+ cohort_properties ,
407+ flags_by_key ,
408+ evaluation_cache ,
409+ distinct_id ,
410+ )
285411
286412
287- def match_property_group (property_group , property_values , cohort_properties ) -> bool :
413+ def match_property_group (
414+ property_group ,
415+ property_values ,
416+ cohort_properties ,
417+ flags_by_key = None ,
418+ evaluation_cache = None ,
419+ distinct_id = None ,
420+ ) -> bool :
288421 if not property_group :
289422 return True
290423
@@ -301,7 +434,14 @@ def match_property_group(property_group, property_values, cohort_properties) ->
301434 # a nested property group
302435 for prop in properties :
303436 try :
304- matches = match_property_group (prop , property_values , cohort_properties )
437+ matches = match_property_group (
438+ prop ,
439+ property_values ,
440+ cohort_properties ,
441+ flags_by_key ,
442+ evaluation_cache ,
443+ distinct_id ,
444+ )
305445 if property_group_type == "AND" :
306446 if not matches :
307447 return False
@@ -324,14 +464,23 @@ def match_property_group(property_group, property_values, cohort_properties) ->
324464 for prop in properties :
325465 try :
326466 if prop .get ("type" ) == "cohort" :
327- matches = match_cohort (prop , property_values , cohort_properties )
467+ matches = match_cohort (
468+ prop ,
469+ property_values ,
470+ cohort_properties ,
471+ flags_by_key ,
472+ evaluation_cache ,
473+ distinct_id ,
474+ )
328475 elif prop .get ("type" ) == "flag" :
329- log .warning (
330- "Flag dependency filters are not supported in local evaluation. "
331- "Skipping condition with dependency on flag '%s'" ,
332- prop .get ("key" , "unknown" ),
476+ matches = evaluate_flag_dependency (
477+ prop ,
478+ flags_by_key ,
479+ evaluation_cache ,
480+ distinct_id ,
481+ property_values ,
482+ cohort_properties ,
333483 )
334- continue
335484 else :
336485 matches = match_property (prop , property_values )
337486
0 commit comments