Skip to content

feat(feature-flags): support early_exit in local evaluation#648

Merged
gustavohstrassburger merged 2 commits into
mainfrom
posthog-code/feature-flag-early-exit
Jun 5, 2026
Merged

feat(feature-flags): support early_exit in local evaluation#648
gustavohstrassburger merged 2 commits into
mainfrom
posthog-code/feature-flag-early-exit

Conversation

@gustavohstrassburger
Copy link
Copy Markdown
Contributor

💡 Motivation and Context

PostHog/posthog#57321 added an early exit option to feature flags: when enabled, condition evaluation stops and returns false as 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 to posthog-node's local evaluation.

posthog-python re-implements the same matching loop, so without honoring filters.early_exit its locally-evaluated flags could diverge from server-side results. This PR applies the same approach to posthog-python.

💚 How did you test it?

Added unit tests in test_feature_flags.py covering early-exit enabled (returns False without 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

  • Introduce a ConditionMatch tri-state (MATCH / NO_MATCH / OUT_OF_ROLLOUT_BOUND) returned by is_condition_match, so the evaluation loop can distinguish a rollout exclusion from a property mismatch.
  • Honor filters.early_exit in match_feature_flag_properties: when enabled and a group's property filters match but rollout excludes the user (OUT_OF_ROLLOUT_BOUND), return False immediately rather than evaluating later groups. Property mismatches still fall through, mirroring the Rust semantics exactly.
  • Both local-evaluation entry points (get_feature_flag and evaluate_flags) route through match_feature_flag_properties, so both honor the new behavior.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran sampo add to generate a changeset file

Created with PostHog Code

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
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

posthog-python Compliance Report

Date: 2026-06-04 18:29:13 UTC
Duration: 176159ms

✅ All Tests Passed!

45/45 tests passed


Capture Tests

29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields 518ms
Format Validation.Event Has Uuid 1507ms
Format Validation.Event Has Lib Properties 1508ms
Format Validation.Distinct Id Is String 1507ms
Format Validation.Token Is Present 1507ms
Format Validation.Custom Properties Preserved 1508ms
Format Validation.Event Has Timestamp 1507ms
Retry Behavior.Retries On 503 9517ms
Retry Behavior.Does Not Retry On 400 3508ms
Retry Behavior.Does Not Retry On 401 3509ms
Retry Behavior.Respects Retry After Header 9514ms
Retry Behavior.Implements Backoff 23519ms
Retry Behavior.Retries On 500 7512ms
Retry Behavior.Retries On 502 7517ms
Retry Behavior.Retries On 504 7512ms
Retry Behavior.Max Retries Respected 23531ms
Deduplication.Generates Unique Uuids 1498ms
Deduplication.Preserves Uuid On Retry 7511ms
Deduplication.Preserves Uuid And Timestamp On Retry 14526ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 7504ms
Deduplication.No Duplicate Events In Batch 1508ms
Deduplication.Different Events Have Different Uuids 1507ms
Compression.Sends Gzip When Enabled 1508ms
Batch Format.Uses Proper Batch Structure 1507ms
Batch Format.Flush With No Events Sends Nothing 1005ms
Batch Format.Multiple Events Batched Together 1505ms
Error Handling.Does Not Retry On 403 3510ms
Error Handling.Does Not Retry On 413 3506ms
Error Handling.Retries On 408 7515ms

Feature_Flags Tests

16/16 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id 1003ms
Request Payload.Flags Request Uses V2 Query Param 1007ms
Request Payload.Flags Request Hits Flags Path Not Decide 1007ms
Request Payload.Flags Request Omits Authorization Header 1007ms
Request Payload.Token In Flags Body Matches Init 1007ms
Request Payload.Groups Round Trip 1006ms
Request Payload.Groups Default To Empty Object 1007ms
Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It 1007ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False 1007ms
Request Payload.Disable Geoip Omitted Defaults To False 1007ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key 1006ms
Request Lifecycle.No Flags Request On Init Alone 503ms
Request Lifecycle.No Flags Request On Normal Capture 1507ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests 1012ms
Request Lifecycle.Mock Response Value Is Returned To Caller 1002ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event 1509ms

@gustavohstrassburger gustavohstrassburger marked this pull request as ready for review June 4, 2026 18:44
@gustavohstrassburger gustavohstrassburger requested a review from a team as a code owner June 4, 2026 18:44
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 4, 2026

Comments Outside Diff (1)

  1. posthog/test/test_feature_flags.py, line 256-325 (link)

    P2 Three non-parameterized tests covering related early-exit scenarios

    test_early_exit_on_rollout_only_group_with_no_property_filters, test_early_exit_does_not_trigger_on_property_mismatch, and test_early_exit_on_multivariate_flag each test a distinct scenario (no property filters, property mismatch, multivariate variant) but all follow the same two-group flag structure with early_exit: True. Per the repo's preference, these should be expressed as parameterized cases — either added to the existing @parameterized.expand block on test_early_exit (where the full flag fixture is passed as a parameter) or as a second parameterized test covering the alternative flag shapes.

    Context Used: Do not attempt to comment on incorrect alphabetica... (source)

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: posthog/test/test_feature_flags.py
    Line: 256-325
    
    Comment:
    **Three non-parameterized tests covering related early-exit scenarios**
    
    `test_early_exit_on_rollout_only_group_with_no_property_filters`, `test_early_exit_does_not_trigger_on_property_mismatch`, and `test_early_exit_on_multivariate_flag` each test a distinct scenario (no property filters, property mismatch, multivariate variant) but all follow the same two-group flag structure with `early_exit: True`. Per the repo's preference, these should be expressed as parameterized cases — either added to the existing `@parameterized.expand` block on `test_early_exit` (where the full flag fixture is passed as a parameter) or as a second parameterized test covering the alternative flag shapes.
    
    **Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
posthog/test/test_feature_flags.py:256-325
**Three non-parameterized tests covering related early-exit scenarios**

`test_early_exit_on_rollout_only_group_with_no_property_filters`, `test_early_exit_does_not_trigger_on_property_mismatch`, and `test_early_exit_on_multivariate_flag` each test a distinct scenario (no property filters, property mismatch, multivariate variant) but all follow the same two-group flag structure with `early_exit: True`. Per the repo's preference, these should be expressed as parameterized cases — either added to the existing `@parameterized.expand` block on `test_early_exit` (where the full flag fixture is passed as a parameter) or as a second parameterized test covering the alternative flag shapes.

Reviews (1): Last reviewed commit: "fix(feature-flags): remove redundant or ..." | Re-trigger Greptile

@gustavohstrassburger gustavohstrassburger merged commit a2ce51e into main Jun 5, 2026
30 checks passed
@gustavohstrassburger gustavohstrassburger deleted the posthog-code/feature-flag-early-exit branch June 5, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants