Skip to content

fix(flags): fall back for incompatible local regex evaluation - #316

Merged
marandaneto merged 3 commits into
mainfrom
fix/local-regex-compatibility-5354
Oct 23, 2026
Merged

marandaneto merged 3 commits into
mainfrom
fix/local-regex-compatibility-5354

Conversation

@marandaneto

@marandaneto marandaneto commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

💡 Motivation and Context

Local feature flag regex evaluation can return a different boolean from the service even when Ruby compiles the pattern. For example, Ruby's \d+ does not match ١, while the service matches it. Ruby also treats ^foo$ as a line match within a multiline value, unlike the service's default absolute anchors. These local booleans prevent remote fallback.

This adapts the agreed shared-syntax/local and unsupported-syntax/fallback behavior from posthog-js #5354 and its cross-SDK discussion to Ruby's Regexp semantics. It does not assume that JS PR is merged or released.

The private classifier now raises the existing inconclusive error for incompatible or uncertain cases. Remote-capable callers fall back, and local-only callers remain unresolved. not_regex only negates successful searches. Invalid patterns in the supported subset still return false for both operators, and unsupported failures cannot become matches through cohort negation.

Common patterns stay local, including ASCII shorthand, scalar Unicode literals and dot, empty patterns, escaped punctuation and class-local backspace. The compatibility scan now tokenizes multibyte patterns once and parses repetition bounds with a forward-only cursor instead of repeatedly indexing Unicode strings or copying suffixes. This preserves Unicode literals locally with O(pattern length) token storage. Coverage is deliberately reduced for uncertain syntax, POSIX/set classes, Unicode shorthand on non-ASCII values, newline-sensitive anchors, boundaries and some repetitions. This does not claim universal Unicode, compiler-limit or backtracking-budget equivalence. Existing operand stringification and property-matching-version behavior are unchanged. No public API, dependency or regex engine was added.

💚 How did you test it?

After safely fast-forwarding this branch to current main, on Ruby 4.0.7 with the committed bundle:

  • Focused regex specs: 234 examples, zero failures. These exercise real matching, both public callers, remote interception, missing-property controls, local-only behavior and cohort negation.
  • Full default RSpec suite: 1,792 examples, zero failures, two expected pending optional-OpenTelemetry integration tests.
  • Full RuboCop: 134 files, no offenses.
  • Public API snapshot check and core/Rails gem builds passed.
  • Saved pre-fix runs demonstrated 16 wrong definitive results across 54 operator evaluations, then RED regression runs before the matcher and repetition fixes. The initial compatibility implementation produced no wrong definitive results across 9,736 operator evaluations of the supplied frozen service corpus. For the scanner follow-up, 26 targeted real Ruby/service outcomes agree for the new multibyte local and shared-invalid controls. Inconclusive results are allowed and are not counted as local matches.

The traversal concern was reproduced before source changes on Ruby 4.0.7. Median classifier timings for 25k/50k/100k multibyte literals were 138/551/2191 ms before and 3.2/6.1/12.1 ms after. The real matcher at 100k went from 2272 ms to 15.2 ms. Repeated-bound patterns also scaled approximately linearly afterward. These are bounded three-sample measurements, not unit-test timing thresholds or universal latency guarantees.

Other Ruby versions and the optional OpenTelemetry bundle were not run locally. No live PostHog service was used. CI results are separate from these local checks.

At the post-push CI checkpoint, RSpec on Ruby 3.2/3.3/3.4, both locked OpenTelemetry integration jobs, lint, API and gem build passed. The latest Rails / stable Ruby job failed 48 MCP integration examples after its fresh dependency resolution, including MCP 1.8.0. Those failures were recorded, not investigated or fixed as part of this regex traversal comment. Overall CI is not green.

📝 Checklist

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

If releasing new changes

  • Added a posthog-ruby patch changeset directly, following RELEASING.md. No version bump was made.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

@marandaneto directed this Ruby port. A Pi worker used local Git, Ruby/RSpec, the frozen Rust service oracle, deslop, test-audit and isolated branch autoreview. The session transcript is local, not publicly linked. The implementation uses conservative existing fallback instead of replacing the regex engine.

Final branch autoreview on dadad6eb8992dd46cfcff72a98e98b3476bc450c against origin/main completed with no findings. Earlier class-backspace concerns were checked with actual service and Ruby probes rather than plain Rust regex assumptions. Human review is still required.

@marandaneto marandaneto self-assigned this Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

posthog-ruby-sync Compliance Report

Date: 2026-10-23T10:23:43.339047+00:00
Duration: 94259ms

⚠️ Some Tests Failed

45/47 tests passed, 2 failed


Capture Tests

⚠️ 29/30 tests passed, 1 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 10ms
Format Validation.Event Has Uuid ✅ 6ms
Format Validation.Event Has Lib Properties ✅ 6ms
Format Validation.Distinct Id Is String ✅ 9ms
Format Validation.Token Is Present ✅ 7ms
Format Validation.Custom Properties Preserved ✅ 6ms
Format Validation.Event Has Timestamp ✅ 8ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 7ms
Retry Behavior.Retries On 503 ✅ 5245ms
Retry Behavior.Does Not Retry On 400 ✅ 2010ms
Retry Behavior.Does Not Retry On 401 ✅ 2012ms
Retry Behavior.Respects Retry After Header ✅ 8016ms
Retry Behavior.Implements Backoff ✅ 15318ms
Retry Behavior.Retries On 500 ✅ 5145ms
Retry Behavior.Retries On 502 ✅ 5160ms
Retry Behavior.Retries On 504 ✅ 5165ms
Retry Behavior.Max Retries Respected ✅ 15660ms
Deduplication.Generates Unique Uuids ✅ 23ms
Deduplication.Preserves Uuid On Retry ✅ 5157ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10319ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5165ms
Deduplication.No Duplicate Events In Batch ✅ 20ms
Deduplication.Different Events Have Different Uuids ✅ 8ms
Compression.Sends Gzip When Enabled ✅ 8ms
Batch Format.Uses Proper Batch Structure ✅ 5ms
Batch Format.Flush With No Events Sends Nothing ✅ 3ms
Batch Format.Multiple Events Batched Together ❌ 17ms
Error Handling.Does Not Retry On 403 ✅ 2008ms
Error Handling.Does Not Retry On 413 ✅ 2010ms
Error Handling.Retries On 408 ✅ 5117ms

