Skip to content

feat: support starts_with and ends_with operators in local evaluation - #209

Merged
haacked merged 3 commits into
mainfrom
posthog-code/starts-with-ends-with-operators
Aug 5, 2026
Merged

feat: support starts_with and ends_with operators in local evaluation#209
haacked merged 3 commits into
mainfrom
posthog-code/starts-with-ends-with-operators

Conversation

@haacked

@haacked haacked commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

PostHog/posthog#72992 added four case-insensitive property filter operators — starts_with, not_starts_with, ends_with, not_ends_with — across HogQL, the flags service, and the UI. The UI surface is gated behind a feature flag until server-side SDKs can evaluate these operators locally; on SDK versions without support, flags using them fall back to remote evaluation.

This adds local evaluation support for the four operators, mirroring icontains: both sides are stringified and lowercased, and the not_ variants are exact negations. Filter values are single strings — server-side validation rejects list values for these operators, matching the flags service behavior.

💚 How did you test it?

New unit tests mirror the flags service's own test matrix for these operators: case-insensitive anchored matching, mid-string non-matches (prevalue does not match starts_with: "Val"), numeric stringification (323 matches starts_with: "3", 123 does not), negation inverses, and missing-key inconclusive behavior.

vendor/bin/phpunit test/FeatureFlagLocalEvaluationTest.php passes locally (109 tests, 2531 assertions), and composer run api:check confirms the public API snapshot is unchanged.

📝 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

  • Added a changeset file (minor)

Created with PostHog Code

Adds local evaluation for the starts_with, not_starts_with, ends_with,
and not_ends_with property filter operators (PostHog/posthog#72992).
Matching is case-insensitive and mirrors icontains: both sides are
stringified and lowercased, and the not_ variants are exact negations.

Generated-By: PostHog Code
Task-Id: ef980bb5-ff81-4191-a7df-796e932b8251
@haacked haacked self-assigned this Jul 31, 2026
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

posthog-php Compliance Report

Date: 2026-08-05 15:32:17 UTC
Duration: 95317ms

✅ All Tests Passed!

46/46 tests passed


Capture Tests

29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields 12ms
Format Validation.Event Has Uuid 5ms
Format Validation.Event Has Lib Properties 6ms
Format Validation.Distinct Id Is String 6ms
Format Validation.Token Is Present 6ms
Format Validation.Custom Properties Preserved 5ms
Format Validation.Event Has Timestamp 6ms
Retry Behavior.Retries On 503 5315ms
Retry Behavior.Does Not Retry On 400 2010ms
Retry Behavior.Does Not Retry On 401 2009ms
Retry Behavior.Respects Retry After Header 8011ms
Retry Behavior.Implements Backoff 15730ms
Retry Behavior.Retries On 500 5115ms
Retry Behavior.Retries On 502 5115ms
Retry Behavior.Retries On 504 5116ms
Retry Behavior.Max Retries Respected 16526ms
Deduplication.Generates Unique Uuids 12ms
Deduplication.Preserves Uuid On Retry 5113ms
Deduplication.Preserves Uuid And Timestamp On Retry 10312ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 5117ms
Deduplication.No Duplicate Events In Batch 12ms
Deduplication.Different Events Have Different Uuids 7ms
Compression.Sends Gzip When Enabled 5ms
Batch Format.Uses Proper Batch Structure 6ms
Batch Format.Flush With No Events Sends Nothing 4ms
Batch Format.Multiple Events Batched Together 10ms
Error Handling.Does Not Retry On 403 2008ms
Error Handling.Does Not Retry On 413 2009ms
Error Handling.Retries On 408 5114ms

Feature_Flags Tests

17/17 tests passed

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

The changeset previously claimed these operators fell back to remote
evaluation; unrecognized operators actually evaluated to false locally.
Tests now cover the missing-key inconclusive path and explicit null
property values for all four operators.

Generated-By: PostHog Code
Task-Id: bb15888a-04c4-4dc0-916e-793a0540ece3
@posthog

posthog Bot commented Aug 3, 2026

Copy link
Copy Markdown

🦔 ReviewHog reviewed this pull request

Nothing worth raising this time, so here's a calming picture instead:

A panda relaxing and waving

@haacked
haacked marked this pull request as ready for review August 4, 2026 20:24
@haacked
haacked requested a review from a team as a code owner August 4, 2026 20:24
@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown
Prompt To Fix All With AI
### Issue 1
test/FeatureFlagLocalEvaluationTest.php:336
**Duplicate operator test matrices**

The starts-with and ends-with tests manually repeat the same positive, negated, case-insensitive, numeric, null, and nonmatching scenarios. A parameterised matrix would keep the shared contract in one place and prevent sibling operators from receiving inconsistent coverage as these cases change.

---

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

Reviews (1): Last reviewed commit: "Fix changeset wording and add missing-ke..." | Re-trigger Greptile

Comment thread test/FeatureFlagLocalEvaluationTest.php

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One non-blocking parity question from the cross-SDK review.

Comment thread lib/FeatureFlag.php
}

if ($operator == "starts_with") {
return str_starts_with(strtolower(FeatureFlag::valueToString($overrideValue)), strtolower(FeatureFlag::valueToString($value)));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

strtolower() performs ASCII-only case conversion, so international values can differ from the Python SDK’s Unicode casefold() behavior (for example, Äpfel does not start with ä here). The current implementation does match PHP’s existing icontains convention and the authoritative flags service, which uses ASCII-only lowering. Would you prefer server/PHP consistency as implemented, or literal Python SDK parity for Unicode values? Either choice seems defensible, but a non-ASCII test or documentation note would make the intended contract explicit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I decided to match existing PHP conventions. In all the SDKs, I matched local conventions.

I think we may want to try and match what happens on the server as closely as possible. I opened a separate issue on that: PostHog/posthog#78019

I'd rather do that as an all or nothing follow-up.

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One forward-compatibility concern from the cross-SDK review.

"posthog-php": minor
---

Support the `starts_with`, `not_starts_with`, `ends_with`, and `not_ends_with` property filter operators in feature flag local evaluation. Matching is case-insensitive and mirrors `icontains`. Previously, local evaluation treated these operators as unrecognized and silently evaluated their conditions to `false`; they now match correctly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The previous silent-false behavior still applies to any future operator the server adds: matchProperty() reaches its final return false, so the condition can produce an incorrect local result without triggering remote fallback. The other SDK implementations generally treat an unknown operator as inconclusive and fall back to /flags. Could we change the unknown-operator path to throw InconclusiveMatchException and add a regression test using something like future_operator? This is pre-existing behavior, but this PR demonstrates the forward-compatibility risk.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed!

Unknown operators now throw InconclusiveMatchException so the flag
defers to the /flags endpoint instead of silently evaluating to false,
matching the other SDKs. Non-ASCII tests pin the intentional ASCII-only
strtolower contract, which mirrors the flags service.

Generated-By: PostHog Code
Task-Id: bb15888a-04c4-4dc0-916e-793a0540ece3
@haacked
haacked merged commit 0a58f7f into main Aug 5, 2026
21 checks passed
@haacked
haacked deleted the posthog-code/starts-with-ends-with-operators branch August 5, 2026 17:23
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