Skip to content

Commit a91a208

Browse files
authored
fix(flags): flag dependency evaluation for multivariate flags (#316)
1 parent 10472e7 commit a91a208

File tree

4 files changed

+2716
-2062
lines changed

4 files changed

+2716
-2062
lines changed

example.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,98 @@ def load_env_file():
274274
print(" - Zero API calls needed: ✅ YES (all evaluated locally)")
275275
print(" - Python SDK supports flag dependencies: ✅ YES")
276276

277+
print("\n" + "-" * 60)
278+
print("PRODUCTION-STYLE MULTIVARIATE DEPENDENCY CHAIN")
279+
print("-" * 60)
280+
print("🔗 Testing complex multivariate flag dependencies...")
281+
print(
282+
" Structure: multivariate-root-flag -> multivariate-intermediate-flag -> multivariate-leaf-flag"
283+
)
284+
print("")
285+
print("📋 Required setup (if flags don't exist):")
286+
print(
287+
" 1. Create 'multivariate-leaf-flag' with fruit variants (pineapple, mango, papaya, kiwi)"
288+
)
289+
print(" - pineapple: email = '[email protected]'")
290+
print(" - mango: email = '[email protected]'")
291+
print(
292+
" 2. Create 'multivariate-intermediate-flag' with color variants (blue, red)"
293+
)
294+
print(" - blue: depends on multivariate-leaf-flag = 'pineapple'")
295+
print(" - red: depends on multivariate-leaf-flag = 'mango'")
296+
print(
297+
" 3. Create 'multivariate-root-flag' with show variants (breaking-bad, the-wire)"
298+
)
299+
print(" - breaking-bad: depends on multivariate-intermediate-flag = 'blue'")
300+
print(" - the-wire: depends on multivariate-intermediate-flag = 'red'")
301+
print("")
302+
303+
# Test pineapple -> blue -> breaking-bad chain
304+
dependent_result3 = posthog.get_feature_flag(
305+
"multivariate-root-flag",
306+
"regular_user",
307+
person_properties={"email": "[email protected]"},
308+
only_evaluate_locally=True,
309+
)
310+
if str(dependent_result3) != "breaking-bad":
311+
print(
312+
f" ❌ Something went wrong evaluating 'multivariate-root-flag' with [email protected]. Expected 'breaking-bad', got '{dependent_result3}'"
313+
)
314+
else:
315+
print("✅ 'multivariate-root-flag' with email [email protected] succeeded")
316+
317+
# Test mango -> red -> the-wire chain
318+
dependent_result4 = posthog.get_feature_flag(
319+
"multivariate-root-flag",
320+
"regular_user",
321+
person_properties={"email": "[email protected]"},
322+
only_evaluate_locally=True,
323+
)
324+
if str(dependent_result4) != "the-wire":
325+
print(
326+
f" ❌ Something went wrong evaluating multivariate-root-flag with [email protected]. Expected 'the-wire', got '{dependent_result4}'"
327+
)
328+
else:
329+
print("✅ 'multivariate-root-flag' with email [email protected] succeeded")
330+
331+
# Show the complete chain evaluation
332+
print("\n🔍 Complete dependency chain evaluation:")
333+
for email, expected_chain in [
334+
("[email protected]", ["pineapple", "blue", "breaking-bad"]),
335+
("[email protected]", ["mango", "red", "the-wire"]),
336+
]:
337+
leaf = posthog.get_feature_flag(
338+
"multivariate-leaf-flag",
339+
"regular_user",
340+
person_properties={"email": email},
341+
only_evaluate_locally=True,
342+
)
343+
intermediate = posthog.get_feature_flag(
344+
"multivariate-intermediate-flag",
345+
"regular_user",
346+
person_properties={"email": email},
347+
only_evaluate_locally=True,
348+
)
349+
root = posthog.get_feature_flag(
350+
"multivariate-root-flag",
351+
"regular_user",
352+
person_properties={"email": email},
353+
only_evaluate_locally=True,
354+
)
355+
356+
actual_chain = [str(leaf), str(intermediate), str(root)]
357+
chain_success = actual_chain == expected_chain
358+
359+
print(f" 📧 {email}:")
360+
print(f" Expected: {' -> '.join(map(str, expected_chain))}")
361+
print(f" Actual: {' -> '.join(map(str, actual_chain))}")
362+
print(f" Status: {'✅ SUCCESS' if chain_success else '❌ FAILED'}")
363+
364+
print("\n🎯 Multivariate Chain Summary:")
365+
print(" - Complex dependency chains: ✅ SUPPORTED")
366+
print(" - Multivariate flag dependencies: ✅ SUPPORTED")
367+
print(" - Local evaluation of chains: ✅ WORKING")
368+
277369
elif choice == "5":
278370
print("\n" + "=" * 60)
279371
print("CONTEXT MANAGEMENT AND TAGGING EXAMPLES")

posthog/feature_flags.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,70 @@ def evaluate_flag_dependency(
139139
# Definitive False result - dependency failed
140140
return False
141141

142+
# All dependencies in the chain have been evaluated successfully
143+
# Now check if the final flag value matches the expected value in the property
144+
flag_key = property.get("key")
145+
expected_value = property.get("value")
146+
operator = property.get("operator", "exact")
147+
148+
if flag_key and expected_value is not None:
149+
# Get the actual value of the flag we're checking
150+
actual_value = evaluation_cache.get(flag_key)
151+
152+
if actual_value is None:
153+
# Flag wasn't evaluated - this shouldn't happen if dependency chain is correct
154+
raise InconclusiveMatchError(
155+
f"Flag '{flag_key}' was not evaluated despite being in dependency chain"
156+
)
157+
158+
# For flag dependencies, we need to compare the actual flag result with expected value
159+
# using the flag_evaluates_to operator logic
160+
if operator == "flag_evaluates_to":
161+
return matches_dependency_value(expected_value, actual_value)
162+
else:
163+
# This should never happen, but just to be defensive.
164+
raise InconclusiveMatchError(
165+
f"Flag dependency property for '{property.get('key', 'unknown')}' has invalid operator '{operator}'"
166+
)
167+
168+
# If no value check needed, return True (all dependencies passed)
142169
return True
143170

144171

172+
def matches_dependency_value(expected_value, actual_value):
173+
"""
174+
Check if the actual flag value matches the expected dependency value.
175+
176+
This follows the same logic as the C# MatchesDependencyValue function:
177+
- String variant case: check for exact match or boolean true
178+
- Boolean case: must match expected boolean value
179+
180+
Args:
181+
expected_value: The expected value from the property
182+
actual_value: The actual value returned by the flag evaluation
183+
184+
Returns:
185+
bool: True if the values match according to flag dependency rules
186+
"""
187+
# String variant case - check for exact match or boolean true
188+
if isinstance(actual_value, str) and len(actual_value) > 0:
189+
if isinstance(expected_value, bool):
190+
# Any variant matches boolean true
191+
return expected_value
192+
elif isinstance(expected_value, str):
193+
# variants are case-sensitive, hence our comparison is too
194+
return actual_value == expected_value
195+
else:
196+
return False
197+
198+
# Boolean case - must match expected boolean value
199+
elif isinstance(actual_value, bool) and isinstance(expected_value, bool):
200+
return actual_value == expected_value
201+
202+
# Default case
203+
return False
204+
205+
145206
def match_feature_flag_properties(
146207
flag,
147208
distinct_id,

0 commit comments

Comments
 (0)