Skip to content

feat(evaluation): multi-value array property support for non-set operators#65

Open
kyeh-amp wants to merge 6 commits into
mainfrom
evaluation-array-values-fix
Open

feat(evaluation): multi-value array property support for non-set operators#65
kyeh-amp wants to merge 6 commits into
mainfrom
evaluation-array-values-fix

Conversation

@kyeh-amp
Copy link
Copy Markdown
Contributor

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

Summary

Non-set operators (is, is not, contains, does not contain, greater, less, regex, version) now match array-valued user properties element-wise, returning true if any element satisfies the predicate. Previously, array values were stringified via coerceString and compared as a single blob, diverging from Amplitude analytics/charts behavior.

Applies to both native PHP arrays and JSON-array-string properties (e.g. '["a","b"]'). Set operators retain their existing semantics. Boolean short-circuit and scalar-string paths are unchanged.

Changes

  • matchCondition restructured so coerceStringArray is called for every non-null, non-boolean value. New matchStringsNonSet helper delegates to matchString per element.
  • coerceStringArray gains a [-prefix pre-check (via strncmp, PHP 7.4-safe) so scalar strings short-circuit before json_decode.
  • coerceStringArray now correctly preserves "0" and "" elements — it used to drop them via default array_filter truthiness, which contradicted the spec's "filter nulls only" contract.
  • Unreachable try/catch around json_decode removed (PHP returns null on parse failure; the existing is_array check already handles it).

Tests

  • Unit test harness + 16 cases in EvaluationEngineTest::testMultiValueArrayPropertyMatching covering scalars, JSON-array strings, PHP collections, malformed JSON, empty arrays, leading-whitespace strings, and falsy-string elements across both set and non-set operators.
  • 7 new integration tests in EvaluateIntegrationTest exercising the array-property flags on the shared evaluation deployment.
  • Integration deployment key consolidated to a single superset key per the cross-SDK spec.
  • Full suite: 154 tests / 2522 assertions passing; phpstan clean.

Test plan

  • ./vendor/bin/phpunit --no-coverage
  • ./vendor/bin/phpstan analyse
  • Spot-check: flag with is operator against a user property set to a PHP array — should now match any element (previously required set operator or JSON-array string contortions)

Note

Medium Risk
Changes core flag targeting semantics: non-set operators now evaluate array/JSON-array user properties element-wise, which can alter rollout/segment matches for existing flags. Risk is mitigated by added unit/integration coverage but still impacts evaluation behavior broadly.

Overview
Adds multi-value property matching for non-set operators. matchCondition now attempts to coerce non-null, non-boolean properties into string arrays and, for non-set operators (e.g. is, contains, comparisons, regex, version), returns true if any element matches via a new matchStringsNonSet helper; set operators keep existing semantics.

Tightens array coercion behavior. coerceStringArray now fast-paths non-array strings that don’t start with [ before attempting json_decode, removes the unused try/catch, and filters out only null elements (preserving falsy strings like "0"/"") via filterNonNullStrings.

Updates tests. Adds comprehensive unit coverage for scalar/array/JSON-array cases and edge conditions, and extends integration tests for array/JSON-array targeting; also updates the integration deployment key used to fetch test flags.

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

Restructure matchCondition so coerceStringArray runs for every
non-null, non-boolean value. Set operators keep matchSet semantics;
non-set operators against array-valued properties now delegate to a
new matchStringsNonSet helper that returns true if any element
satisfies the predicate -- matching Amplitude analytics behavior.

Scalar strings short-circuit via a strncmp prefix check in
coerceStringArray to avoid json_decode overhead on every eval.
…rray

Address code review feedback:

- coerceStringArray now drops only null elements (matching the spec's
  filterNotNull semantic). Previously array_filter without a callback
  silently dropped "0" and "" as well, which the new any-match path
  would have made more user-visible.
- Remove the unreachable try/catch around json_decode (PHP returns null
  on error by default; the is_array check already handles it). Drop
  the now-unused Exception import.
- Add two unit assertions pinning the new falsy-string behavior.
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 wrongly use any-match for arrays
    • Modified matchStringsNonSet to use all-must-satisfy logic for negation operators (IS_NOT, DOES_NOT_CONTAIN, REGEX_DOES_NOT_MATCH), ensuring they require all array elements to satisfy the negation rather than just any element.

Create PR

Or push these changes by commenting:

@cursor push 56abe827eb

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

Reviewed by Cursor Bugbot for commit 2045fa9. Configure here.

}
}
return false;
}
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 wrongly use any-match for arrays

High Severity

matchStringsNonSet applies "return true if ANY element satisfies" logic uniformly to all operators, including negation ones (is not, does not contain, regex does not match). This causes both is X and is not X to return true for the same array value (e.g., ['a', 'b'] with filter ['a']), breaking complementarity. For targeting, is not X on an array almost certainly means "X is not among the values" (all-must-satisfy / none-matches-positive), not "some element differs from X" which is trivially true for any multi-element array. The integration tests use non-overlapping values that pass under both interpretations, masking this issue.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2045fa9. Configure here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this valid?

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.

It's a valid assertion of the logic, but the relevance is off. In the end the implementation mirrors the observed filtering logic in charts/analytics.

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