Skip to content

feat: match array properties element-wise for non-set operators#92

Open
kyeh-amp wants to merge 2 commits intomainfrom
feat/multi-value-array-support
Open

feat: match array properties element-wise for non-set operators#92
kyeh-amp wants to merge 2 commits intomainfrom
feat/multi-value-array-support

Conversation

@kyeh-amp
Copy link
Copy Markdown
Collaborator

@kyeh-amp kyeh-amp commented Apr 22, 2026

Summary

Adds element-wise (any-match) evaluation for multi-valued user properties when paired with non-set operators (is, contains, greater, etc.), aligning the local evaluation engine with Amplitude analytics/charts behavior.

Previously, non-set operators always stringified the whole property value via coerce_string — so an array property like ["a","b"] would never satisfy is "a". Now:

  • match_condition calls coerce_string_array for every non-null value. If the value is multi-valued (native array or JSON array string) and the operator is non-set, it delegates to a new match_strings_non_set helper that returns true if any element satisfies the operator.
  • Negation operators (is not, does not contain) also use any-match semantics — is not "A" on ["A","B"] is true because "B" is not "A".
  • Scalars still fall through to the existing coerce_stringmatch_string path unchanged.

Also fixes two issues in coerce_string_array:

  • Added a start_with?('[') pre-check so scalar strings skip JSON.parse and avoid exception-driven control flow on every evaluation.
  • Corrected a pre-existing bug where scalars (non-array JSON or malformed JSON) were returned as [value] instead of nil, which caused set operators to spuriously match single-string properties.

Tests

  • New spec/experiment/evaluation/evaluation_spec.rb with an inline test harness (flag_with_condition, context_with_prop, evaluate, assert_match, assert_no_match) and 14 unit cases covering scalars, native arrays, JSON array strings, malformed JSON, and leading-whitespace edge cases.
  • 7 new integration cases added inline in the existing operator tests block; the deployment key is consolidated to server-VVhLULXCxxY0xqmszXouXxiEzoeJWmSh (a superset of the prior key, containing all existing flags plus the new multi-value flags).

Full suite: 271 examples, 0 failures. Rubocop: clean.

Checklist

  • Does your PR title have the correct title format?
  • Does your PR have a breaking change?: No

Note

Medium Risk
Changes core flag targeting semantics when a property is an array/JSON array string, which can alter which users qualify for segments. Also tightens coerce_string_array parsing behavior, potentially changing outcomes for previously mis-coerced scalar values.

Overview
The evaluation engine now treats multi-valued properties (native arrays or JSON array strings) as element-wise inputs for non-set operators like is, contains, and comparisons, matching if any element satisfies the operator (via new match_strings_non_set).

coerce_string_array is tightened to only parse JSON when the string looks like an array (starts with '[') and to return nil for non-array/malformed JSON instead of coercing scalars into single-element arrays, reducing accidental set-operator matches. Tests are expanded with new unit coverage and additional integration cases, plus the integration spec caches the flags URI and updates the deployment key.

Reviewed by Cursor Bugbot for commit c73b5c6. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Negation operators on arrays use wrong quantifier
    • Changed match_strings_non_set to use all? for negation operators (IS_NOT, DOES_NOT_CONTAIN, REGEX_DOES_NOT_MATCH) instead of any?, ensuring correct 'no element matches' semantics.

Create PR

Or push these changes by commenting:

@cursor push 130164c824
Preview (130164c824)
diff --git a/lib/experiment/evaluation/evaluation.rb b/lib/experiment/evaluation/evaluation.rb
--- a/lib/experiment/evaluation/evaluation.rb
+++ b/lib/experiment/evaluation/evaluation.rb
@@ -82,9 +82,22 @@
       end
 
       def match_strings_non_set(prop_values, op, filter_values)
-        prop_values.any? { |v| match_string(v, op, filter_values) }
+        if negation_operator?(op)
+          prop_values.all? { |v| match_string(v, op, filter_values) }
+        else
+          prop_values.any? { |v| match_string(v, op, filter_values) }
+        end
       end
 
+      def negation_operator?(op)
+        case op
+        when Operator::IS_NOT, Operator::DOES_NOT_CONTAIN, Operator::REGEX_DOES_NOT_MATCH
+          true
+        else
+          false
+        end
+      end
+
       def get_hash(key)
         Murmur3.hash32x86(key)
       end

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit c9f7cb1. Configure here.


def match_strings_non_set(prop_values, op, filter_values)
prop_values.any? { |v| match_string(v, op, filter_values) }
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Negation operators on arrays use wrong quantifier

Medium Severity

match_strings_non_set uses any? uniformly for all operators, including negation ones like IS_NOT, DOES_NOT_CONTAIN, and REGEX_DOES_NOT_MATCH. Because match_string already negates internally (e.g. !matches_is?), any? produces "at least one element doesn't match" rather than "no element matches." This means a property like ["A","B"] simultaneously satisfies both IS "A" and IS NOT "A", which is a logical contradiction. The existing tests only use non-overlapping values for negation, so they don't distinguish between any? and the correct all? (or equivalently, !any? { positive_match }) semantics for negated operators.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c9f7cb1. Configure here.

@kyeh-amp kyeh-amp deployed to Unit Test April 22, 2026 21:26 — with GitHub Actions Active
@kyeh-amp kyeh-amp requested a review from a team April 22, 2026 21:40
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