Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-not-regex-value-types.md
Original file line number Diff line number Diff line change
@@ -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 `<nil>` representation.
28 changes: 28 additions & 0 deletions feature_flags_matching_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<nil>" representation must not leak into
// local evaluation.
m, err = matchProperty(FlagProperty{Key: "k", Value: "^<nil>$", 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"}

Expand Down
34 changes: 13 additions & 21 deletions featureflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<nil>" 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))
Comment thread
marandaneto marked this conversation as resolved.
// 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" {
Expand Down