feat: match non-set operators against array-typed user properties#74
Merged
Conversation
Route non-set operators (is, contains, greater, etc.) through the same
list-coercion path as set operators. When the property value is a list
or a JSON array string, match against each element with any-match
semantics — the condition is true if any single element satisfies the
operator, including for negation operators like `is not` and
`does not contain`. Scalar values continue to flow through the existing
single-string match path.
Add a `startswith("[")` guard to coerce_string_array so scalar strings
skip JSON parsing (and its exception) on every evaluation.
Fix two pre-existing bugs in coerce_string_array surfaced by the new
path: the JSON-array branch iterated the original string value rather
than the parsed list, and scalars were wrapped as `[s]` rather than
returning None.
Add engine-level unit tests covering scalar, list, and JSON-array-string
property values across set and non-set operators. Update the integration
test deployment key to the new superset deployment and add operator
cases for arrays and JSON array strings.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Negation operators use wrong quantifier on arrays
- Changed match_strings_non_set to use all() instead of any() for negation operators (IS_NOT, DOES_NOT_CONTAIN, REGEX_DOES_NOT_MATCH), ensuring correct universal quantification semantics where no element matches rather than at least one element doesn't match.
Or push these changes by commenting:
@cursor push 0ccd8617c8
Preview (0ccd8617c8)
diff --git a/src/amplitude_experiment/evaluation/engine.py b/src/amplitude_experiment/evaluation/engine.py
--- a/src/amplitude_experiment/evaluation/engine.py
+++ b/src/amplitude_experiment/evaluation/engine.py
@@ -234,6 +234,15 @@
filter_values: List[str]
) -> bool:
"""Match any element of a multi-valued property against a non-set operator."""
+ if op in {
+ EvaluationOperator.IS_NOT,
+ EvaluationOperator.DOES_NOT_CONTAIN,
+ EvaluationOperator.REGEX_DOES_NOT_MATCH,
+ }:
+ return all(
+ self.match_string(prop_value, op, filter_values)
+ for prop_value in prop_values
+ )
return any(
self.match_string(prop_value, op, filter_values)
for prop_value in prop_valuesYou can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit b07bfd4. Configure here.
Collaborator
Author
|
Rejecting the Bugbot suggestion to use The current
Added an inline docstring at |
vaibhav-jain-exp
approved these changes
Apr 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
is,contains,greater, etc.) through the same list-coercion path as set operators so they match array/JSON-array-string user properties using any-match semantics (including negation operators likeis notanddoes not contain). Scalars still flow through the existing single-string path.startswith("[")guard tocoerce_string_arrayso scalar strings skip JSON parsing (and its exception) on every evaluation.coerce_string_arraysurfaced by the new path: the JSON-array branch iterated the original string value instead of the parsed list, and scalars were wrapped as[s]instead of returningNone.Test plan
is,is not,contains,does not contain, andset is.Note
Medium Risk
Changes core targeting semantics for multi-valued properties and JSON-array strings, which can alter flag rollout behavior across existing segments. Added tests reduce regression risk but evaluation logic changes warrant careful review.
Overview
Multi-valued user properties now work with non-set operators.
EvaluationEngine.match_conditionroutes non-set ops (e.g.,is,contains, comparisons) through array coercion and applies any-element matching via newmatch_strings_non_set, while preserving the existing scalar-string path.Array coercion behavior is tightened/fixed.
coerce_string_arraynow only returns lists for actual Python lists or JSON array strings (guarded bystartswith("[")), and no longer falls back to wrapping scalars / malformed JSON; this also fixes the prior bug where the parsed JSON array wasn’t actually iterated.Tests expanded. Adds a new unit test suite for array/non-set matching and extends integration tests (plus a new deployment key) to cover list + JSON-array-string inputs for both set and non-set operators.
Reviewed by Cursor Bugbot for commit 29dddfa. Bugbot is set up for automated code reviews on this repo. Configure here.