Skip to content

fix(flags): honor versioned local property matching - #317

Draft
marandaneto wants to merge 2 commits into
mainfrom
fix/versioned-boolean-local-evaluation
Draft

fix(flags): honor versioned local property matching#317
marandaneto wants to merge 2 commits into
mainfrom
fix/versioned-boolean-local-evaluation

Conversation

@marandaneto

@marandaneto marandaneto commented Sep 5, 2026

Copy link
Copy Markdown
Member

💡 Motivation and Context

Local feature flag evaluation needs to honor the property matching version returned with flag definitions. Boolean coercion can otherwise produce different results from the service, such as matching false against "banana" or rejecting "true" from ["true", "false"].

This follows the backend change and shared matching contract.

  • Retain nullable property_matching_version metadata in the definitions response. Only exactly version 2 enables explicit matching. Missing, version 1, and other versions keep legacy behavior.
  • Use normalized scalar equality and nonempty filter-array member equality in version 2. Empty filter arrays retain recursive truthiness. is_not complements exact matching for known properties. Missing properties and unsupported null filters remain inconclusive.
  • Normalize decimal properties through their JSON wire representation for version 2. For example, 1.00m now matches numeric filter [1.00], whose canonical representation is 1.0. Preserve the distinction between integer 1 and floating-point 1.0. This correction does not change legacy decimal matching or other operators.
  • Read the version from the existing immutable LocalEvaluator snapshot for person, group, recursive cohort, and dependency evaluation. Existing loader replacement already keeps metadata with definitions, so no cache-provider changes are needed. Version-only refreshes take effect, while 304 responses and failures retain the previous snapshot.
  • Preserve the public helper's signature and legacy default. Include the existing PostHog patch changeset.

The optional harness coverage is related, but this PR does not opt an SDK adapter into it.

💚 How did you test it?

Independently reproduced the decimal regression on the previous PR head. Both exact and is_not returned the wrong result for 1.00m against [1.00] on both test targets, while the wire-equivalent JSON and legacy controls passed. Added permanent decimal cases for scale, negative values, zero, exponent formatting, high precision, large integers, scalar string matching, nonmatches, and legacy compatibility.

Validation of the submitted source:

dotnet restore tests/UnitTests/UnitTests.csproj --locked-mode
dotnet test tests/UnitTests/UnitTests.csproj --configuration Release --no-restore --nologo --filter 'FullyQualifiedName~VersionedPropertyMatchingTests|FullyQualifiedName~VersionedDefinitionSnapshots'
dotnet test tests/UnitTests/UnitTests.csproj --configuration Release --no-restore --nologo
dotnet build src/PostHog/PostHog.csproj --configuration Release --no-restore --nologo

All 57 focused tests passed on each target. Coverage includes exact/is_not across missing, 1, 2, 0, and 3 versions, boolean and nested-array operands, known nulls, Unicode normalization, decimal normalization, person/group/cohort/dependency propagation, and cached snapshot retention. Real PostHogClient single, bulk, and full-result APIs use mocked definitions HTTP across version-only reloads and assert that no remote /flags request is sent.

The full suite passed with 1,198 tests on net8.0 and 1,189 on netcoreapp3.1, with two existing cancellation-test skips per target. The first full run had an unrelated timing-sensitive failure in RetriesOnGatewayHttpStatusCodeThenSucceeds on netcoreapp3.1. That test passed in isolation and the full suite passed on rerun without changes. All three library targets built with zero warnings or errors, including public API analyzers. Focused whitespace and style checks passed with workspace-loading warnings only.

Both test targets use repository-configured major roll-forward to installed .NET 9.0.19, not native .NET 8 or 3.1 runtimes. Full solution, live-service, and native-runtime testing were not run. Unrelated recursive FilterSet serialization behavior observed during earlier test development was not changed.

git diff --check and the required isolated committed-branch autoreview passed on ee37c1c8404e1e02a51c236278b5f9ed257d0c7c against eadb15eaa78ee86469f9841eb23b8b14770b9e96, with no actionable findings. Local checks are not a claim that CI passed.

📝 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 pnpm changeset to generate a changeset file

The existing .changeset/versioned-property-matching.md selects a patch release for PostHog. It was preserved, not regenerated with pnpm changeset during publication.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Implemented and published with Pi agent assistance using Git, dotnet, GitHub CLI, and the isolated autoreview helper. The human-directed task was to apply the shared versioned matching contract and fix confirmed review findings in this draft. Local session artifacts are not publicly linked.

Kept the existing immutable evaluator snapshot as the metadata ownership boundary rather than adding cache infrastructure. Preserved nullable response metadata and the public legacy-default helper. The decimal correction reuses existing JSON normalization only for explicit matching. Human review is required before merge.

