From 9daf8f4cec080ffc34cc7307055cdec82864d732 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Thu, 30 Jul 2026 18:21:02 -0700 Subject: [PATCH] feat: support starts_with and ends_with operators in local evaluation 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 --- .changeset/starts-with-ends-with-operators.md | 5 ++ lib/posthog/feature_flags.rb | 8 +++ spec/posthog/feature_flag_spec.rb | 71 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 .changeset/starts-with-ends-with-operators.md diff --git a/.changeset/starts-with-ends-with-operators.md b/.changeset/starts-with-ends-with-operators.md new file mode 100644 index 0000000..f7a9acd --- /dev/null +++ b/.changeset/starts-with-ends-with-operators.md @@ -0,0 +1,5 @@ +--- +"posthog-ruby": 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`, so flags using these operators no longer fall back to remote evaluation. diff --git a/lib/posthog/feature_flags.rb b/lib/posthog/feature_flags.rb index e125886..1c4790e 100644 --- a/lib/posthog/feature_flags.rb +++ b/lib/posthog/feature_flags.rb @@ -654,6 +654,14 @@ def self.match_property(property, property_values, cohort_properties = {}) override_value.to_s.downcase.include?(value.to_s.downcase) when 'not_icontains' !override_value.to_s.downcase.include?(value.to_s.downcase) + when 'starts_with' + override_value.to_s.downcase.start_with?(value.to_s.downcase) + when 'not_starts_with' + !override_value.to_s.downcase.start_with?(value.to_s.downcase) + when 'ends_with' + override_value.to_s.downcase.end_with?(value.to_s.downcase) + when 'not_ends_with' + !override_value.to_s.downcase.end_with?(value.to_s.downcase) when 'regex' PostHog::Utils.is_valid_regex(value.to_s) && !Regexp.new(value.to_s).match(override_value.to_s).nil? when 'not_regex' diff --git a/spec/posthog/feature_flag_spec.rb b/spec/posthog/feature_flag_spec.rb index cbead9f..d7b4ff2 100644 --- a/spec/posthog/feature_flag_spec.rb +++ b/spec/posthog/feature_flag_spec.rb @@ -1475,6 +1475,77 @@ module PostHog expect(FeatureFlagsPoller.match_property(property_b, { 'key' => 'three' })).to be false end + it 'with operator starts_with' do + property_a = { 'key' => 'key', 'value' => 'Val', 'operator' => 'starts_with' } + + expect(FeatureFlagsPoller.match_property(property_a, { 'key' => 'value' })).to be true + expect(FeatureFlagsPoller.match_property(property_a, { 'key' => 'VALUE' })).to be true + expect(FeatureFlagsPoller.match_property(property_a, { 'key' => 'vaLue4' })).to be true + + expect(FeatureFlagsPoller.match_property(property_a, { 'key' => 'prevalue' })).to be false + expect(FeatureFlagsPoller.match_property(property_a, { 'key' => 'Alakazam' })).to be false + expect(FeatureFlagsPoller.match_property(property_a, { 'key' => '' })).to be false + expect(FeatureFlagsPoller.match_property(property_a, { 'key' => nil })).to be false + expect(FeatureFlagsPoller.match_property(property_a, { 'key' => 123 })).to be false + + expect do + FeatureFlagsPoller.match_property(property_a, { 'key2' => 'value' }) + end.to raise_error(InconclusiveMatchError) + expect { FeatureFlagsPoller.match_property(property_a, {}) }.to raise_error(InconclusiveMatchError) + + property_b = { 'key' => 'key', 'value' => '3', 'operator' => 'starts_with' } + + expect(FeatureFlagsPoller.match_property(property_b, { 'key' => '3' })).to be true + expect(FeatureFlagsPoller.match_property(property_b, { 'key' => 323 })).to be true + + expect(FeatureFlagsPoller.match_property(property_b, { 'key' => 123 })).to be false + expect(FeatureFlagsPoller.match_property(property_b, { 'key' => 'val3' })).to be false + + property_c = { 'key' => 'key', 'value' => 'Val', 'operator' => 'not_starts_with' } + + expect(FeatureFlagsPoller.match_property(property_c, { 'key' => 'value' })).to be false + expect(FeatureFlagsPoller.match_property(property_c, { 'key' => 'VALUE' })).to be false + + expect(FeatureFlagsPoller.match_property(property_c, { 'key' => 'prevalue' })).to be true + expect(FeatureFlagsPoller.match_property(property_c, { 'key' => 'Alakazam' })).to be true + end + + it 'with operator ends_with' do + property_a = { 'key' => 'key', 'value' => 'lUe', 'operator' => 'ends_with' } + + expect(FeatureFlagsPoller.match_property(property_a, { 'key' => 'value' })).to be true + expect(FeatureFlagsPoller.match_property(property_a, { 'key' => 'VALUE' })).to be true + expect(FeatureFlagsPoller.match_property(property_a, { 'key' => '343tfvalue' })).to be true + + expect(FeatureFlagsPoller.match_property(property_a, { 'key' => 'value2' })).to be false + expect(FeatureFlagsPoller.match_property(property_a, { 'key' => 'Alakazam' })).to be false + expect(FeatureFlagsPoller.match_property(property_a, { 'key' => '' })).to be false + expect(FeatureFlagsPoller.match_property(property_a, { 'key' => nil })).to be false + expect(FeatureFlagsPoller.match_property(property_a, { 'key' => 123 })).to be false + + expect do + FeatureFlagsPoller.match_property(property_a, { 'key2' => 'value' }) + end.to raise_error(InconclusiveMatchError) + expect { FeatureFlagsPoller.match_property(property_a, {}) }.to raise_error(InconclusiveMatchError) + + property_b = { 'key' => 'key', 'value' => '3', 'operator' => 'ends_with' } + + expect(FeatureFlagsPoller.match_property(property_b, { 'key' => '3' })).to be true + expect(FeatureFlagsPoller.match_property(property_b, { 'key' => 323 })).to be true + expect(FeatureFlagsPoller.match_property(property_b, { 'key' => 13 })).to be true + + expect(FeatureFlagsPoller.match_property(property_b, { 'key' => 321 })).to be false + expect(FeatureFlagsPoller.match_property(property_b, { 'key' => '3val' })).to be false + + property_c = { 'key' => 'key', 'value' => 'lUe', 'operator' => 'not_ends_with' } + + expect(FeatureFlagsPoller.match_property(property_c, { 'key' => 'value' })).to be false + expect(FeatureFlagsPoller.match_property(property_c, { 'key' => 'VALUE' })).to be false + + expect(FeatureFlagsPoller.match_property(property_c, { 'key' => 'value2' })).to be true + expect(FeatureFlagsPoller.match_property(property_c, { 'key' => 'Alakazam' })).to be true + end + it 'with operator regex' do property_a = { 'key' => 'key', 'value' => '\.com$', 'operator' => 'regex' }