Failures

batch_format.multiple_events_batched_together

Expected 1 requests, got 5

Feature_Flags Tests

⚠️ 16/17 tests passed, 1 failed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 11ms
Request Payload.Flags Request Uses V2 Query Param ✅ 8ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 8ms
Request Payload.Flags Request Omits Authorization Header ✅ 7ms
Request Payload.Token In Flags Body Matches Init ✅ 6ms
Request Payload.Groups Round Trip ✅ 7ms
Request Payload.Groups Default To Empty Object ✅ 6ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 7ms
Request Payload.Disable Geoip Omitted Defaults To False ❌ 6ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 7ms
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 ✅ 9ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 8ms
Retry Behavior.Retries Flags On 502 ✅ 109ms
Retry Behavior.Retries Flags On 504 ✅ 142ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 9ms

Failures

request_payload.disable_geoip_omitted_defaults_to_false

Field 'geoip_disable' not found in /flags request body at path 'geoip_disable'. Available keys: ['distinct_id', 'groups', 'person_properties', 'group_properties', 'flag_keys_to_evaluate', 'token']

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

posthog-ruby-async Compliance Report

Date: 2026-10-23T10:23:56.637543+00:00
Duration: 98135ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

✅ 30/30 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 107ms
Format Validation.Event Has Uuid ✅ 104ms
Format Validation.Event Has Lib Properties ✅ 107ms
Format Validation.Distinct Id Is String ✅ 105ms
Format Validation.Token Is Present ✅ 106ms
Format Validation.Custom Properties Preserved ✅ 106ms
Format Validation.Event Has Timestamp ✅ 105ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 8ms
Retry Behavior.Retries On 503 ✅ 5309ms
Retry Behavior.Does Not Retry On 400 ✅ 2107ms
Retry Behavior.Does Not Retry On 401 ✅ 2107ms
Retry Behavior.Respects Retry After Header ✅ 8014ms
Retry Behavior.Implements Backoff ✅ 15713ms
Retry Behavior.Retries On 500 ✅ 5211ms
Retry Behavior.Retries On 502 ✅ 5211ms
Retry Behavior.Retries On 504 ✅ 5210ms
Retry Behavior.Max Retries Respected ✅ 15512ms
Deduplication.Generates Unique Uuids ✅ 109ms
Deduplication.Preserves Uuid On Retry ✅ 5210ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10417ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5211ms
Deduplication.No Duplicate Events In Batch ✅ 108ms
Deduplication.Different Events Have Different Uuids ✅ 105ms
Compression.Sends Gzip When Enabled ✅ 104ms
Batch Format.Uses Proper Batch Structure ✅ 105ms
Batch Format.Flush With No Events Sends Nothing ✅ 2ms
Batch Format.Multiple Events Batched Together ✅ 107ms
Error Handling.Does Not Retry On 403 ✅ 2107ms
Error Handling.Does Not Retry On 413 ✅ 2106ms
Error Handling.Retries On 408 ✅ 5210ms

Feature_Flags Tests

⚠️ 16/17 tests passed, 1 failed

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

Failures

request_payload.disable_geoip_omitted_defaults_to_false

Field 'geoip_disable' not found in /flags request body at path 'geoip_disable'. Available keys: ['distinct_id', 'groups', 'person_properties', 'group_properties', 'flag_keys_to_evaluate', 'token']

@posthog posthog Bot left a comment

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.

PostHog Review · Standard · reviewed d75ef7f: 1 Should fix.

This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.

Comment thread lib/posthog/feature_flags.rb Outdated
@marandaneto
marandaneto requested a review from a team October 23, 2026 08:42
@marandaneto
marandaneto marked this pull request as ready for review October 23, 2026 08:42
@marandaneto
marandaneto requested a review from a team as a code owner October 23, 2026 08:42
@posthog-project-board-bot posthog-project-board-bot Bot moved this to In Progress in Feature Flags Oct 23, 2026
@posthog-project-board-bot posthog-project-board-bot Bot moved this from In Progress to In Review in Feature Flags Oct 23, 2026

@posthog posthog Bot left a comment

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.

PostHog Review · Standard · reviewed dadad6e: no findings.

This Standard review started from the posthog-review label. Comment @posthog review --deep for a Deep review, or @posthog help to see all commands.

@posthog posthog Bot removed the posthog-review label Oct 23, 2026
@github-project-automation github-project-automation Bot moved this from In Review to Approved in Feature Flags Oct 23, 2026
@posthog-project-board-bot posthog-project-board-bot Bot moved this from Approved to In Review in Feature Flags Oct 23, 2026
@marandaneto
marandaneto merged commit 4003bb2 into main Oct 23, 2026
25 checks passed
@marandaneto
marandaneto deleted the fix/local-regex-compatibility-5354 branch October 23, 2026 10:26
@github-project-automation github-project-automation Bot moved this from In Review to Done in Feature Flags Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants