feat(feature-flags): support early_exit in local evaluation#648
Conversation
Port PostHog/posthog-js#3705 to posthog-python. When a flag enables `filters.early_exit`, condition evaluation now stops and returns `False` as soon as a condition group's property filters match but the rollout percentage excludes the user, instead of falling through to later groups — matching the server-side (Rust) engine's `OutOfRolloutBound` short-circuit. - Introduce a `ConditionMatch` tri-state (MATCH / NO_MATCH / OUT_OF_ROLLOUT_BOUND) returned by `is_condition_match`, so the loop can distinguish a rollout exclusion from a property mismatch. Property mismatches still fall through, mirroring the Rust semantics exactly. - Read `filters.early_exit` in `match_feature_flag_properties` and short-circuit to `False` on OUT_OF_ROLLOUT_BOUND when enabled. - Tests for early-exit on, default off (regression), explicit off, rollout-only groups, and the property-mismatch case. Generated-By: PostHog Code Task-Id: 707b13a5-0e5d-4764-915a-21e1f2a80c63
posthog-python Compliance ReportDate: 2026-06-04 18:29:13 UTC ✅ All Tests Passed!45/45 tests passed Capture Tests✅ 29/29 tests passed View Details
Feature_Flags Tests✅ 16/16 tests passed View Details
|
|
💡 Motivation and Context
PostHog/posthog#57321 added an early exit option to feature flags: when enabled, condition evaluation stops and returns
falseas soon as a user matches a group's targeting but is excluded by its rollout percentage, instead of falling through to later groups. That was implemented server-side (Rust engine), and PostHog/posthog-js#3705 ported it toposthog-node's local evaluation.posthog-pythonre-implements the same matching loop, so without honoringfilters.early_exitits locally-evaluated flags could diverge from server-side results. This PR applies the same approach toposthog-python.💚 How did you test it?
Added unit tests in
test_feature_flags.pycovering early-exit enabled (returnsFalsewithout evaluating later groups), unset/explicitly-disabled (falls through to a later matching group), a rollout-only group with no property filters, and a property-mismatch case (does not early exit). The full feature-flag suites pass (test_feature_flags.py,test_feature_flag_result.py,test_client.py— 285 tests). Lint (ruff) is clean.Changes
ConditionMatchtri-state (MATCH/NO_MATCH/OUT_OF_ROLLOUT_BOUND) returned byis_condition_match, so the evaluation loop can distinguish a rollout exclusion from a property mismatch.filters.early_exitinmatch_feature_flag_properties: when enabled and a group's property filters match but rollout excludes the user (OUT_OF_ROLLOUT_BOUND), returnFalseimmediately rather than evaluating later groups. Property mismatches still fall through, mirroring the Rust semantics exactly.get_feature_flagandevaluate_flags) route throughmatch_feature_flag_properties, so both honor the new behavior.📝 Checklist
If releasing new changes
sampo addto generate a changeset fileCreated with PostHog Code