From e2271664731c1fea06ae495a399d67cc174da672 Mon Sep 17 00:00:00 2001 From: eeshsaxena <139802361+eeshsaxena@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:18:58 +0530 Subject: [PATCH 1/3] fix: not_regex should handle all value types like regex The not_regex operator used a manual string/int type switch and returned an error for any other type, most notably float64 (what JSON numbers deserialize to), so it failed on a numeric property value even though regex handled the same value. Coerce both the pattern and the property value with valueToString, mirroring the regex operator, and add a regression test. --- .changeset/fix-not-regex-value-types.md | 5 +++++ feature_flags_matching_test.go | 14 +++++++++++++ featureflags.go | 28 +++++++------------------ 3 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 .changeset/fix-not-regex-value-types.md diff --git a/.changeset/fix-not-regex-value-types.md b/.changeset/fix-not-regex-value-types.md new file mode 100644 index 0000000..cacc728 --- /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`. diff --git a/feature_flags_matching_test.go b/feature_flags_matching_test.go index b74892d..b3e9ebb 100644 --- a/feature_flags_matching_test.go +++ b/feature_flags_matching_test.go @@ -309,6 +309,20 @@ 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 +} + func TestMatchPropertyContains(t *testing.T) { shouldMatch := []interface{}{"value", "value2", "value3", "value4", "343tfvalue5"} diff --git a/featureflags.go b/featureflags.go index 6696399..eec7a8f 100644 --- a/featureflags.go +++ b/featureflags.go @@ -1215,32 +1215,18 @@ 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) + // 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(valueToString(override_value)), nil } if operator == "gt" { From ff1c79370e9cf20a0a26ed3ae68e42007f204334 Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Tue, 1 Sep 2026 09:40:05 +0530 Subject: [PATCH 2/3] fix(not_regex): treat nil property value as no-match instead of coercing to --- .changeset/fix-not-regex-value-types.md | 2 +- feature_flags_matching_test.go | 12 ++++++++++++ featureflags.go | 7 +++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.changeset/fix-not-regex-value-types.md b/.changeset/fix-not-regex-value-types.md index cacc728..b04200d 100644 --- a/.changeset/fix-not-regex-value-types.md +++ b/.changeset/fix-not-regex-value-types.md @@ -2,4 +2,4 @@ "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`. +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`. It also no longer coerces a `nil` property value to the Go-specific `` string; like the server, a null property now yields no-match for `not_regex`. diff --git a/feature_flags_matching_test.go b/feature_flags_matching_test.go index b3e9ebb..f24e747 100644 --- a/feature_flags_matching_test.go +++ b/feature_flags_matching_test.go @@ -321,6 +321,18 @@ func TestMatchPropertyNotRegexHandlesAllValueTypes(t *testing.T) { 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 + + // A nil property value must not be coerced to Go's "" string. Like the + // server (posthog-python only allows None for "is_not"), a regex operator + // yields no-match for a null property. + m, err = matchProperty(FlagProperty{Key: "k", Value: "^1", Operator: "not_regex"}, NewProperties().Set("k", nil)) + require.NoError(t, err) + require.False(t, m) + // Regression guard: a pattern that would match the literal "" must not + // sneak through for a null property. + m, err = matchProperty(FlagProperty{Key: "k", Value: "^$", Operator: "not_regex"}, NewProperties().Set("k", nil)) + require.NoError(t, err) + require.False(t, m) } func TestMatchPropertyContains(t *testing.T) { diff --git a/featureflags.go b/featureflags.go index eec7a8f..a69d540 100644 --- a/featureflags.go +++ b/featureflags.go @@ -1215,6 +1215,13 @@ func matchProperty(property FlagProperty, properties Properties) (bool, error) { } if operator == "not_regex" { + // A nil property value never matches a regex operator. The server and + // posthog-python list only "is_not" in NONE_VALUES_ALLOWED_OPERATORS, so + // a null property yields no-match there rather than being coerced to the + // Go-specific "" string (which would spuriously satisfy not_regex). + if override_value == nil { + return false, nil + } // 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, From fa1d6b4028849c9b062532b2d1aad5dfafcaf2fa Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 2 Sep 2026 09:29:33 +0200 Subject: [PATCH 3/3] fix: preserve not_regex null semantics --- .changeset/fix-not-regex-value-types.md | 2 +- feature_flags_matching_test.go | 14 ++++++++------ featureflags.go | 11 +++++------ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.changeset/fix-not-regex-value-types.md b/.changeset/fix-not-regex-value-types.md index b04200d..6757a54 100644 --- a/.changeset/fix-not-regex-value-types.md +++ b/.changeset/fix-not-regex-value-types.md @@ -2,4 +2,4 @@ "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`. It also no longer coerces a `nil` property value to the Go-specific `` string; like the server, a null property now yields no-match for `not_regex`. +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 f24e747..90a7d6f 100644 --- a/feature_flags_matching_test.go +++ b/feature_flags_matching_test.go @@ -322,17 +322,19 @@ func TestMatchPropertyNotRegexHandlesAllValueTypes(t *testing.T) { require.NoError(t, err) require.True(t, m) // "123" does not match ^9 - // A nil property value must not be coerced to Go's "" string. Like the - // server (posthog-python only allows None for "is_not"), a regex operator - // yields no-match for a null property. + // 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: a pattern that would match the literal "" must not - // sneak through for a null property. + // 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.False(t, m) + require.True(t, m) } func TestMatchPropertyContains(t *testing.T) { diff --git a/featureflags.go b/featureflags.go index a69d540..66dfaaa 100644 --- a/featureflags.go +++ b/featureflags.go @@ -1215,12 +1215,11 @@ func matchProperty(property FlagProperty, properties Properties) (bool, error) { } if operator == "not_regex" { - // A nil property value never matches a regex operator. The server and - // posthog-python list only "is_not" in NONE_VALUES_ALLOWED_OPERATORS, so - // a null property yields no-match there rather than being coerced to the - // Go-specific "" string (which would spuriously satisfy not_regex). + // 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 { - return false, nil + overrideValueString = "null" } // Mirror the "regex" branch above: coerce both sides with valueToString // so all property/flag value types are handled. The previous manual @@ -1233,7 +1232,7 @@ func matchProperty(property FlagProperty, properties Properties) (bool, error) { return false, nil } - return !r.MatchString(valueToString(override_value)), nil + return !r.MatchString(overrideValueString), nil } if operator == "gt" {