@marandaneto marandaneto self-assigned this Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

posthog-dotnet Compliance Report

Date: 2026-09-05 15:09:19 UTC
Duration: 4490ms

✅ All Tests Passed!

17/17 tests passed


Feature_Flags Tests

17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id 295ms
Request Payload.Flags Request Uses V2 Query Param 132ms
Request Payload.Flags Request Hits Flags Path Not Decide 111ms
Request Payload.Flags Request Omits Authorization Header 112ms
Request Payload.Token In Flags Body Matches Init 111ms
Request Payload.Groups Round Trip 115ms
Request Payload.Groups Default To Empty Object 110ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False 111ms
Request Payload.Disable Geoip Omitted Defaults To False 110ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key 111ms
Request Lifecycle.No Flags Request On Init Alone 4ms
Request Lifecycle.No Flags Request On Normal Capture 117ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests 215ms
Request Lifecycle.Mock Response Value Is Returned To Caller 115ms
Retry Behavior.Retries Flags On 502 1118ms
Retry Behavior.Retries Flags On 504 1115ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event 215ms

@marandaneto
marandaneto marked this pull request as ready for review September 5, 2026 17:29
@marandaneto
marandaneto requested a review from a team as a code owner September 5, 2026 17:29
@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
### Issue 1
src/PostHog/Json/PropertyFilterValue.cs:205-207
**Nested decimals normalize differently**

Version 2 applies `StringifyDecimal` only when the property itself is a `decimal`. A decimal nested in a CLR collection still uses the generic invariant conversion, so `[1.00m]` becomes `"[1.00]"` while the wire-equivalent JSON `[1.00]` becomes `"[1.0]"`. As a result, local `exact` and `is_not` evaluations can return different flag values depending on whether the same property arrived as CLR objects or JSON.

### Issue 2
src/PostHog/Json/PropertyFilterValue.cs:233-235
**Large decimals lose precision**

`StringifyDecimal` reparses the serialized number through `StringifyJsonElement`, which converts values outside the 64-bit integer ranges to `double`. Distinct high-precision decimals near `decimal.MaxValue` can therefore collapse to the same string and incorrectly satisfy version-2 `exact` matching. The maximum-value test does not catch this because both operands undergo the same lossy conversion.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(flags): normalize decimal properties..." | Re-trigger Greptile

Comment on lines +205 to +207
var comparand = overrideValue is decimal decimalValue
? StringifyDecimal(decimalValue)
: ToInvariantString(overrideValue);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Nested decimals normalize differently

Version 2 applies StringifyDecimal only when the property itself is a decimal. A decimal nested in a CLR collection still uses the generic invariant conversion, so [1.00m] becomes "[1.00]" while the wire-equivalent JSON [1.00] becomes "[1.0]". As a result, local exact and is_not evaluations can return different flag values depending on whether the same property arrived as CLR objects or JSON.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/PostHog/Json/PropertyFilterValue.cs
Line: 205-207

Comment:
**Nested decimals normalize differently**

Version 2 applies `StringifyDecimal` only when the property itself is a `decimal`. A decimal nested in a CLR collection still uses the generic invariant conversion, so `[1.00m]` becomes `"[1.00]"` while the wire-equivalent JSON `[1.00]` becomes `"[1.0]"`. As a result, local `exact` and `is_not` evaluations can return different flag values depending on whether the same property arrived as CLR objects or JSON.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +233 to +235
// Preserve the wire number's integer/float distinction and normalize its scale and precision like JSON filters.
using var document = JsonDocument.Parse(JsonSerializer.Serialize(value));
return StringifyJsonElement(document.RootElement);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Large decimals lose precision

StringifyDecimal reparses the serialized number through StringifyJsonElement, which converts values outside the 64-bit integer ranges to double. Distinct high-precision decimals near decimal.MaxValue can therefore collapse to the same string and incorrectly satisfy version-2 exact matching. The maximum-value test does not catch this because both operands undergo the same lossy conversion.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/PostHog/Json/PropertyFilterValue.cs
Line: 233-235

Comment:
**Large decimals lose precision**

`StringifyDecimal` reparses the serialized number through `StringifyJsonElement`, which converts values outside the 64-bit integer ranges to `double`. Distinct high-precision decimals near `decimal.MaxValue` can therefore collapse to the same string and incorrectly satisfy version-2 `exact` matching. The maximum-value test does not catch this because both operands undergo the same lossy conversion.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@marandaneto
marandaneto marked this pull request as draft September 6, 2026 07:00
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.

1 participant