diff --git a/.changeset/fix-not-regex-value-types.md b/.changeset/fix-not-regex-value-types.md new file mode 100644 index 0000000..6757a54 --- /dev/null +++ b/.changeset/fix-not-regex-value-types.md @@ -0,0 +1,5 @@ +--- +"posthog-go": patch +--- + +Fix `not_regex` local flag evaluation erroring on non-string/int property values. The `not_regex` operator used a manual string/int type switch and returned an error for other types, most notably `float64`, which is what JSON numbers deserialize to, so it failed on a numeric property value even though `regex` handled it. `not_regex` now coerces both sides with `valueToString`, mirroring `regex`. An explicit `nil` property value is represented as `null`, matching the feature flags evaluation service rather than Go's default `` representation. diff --git a/feature_flags_matching_test.go b/feature_flags_matching_test.go index b74892d..90a7d6f 100644 --- a/feature_flags_matching_test.go +++ b/feature_flags_matching_test.go @@ -309,6 +309,34 @@ func TestMatchPropertyRegex(t *testing.T) { } } +func TestMatchPropertyNotRegexHandlesAllValueTypes(t *testing.T) { + // not_regex must coerce like regex does. JSON numbers arrive as float64, + // which the old manual type switch rejected with an error even though + // regex handled it. + for _, ov := range []interface{}{"123", 123, 123.0, float64(123)} { + m, err := matchProperty(FlagProperty{Key: "k", Value: "^1", Operator: "not_regex"}, NewProperties().Set("k", ov)) + require.NoError(t, err) + require.False(t, m) // "123" matches ^1, so not_regex is false + } + m, err := matchProperty(FlagProperty{Key: "k", Value: "^9", Operator: "not_regex"}, NewProperties().Set("k", 123.0)) + require.NoError(t, err) + require.True(t, m) // "123" does not match ^9 + + // The feature flags evaluation service stringifies a null property as "null" + // before matching it. not_regex must negate the regex result as usual. + m, err = matchProperty(FlagProperty{Key: "k", Value: "^1", Operator: "not_regex"}, NewProperties().Set("k", nil)) + require.NoError(t, err) + require.True(t, m) + m, err = matchProperty(FlagProperty{Key: "k", Value: "^null$", Operator: "not_regex"}, NewProperties().Set("k", nil)) + require.NoError(t, err) + require.False(t, m) + // Regression guard: Go's default "" representation must not leak into + // local evaluation. + m, err = matchProperty(FlagProperty{Key: "k", Value: "^$", Operator: "not_regex"}, NewProperties().Set("k", nil)) + require.NoError(t, err) + require.True(t, m) +} + func TestMatchPropertyContains(t *testing.T) { shouldMatch := []interface{}{"value", "value2", "value3", "value4", "343tfvalue5"} diff --git a/featureflags.go b/featureflags.go index 6696399..66dfaaa 100644 --- a/featureflags.go +++ b/featureflags.go @@ -1215,32 +1215,24 @@ func matchProperty(property FlagProperty, properties Properties) (bool, error) { } if operator == "not_regex" { - var pattern string - - if valueString, ok := value.(string); ok { - pattern = valueString - } else if valueInt, ok := value.(int); ok { - pattern = strconv.Itoa(valueInt) - } else { - return false, errors.New("regex expression not allowed") - } - - r, err := getOrCompileRegex(pattern) + // The feature flags evaluation service stringifies an explicit JSON null + // as "null" before regex matching. Avoid Go's default "" representation. + overrideValueString := valueToString(override_value) + if override_value == nil { + overrideValueString = "null" + } + // Mirror the "regex" branch above: coerce both sides with valueToString + // so all property/flag value types are handled. The previous manual + // string/int type switch errored on other types, most notably float64, + // which is what JSON numbers deserialize to, so not_regex failed on a + // numeric property value even though regex handled it. + r, err := getOrCompileRegex(valueToString(value)) // invalid regex if err != nil { return false, nil } - var match bool - if valueString, ok := override_value.(string); ok { - match = r.MatchString(valueString) - } else if valueInt, ok := override_value.(int); ok { - match = r.MatchString(strconv.Itoa(valueInt)) - } else { - return false, errors.New("value type not supported") - } - - return !match, nil + return !r.MatchString(overrideValueString), nil } if operator == "gt" {