💡 Motivation and Context
Local feature flag evaluation needs to honor the property_matching_version supplied with flag definitions. Otherwise Python can return a different result from the service when a project uses version 2 matching.
This follows the backend behavior and shared SDK contract.
- Exactly version 2 uses explicit scalar equality and member equality for nonempty filter arrays. Missing/1 and unsupported selectors keep legacy matching. Empty filters retain recursive truthiness. For known properties,
is_not complements exact.
- The selector travels with person, group, nested cohort, and flag dependency evaluations, including multi-flag evaluations.
- Definition snapshots and synchronous/asynchronous definition cache providers retain the selector. Version-only refreshes invalidate evaluated results even when flags are unchanged.
- The first result after lazy definition loading is cached for fallback during an API outage. Definition publication advances an in-memory generation without waiting for Redis I/O. Separate write locks prevent an old in-flight write from overwriting a newer result within a Client. Locks are replaced after fork, and memory writes prune invalidated flags for reused users.
- Client-managed Redis entries now include a private deterministic fingerprint of flags, cohorts, group mapping, and matching version. Normal and stale reads reject entries from other snapshots, including late writes and entries written by another worker. Matching snapshots can reuse results across workers even when their local generation counters differ. Fingerprint serialization happens outside the publication lock for API and provider loads.
- Every snapshot change advances the local generation, including initial hydration and cohort/group-mapping-only changes. Writes retain the identity of the evaluation that produced them rather than adopting the definitions current at write completion.
Compatibility
The optional definition-cache field and matcher arguments are additive and included in the public API snapshot. Client supports async definition providers here. The separate AsyncClient remains remote-only. The existing Sampo changeset is a patch for pypi/posthog.
Redis keys, TTLs, public cache signatures, and standalone invalidation/clear behavior are unchanged. The new entry metadata is additive and readable by older SDKs. Older or unverifiable entries become cache misses for upgraded Client-managed caches. Older SDK readers do not gain these protections.
Before definitions load, remote results can still serve outage fallback within the same Client. Their private identity is unique to that Client and renewed after fork, so new Clients and workers cannot reuse unverifiable remote-only entries. Authentication or billing resets disable Redis result caching until valid definitions load again. Remote requests use their request-start generation only as a local invalidation boundary, not as proof of the server's definition snapshot.
Redis result writes remain synchronous. This change does not add Redis timeouts or make single-result cache writes nonblocking. Old entries can remain until their existing TTL expires, but upgraded readers only accept matching provenance.
Optional harness coverage is separate. This PR does not opt an SDK adapter into that coverage.
💚 How did you test it?
Using the existing cached virtualenv with PYTHONDONTWRITEBYTECODE=1 and PYTHONPATH=.:
- 1,036 focused pytest cases passed across versioned matching, publication concurrency, definition providers, single/full/bulk APIs, async feature flags, fork handling, Client behavior, and memory/Redis cache serialization. These runs emitted 127 deprecation warnings.
- The independent Redis restart reproducer failed both ordinary-definition and version-only cases on
ef127fd. Permanent regression tests failed 24 cases on that head and pass with the fix. They cover restart, different worker snapshots, synchronous/asynchronous providers, cohort/group-mapping-only changes, paused writes, old entries without metadata, and delayed remote responses across hydration or reset.
- Committed-branch autoreview found a further remote-only restart case on
806f735. Three regression cases failed before replacing shared remote-only provenance with per-Client identities. Same-Client fallback, matching-definition reuse, and fork controls pass.
- Changed-file Ruff formatting and lint checks, public API snapshot verification, warning-as-error import, and whitespace checks passed. Mypy with baseline filtering passed for 230 source files.
- Both new commits are signed. Commit
3dbe088 passed isolated committed-branch autoreview against origin/main with no actionable findings.
Targeted mutation CI repair
The targeted job stopped before mutation testing because its CRAP gate selects only test_utils.py and test_size_limited_dict.py. Cache publication tests in other files did not cover the new snapshot paths in that selection. The CRAP calculator also counts non-executable lines, and the Redis read method's complexity of 10 could not satisfy the strict below-10 limit.
- Added 14 selected regression cases for generation fences, reused-user pruning, snapshot serialization, cross-worker reuse, wrong or missing fingerprints, disabled snapshots, TTL boundaries, and refresh during serialization. Added assertions that standalone entries have no snapshot metadata.
- Removed repeated conditions in three cache methods. Memory cache initialization and eviction share one new-user check. Redis reads choose snapshot or standalone validation with an
if/else. Redis writes select the fingerprint once. Publication fences, write locks, Redis I/O ordering, and public signatures are unchanged.
- On Python 3.11.11, all 100 selected tests passed with 100% executable line coverage of
utils.py. The maximum CRAP score is 8.66, below the unchanged limit of 10. A fresh actual mutation run killed all 535 mutants, with empty mutmut results and no survivors, timeouts, or untested mutants.
- All 519 focused cache, snapshot, fork, definition-provider, and versioned-matching tests passed, with 127 deprecation warnings. Repository-wide Ruff format/lint, baseline-filtered Mypy for 230 source files, the public API snapshot check, warning-as-error import, and whitespace checks passed.
- Signed commit
a5a6dfb passed isolated committed-branch autoreview against origin/main with no actionable findings. All GitHub checks for a5a6dfb8e96c4b5cb8ad4e857ab2a18da3e6209e completed successfully, with no failed, pending, or skipped checks. The CI run includes Python 3.10 through 3.14 tests. Its targeted mutation job executed mutation testing rather than skipping on a cache hit and killed all 535 mutants. Both capture protocols also passed SDK compliance.
Full repository tests and generated mirror packaging were not run locally for this repair. Redis regressions use the repository's deterministic FakeRedis, not a live Redis service.
📝 Checklist
If releasing new changes
The existing .sampo/changesets/versioned-property-matching.md patch changeset was updated. sampo add was not rerun and no duplicate changeset was added.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Pi agents implemented and tested this user-directed SDK contract update using repository inspection, Python/pytest, Ruff, Git, GitHub CLI, and the isolated autoreview helper. Human review is required before merging. No public agent session link is available.
Fresh review found that a process-local generation could allow invalidated Redis entries to revive after restart. The repair adds private snapshot provenance without putting Redis I/O under the publication lock. The user-directed review workflow approved the additive metadata and stricter handling of unverifiable remote-only entries. Legacy matching defaults, public cache APIs, and Redis key formats are preserved.
Uh oh!
There was an error while loading. Please reload this page.
💡 Motivation and Context
Local feature flag evaluation needs to honor the
property_matching_versionsupplied with flag definitions. Otherwise Python can return a different result from the service when a project uses version 2 matching.This follows the backend behavior and shared SDK contract.
is_notcomplementsexact.Compatibility
The optional definition-cache field and matcher arguments are additive and included in the public API snapshot.
Clientsupports async definition providers here. The separateAsyncClientremains remote-only. The existing Sampo changeset is a patch forpypi/posthog.Redis keys, TTLs, public cache signatures, and standalone invalidation/clear behavior are unchanged. The new entry metadata is additive and readable by older SDKs. Older or unverifiable entries become cache misses for upgraded Client-managed caches. Older SDK readers do not gain these protections.
Before definitions load, remote results can still serve outage fallback within the same Client. Their private identity is unique to that Client and renewed after fork, so new Clients and workers cannot reuse unverifiable remote-only entries. Authentication or billing resets disable Redis result caching until valid definitions load again. Remote requests use their request-start generation only as a local invalidation boundary, not as proof of the server's definition snapshot.
Redis result writes remain synchronous. This change does not add Redis timeouts or make single-result cache writes nonblocking. Old entries can remain until their existing TTL expires, but upgraded readers only accept matching provenance.
Optional harness coverage is separate. This PR does not opt an SDK adapter into that coverage.
💚 How did you test it?
Using the existing cached virtualenv with
PYTHONDONTWRITEBYTECODE=1andPYTHONPATH=.:ef127fd. Permanent regression tests failed 24 cases on that head and pass with the fix. They cover restart, different worker snapshots, synchronous/asynchronous providers, cohort/group-mapping-only changes, paused writes, old entries without metadata, and delayed remote responses across hydration or reset.806f735. Three regression cases failed before replacing shared remote-only provenance with per-Client identities. Same-Client fallback, matching-definition reuse, and fork controls pass.3dbe088passed isolated committed-branch autoreview againstorigin/mainwith no actionable findings.Targeted mutation CI repair
The targeted job stopped before mutation testing because its CRAP gate selects only
test_utils.pyandtest_size_limited_dict.py. Cache publication tests in other files did not cover the new snapshot paths in that selection. The CRAP calculator also counts non-executable lines, and the Redis read method's complexity of 10 could not satisfy the strict below-10 limit.if/else. Redis writes select the fingerprint once. Publication fences, write locks, Redis I/O ordering, and public signatures are unchanged.utils.py. The maximum CRAP score is 8.66, below the unchanged limit of 10. A fresh actual mutation run killed all 535 mutants, with emptymutmut resultsand no survivors, timeouts, or untested mutants.a5a6dfbpassed isolated committed-branch autoreview againstorigin/mainwith no actionable findings. All GitHub checks fora5a6dfb8e96c4b5cb8ad4e857ab2a18da3e6209ecompleted successfully, with no failed, pending, or skipped checks. The CI run includes Python 3.10 through 3.14 tests. Its targeted mutation job executed mutation testing rather than skipping on a cache hit and killed all 535 mutants. Both capture protocols also passed SDK compliance.Full repository tests and generated mirror packaging were not run locally for this repair. Redis regressions use the repository's deterministic FakeRedis, not a live Redis service.
📝 Checklist
If releasing new changes
sampo addto generate a changeset fileThe existing
.sampo/changesets/versioned-property-matching.mdpatch changeset was updated.sampo addwas not rerun and no duplicate changeset was added.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Pi agents implemented and tested this user-directed SDK contract update using repository inspection, Python/pytest, Ruff, Git, GitHub CLI, and the isolated autoreview helper. Human review is required before merging. No public agent session link is available.
Fresh review found that a process-local generation could allow invalidated Redis entries to revive after restart. The repair adds private snapshot provenance without putting Redis I/O under the publication lock. The user-directed review workflow approved the additive metadata and stricter handling of unverifiable remote-only entries. Legacy matching defaults, public cache APIs, and Redis key formats are preserved.