Files
insights-go/feature_flags_local_test.go
zeekayandHanzo Dev 8ee8950230 Go 1.27.1, and the code it can say more simply
The module declares go 1.27.1 and the adapter image follows. go fix
applied the modernizers and left ptrString and ptrBool with no callers,
so they are gone; omitzero is not applied, since it changes what the
JSON says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:52:31 -07:00

5273 lines
97 KiB
Go

package insights
import (
"bytes"
"fmt"
"log"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
json "github.com/goccy/go-json"
"github.com/stretchr/testify/require"
)
// Note: Property matching tests (TestMatchProperty*) have been moved to feature_flags_matching_test.go
func TestFlagPersonProperty(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-simple-flag-person-prop.json")))
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
isMatch, _ := client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "simple-flag",
DistinctId: "some-distinct-id",
PersonProperties: NewProperties().Set("region", "USA"),
},
)
if isMatch != true {
t.Error("Should match")
}
isMatch, _ = client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "simple-flag",
DistinctId: "some-distinct-id",
PersonProperties: NewProperties().Set("region", "Canada"),
},
)
if isMatch == true {
t.Error("Should not match")
}
}
func TestFlagGroup(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
var reqBody FlagsRequestData
err := decoder.Decode(&reqBody)
if err != nil {
t.Error(err)
}
groupsEquality := reflect.DeepEqual(reqBody.Groups, Groups{"company": "abc"})
if !groupsEquality {
t.Errorf("Expected groups to be map[company:abc], got %s", reqBody.Groups)
}
distinctIdEquality := reflect.DeepEqual(reqBody.DistinctId, "-")
if !distinctIdEquality {
t.Errorf("Expected distinctId to be -, got %s", reqBody.DistinctId)
}
apiKeyEquality := reflect.DeepEqual(reqBody.ApiKey, "Csyjlnlun3OzyNJAafdlv")
if !apiKeyEquality {
t.Errorf("Expected apiKey to be Csyjlnlun3OzyNJAafdlv, got %s", reqBody.ApiKey)
}
personPropertiesEquality := reflect.DeepEqual(reqBody.PersonProperties, Properties{"region": "Canada"})
if !personPropertiesEquality {
t.Errorf("Expected personProperties to be map[region:Canada], got %s", reqBody.PersonProperties)
}
groupPropertiesEquality := reflect.DeepEqual(reqBody.GroupProperties, map[string]Properties{"company": {"name": "Project Name 1"}})
if !groupPropertiesEquality {
t.Errorf("Expected groupProperties to be map[company:map[name:Project Name 1]], got %s", reqBody.GroupProperties)
}
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-flag-group-properties.json")))
} else if strings.HasPrefix(r.URL.Path, "/batch/") {
// Ignore batch requests
} else {
t.Error("Unknown request made by library")
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
isMatch, _ := client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "unknown-flag",
DistinctId: "-",
Groups: Groups{"company": "abc"},
PersonProperties: NewProperties().Set("region", "Canada"),
GroupProperties: map[string]Properties{"company": NewProperties().Set("name", "Project Name 1")},
OnlyEvaluateLocally: false,
},
)
if isMatch != false {
t.Error("Unknown flag shouldn't match known flags")
}
}
func TestFlagGroupProperty(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fixture("feature_flag/test-flag-group-properties.json")))
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
isMatch, _ := client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "group-flag",
DistinctId: "some-distinct-id",
GroupProperties: map[string]Properties{"company": NewProperties().Set("name", "Project Name 1")},
},
)
if isMatch == true {
t.Error("Should not match")
}
isMatch, _ = client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "group-flag",
DistinctId: "some-distinct-id",
GroupProperties: map[string]Properties{"company": NewProperties().Set("name", "Project Name 2")},
},
)
if isMatch == true {
t.Error("Should not match")
}
isMatch, _ = client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "group-flag",
DistinctId: "some-distinct-id",
Groups: Groups{"company": "amazon_without_rollout"},
GroupProperties: map[string]Properties{"company": NewProperties().Set("name", "Project Name 1")},
},
)
if isMatch != true {
t.Error("Should match")
}
}
func TestComplexDefinition(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-complex-definition.json"))) // Don't return anything for local eval
}
}))
defer server.Close()
client, err := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
require.NoError(t, err)
defer client.Close()
isMatch, err := client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "complex-flag",
DistinctId: "some-distinct-id",
PersonProperties: NewProperties().Set("region", "USA").Set("name", "Aloha"),
},
)
require.NoError(t, err)
require.Equal(t, true, isMatch)
isMatch, err = client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "complex-flag",
DistinctId: "some-distinct-id_within_rollout_3",
PersonProperties: NewProperties().Set("region", "USA").Set("email", "[email protected]"),
},
)
require.NoError(t, err)
require.Equal(t, true, isMatch)
}
func TestFallbackToFlags(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte("{}")) // Don't return anything for local eval
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
isMatch, _ := client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "simple-flag",
DistinctId: "some-distinct-id",
},
)
if isMatch != true {
t.Error("Should match")
}
}
func TestFeatureFlagsDontFallbackToFlagsWhenOnlyLocalEvaluationIsTrue(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte("test-flags-v3.json"))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-feature-flags-dont-fallback-to-decide-when-only-local-evaluation-is-true.json")))
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
matchedPayload, _ := client.GetFeatureFlagPayload(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "some-distinct-id",
OnlyEvaluateLocally: true,
},
)
if matchedPayload != "" {
t.Error("Should not match")
}
matchedVariant, _ := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "some-distinct-id",
OnlyEvaluateLocally: true,
},
)
if matchedVariant != nil {
t.Error("Should not match")
}
isMatch, _ := client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "some-distinct-id",
OnlyEvaluateLocally: true,
},
)
if isMatch == true {
t.Error("Should not match")
}
matchedVariant, _ = client.GetFeatureFlag(
FeatureFlagPayload{
Key: "beta-feature2",
DistinctId: "some-distinct-id",
OnlyEvaluateLocally: true,
},
)
if matchedVariant != nil {
t.Error("Should not match")
}
isMatch, _ = client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "beta-feature2",
DistinctId: "some-distinct-id",
OnlyEvaluateLocally: true,
},
)
if isMatch == true {
t.Error("Should not match")
}
}
func TestFeatureFlagDefaultsDontHinderEvaluation(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-false.json")))
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
isMatch, _ := client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "false-flag",
DistinctId: "some-distinct-id",
},
)
if isMatch == true {
t.Error("Should not match")
}
isMatch, _ = client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "false-flag",
DistinctId: "some-distinct-id",
},
)
if isMatch == true {
t.Error("Should not match")
}
isMatch, _ = client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "false-flag-2",
DistinctId: "some-distinct-id",
},
)
if isMatch == true {
t.Error("Should not match")
}
isMatch, _ = client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "false-flag-2",
DistinctId: "some-distinct-id",
},
)
if isMatch == true {
t.Error("Should not match")
}
}
func TestFeatureFlagNullComeIntoPlayOnlyWhenFlagsErrorsOut(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("{ads}"))
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
matchedPayload, _ := client.GetFeatureFlagPayload(
FeatureFlagPayload{
Key: "test-get-feature",
DistinctId: "distinct_id",
},
)
if matchedPayload != "" {
t.Error("Should not match")
}
isMatch, _ := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "test-get-feature",
DistinctId: "distinct_id",
},
)
if isMatch != nil {
t.Error("Should be nil")
}
isMatch, _ = client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "test-get-feature",
DistinctId: "distinct_id",
},
)
if isMatch != nil {
t.Error("Should be nil")
}
}
func TestExperienceContinuityOverride(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-simple-flag.json")))
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
featureVariant, _ := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "distinct_id",
},
)
if featureVariant != "decide-fallback-value" {
t.Error("Should be decide-fallback-value")
}
payload, _ := client.GetFeatureFlagPayload(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "distinct_id",
},
)
if payload != "{\"foo\": \"bar\"}" {
t.Error(`Should be "{"foo": "bar"}"`)
}
}
func TestGetAllFlags(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-multiple-flags.json")))
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
featureVariants, _ := client.GetAllFlags(FeatureFlagPayloadNoKey{
DistinctId: "distinct-id",
})
if featureVariants["beta-feature"] != "decide-fallback-value" || featureVariants["beta-feature2"] != "variant-2" {
t.Error("Should match decide values")
}
}
func TestGetAllFlagsEmptyLocal(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte("{}"))
}
}))
defer server.Close()
client, err := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
require.NoError(t, err)
defer client.Close()
featureVariants, err := client.GetAllFlags(FeatureFlagPayloadNoKey{
DistinctId: "distinct-id",
})
require.NoError(t, err)
if featureVariants["beta-feature"] != "decide-fallback-value" || featureVariants["beta-feature2"] != "variant-2" {
t.Error("Should match decide values")
}
}
func TestGetAllFlagsNoRemoteFallback(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-multiple-flags-valid.json")))
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
featureVariants, _ := client.GetAllFlags(FeatureFlagPayloadNoKey{
DistinctId: "distinct-id",
})
if featureVariants["beta-feature"] != true || featureVariants["disabled-feature"] != false {
t.Error("Should match")
}
}
func TestGetAllFlagsOnlyLocalEvaluationSet(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-get-all-flags-with-fallback-but-only-local-evaluation-set.json")))
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
featureVariants, _ := client.GetAllFlags(FeatureFlagPayloadNoKey{
DistinctId: "distinct-id",
OnlyEvaluateLocally: true,
})
if featureVariants["beta-feature"] != true || featureVariants["disabled-feature"] != false || featureVariants["beta-feature2"] != nil {
t.Error("Should match")
}
}
func TestComputeInactiveFlagsLocally(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-compute-inactive-flags-locally.json")))
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
featureVariants, _ := client.GetAllFlags(FeatureFlagPayloadNoKey{
DistinctId: "distinct-id",
})
if featureVariants["beta-feature"] != true || featureVariants["disabled-feature"] != false {
t.Error("Should match")
}
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-compute-inactive-flags-locally-2.json")))
}
}))
defer server.Close()
client, _ = NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
featureVariants, _ = client.GetAllFlags(FeatureFlagPayloadNoKey{
DistinctId: "distinct-id",
})
if featureVariants["beta-feature"] != false || featureVariants["disabled-feature"] != true {
t.Error("Should match")
}
}
func TestFeatureFlagWithDependencies(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-flag-with-dependencies.json")))
}
}))
defer server.Close()
client, _ := NewWithConfig("test-api-key", Config{
PersonalApiKey: "test-personal-api-key",
Endpoint: server.URL,
})
defer client.Close()
// Test that flag evaluation handles flag dependencies properly
// The flag has a dependency on "beta-feature" which doesn't exist locally
// Since OnlyEvaluateLocally is true and the dependency can't be resolved locally,
// this should return an error
_, err := client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "flag-with-dependencies",
DistinctId: "test-user",
PersonProperties: NewProperties().Set("email", "[email protected]"),
OnlyEvaluateLocally: true,
},
)
// Should return an error since dependency can't be evaluated locally
if err == nil {
t.Error("Should return error when flag dependencies can't be evaluated locally")
}
// Test with email that doesn't match - should also error due to missing dependency
_, err = client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "flag-with-dependencies",
DistinctId: "test-user-2",
PersonProperties: NewProperties().Set("email", "[email protected]"),
OnlyEvaluateLocally: true,
},
)
if err == nil {
t.Error("Should return error when flag dependencies can't be evaluated locally")
}
}
func TestFeatureEnabledSimpleIsTrueWhenRolloutUndefined(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fixture("feature_flag/test-simple-flag-without-rollout.json")))
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
isMatch, _ := client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "simple-flag",
DistinctId: "distinct-id",
},
)
if isMatch != true {
t.Error("Should be enabled")
}
}
func TestGetFeatureFlag(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-simple-flag-person-prop.json")))
}
}))
defer server.Close()
var capturedEvent *CaptureInApi
var mu sync.Mutex
eventCaptured := make(chan struct{}, 1)
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
BatchSize: 1,
Callback: testCallback{
func(m APIMessage) {
if capture, ok := m.(CaptureInApi); ok {
mu.Lock()
capturedEvent = &capture
mu.Unlock()
select {
case eventCaptured <- struct{}{}:
default:
}
}
},
func(m APIMessage, e error) {},
},
})
defer client.Close()
variant, _ := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "test-get-feature",
DistinctId: "distinct_id",
},
)
if variant != "variant-1" {
t.Error("Should match")
}
select {
case <-eventCaptured:
case <-time.After(time.Second):
t.Fatal("Timed out waiting for captured event")
}
mu.Lock()
lastEvent := capturedEvent
mu.Unlock()
if lastEvent == nil || lastEvent.Event != "$feature_flag_called" {
t.Errorf("Expected a $feature_flag_called event, got: %v", lastEvent)
}
if lastEvent != nil {
if lastEvent.Properties["$feature_flag"] != "test-get-feature" {
t.Errorf("Expected feature flag key 'test-get-feature', got: %v", lastEvent.Properties["$feature_flag"])
}
if lastEvent.Properties["$feature_flag_response"] != "variant-1" {
t.Errorf("Expected feature flag response 'variant-1', got: %v", lastEvent.Properties["$feature_flag_response"])
}
if lastEvent.Properties["locally_evaluated"] != false {
t.Errorf("Expected locally_evaluated to be false (test-get-feature is only in /flags response), got: %v", lastEvent.Properties["locally_evaluated"])
}
}
}
func TestGetFeatureFlagLocallyEvaluated(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-simple-flag-person-prop.json")))
}
}))
defer server.Close()
var capturedEvent *CaptureInApi
var mu sync.Mutex
eventCaptured := make(chan struct{}, 1)
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
BatchSize: 1,
Callback: testCallback{
func(m APIMessage) {
if capture, ok := m.(CaptureInApi); ok {
mu.Lock()
capturedEvent = &capture
mu.Unlock()
select {
case eventCaptured <- struct{}{}:
default:
}
}
},
func(m APIMessage, e error) {},
},
})
defer client.Close()
value, err := client.GetFeatureFlag(FeatureFlagPayload{
Key: "simple-flag",
DistinctId: "distinct_id",
PersonProperties: NewProperties().Set("region", "USA"),
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if value != true {
t.Errorf("Expected simple-flag to evaluate to true, got: %v", value)
}
select {
case <-eventCaptured:
case <-time.After(time.Second):
t.Fatal("Timed out waiting for captured event")
}
mu.Lock()
lastEvent := capturedEvent
mu.Unlock()
if lastEvent == nil || lastEvent.Event != "$feature_flag_called" {
t.Fatalf("Expected a $feature_flag_called event, got: %v", lastEvent)
}
if lastEvent.Properties["locally_evaluated"] != true {
t.Errorf("Expected locally_evaluated to be true for local evaluation, got: %v", lastEvent.Properties["locally_evaluated"])
}
}
func TestGetFeatureFlagPayload(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-simple-flag-person-prop.json")))
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
variant, _ := client.GetFeatureFlagPayload(
FeatureFlagPayload{
Key: "test-get-feature",
DistinctId: "distinct_id",
},
)
if variant != "this is a string" {
t.Error("Should match")
}
}
func TestGetRemoteConfigPayload(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fixture("test-remote-config.json")))
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
payload, _ := client.GetRemoteConfigPayload("flag_key")
var payloadMap map[string]any
err := json.Unmarshal([]byte(payload), &payloadMap)
if err != nil {
t.Error("Failed to decode payload")
}
if payloadMap["foo"] != "bar" || payloadMap["baz"] != float64(42) {
t.Error("Should match")
}
}
func TestFlagWithVariantOverrides(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-variant-override.json")))
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
variant, _ := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "test_id",
PersonProperties: NewProperties().Set("email", "[email protected]"),
},
)
if variant != "second-variant" {
t.Error("Should match", variant, "second-variant")
}
payload, _ := client.GetFeatureFlagPayload(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "test_id",
PersonProperties: NewProperties().Set("email", "[email protected]"),
},
)
if payload != "{\"test\": 2}" {
t.Error("Should match", payload, "{\"test\": 2}")
}
variant, _ = client.GetFeatureFlag(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "example_id",
},
)
if variant != "first-variant" {
t.Error("Should match", variant, "first-variant")
}
payload, _ = client.GetFeatureFlagPayload(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "example_id",
},
)
if payload != "{\"test\": 1}" {
t.Error("Should match", payload, "{\"test\": 1}")
}
}
func TestFlagWithClashingVariantOverrides(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-variant-override-clashing.json")))
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
variant, _ := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "test_id",
PersonProperties: NewProperties().Set("email", "[email protected]"),
},
)
if variant != "second-variant" {
t.Error("Should match", variant, "second-variant")
}
payload, _ := client.GetFeatureFlagPayload(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "test_id",
PersonProperties: NewProperties().Set("email", "[email protected]"),
},
)
if payload != "{\"test\": 2}" {
t.Error("Should match", payload, "{\"test\": 2}")
}
variant, _ = client.GetFeatureFlag(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "example_id",
PersonProperties: NewProperties().Set("email", "[email protected]"),
},
)
if variant != "second-variant" {
t.Error("Should match", variant, "second-variant")
}
payload, _ = client.GetFeatureFlagPayload(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "example_id",
PersonProperties: NewProperties().Set("email", "[email protected]"),
},
)
if payload != "{\"test\": 2}" {
t.Error("Should match", payload, "{\"test\": 2}")
}
}
func TestFlagWithInvalidVariantOverrides(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-variant-override-invalid.json")))
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
variant, _ := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "test_id",
PersonProperties: NewProperties().Set("email", "[email protected]"),
},
)
if variant != "third-variant" {
t.Error("Should match", variant, "third-variant")
}
payload, _ := client.GetFeatureFlagPayload(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "test_id",
PersonProperties: NewProperties().Set("email", "[email protected]"),
},
)
if payload != "{\"test\": 3}" {
t.Error("Should match", payload, "{\"test\": 3}")
}
variant, _ = client.GetFeatureFlag(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "example_id",
},
)
if variant != "second-variant" {
t.Error("Should match", variant, "second-variant")
}
payload, _ = client.GetFeatureFlagPayload(
FeatureFlagPayload{
Key: "beta-feature",
DistinctId: "example_id",
},
)
if payload != "{\"test\": 2}" {
t.Error("Should match", payload, "{\"test\": 2}")
}
}
func TestConditionsEvaluatedInOrder(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-condition-order.json")))
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
// Test 1: User with @vip.com email should get "first-condition" variant
// because the first condition (100% rollout) matches before the VIP condition
variant, _ := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "order-test-flag",
DistinctId: "vip-user",
PersonProperties: NewProperties().Set("email", "[email protected]"),
},
)
if variant != "first-condition" {
t.Error("Expected first-condition variant for VIP user, but got", variant)
}
payload, _ := client.GetFeatureFlagPayload(
FeatureFlagPayload{
Key: "order-test-flag",
DistinctId: "vip-user",
PersonProperties: NewProperties().Set("email", "[email protected]"),
},
)
if payload != "{\"order\": 1}" {
t.Error("Expected payload {\"order\": 1} for VIP user, but got", payload)
}
// Test 2: User with [email protected] should also get "first-condition" variant
// because the first condition matches before the specific email condition
variant, _ = client.GetFeatureFlag(
FeatureFlagPayload{
Key: "order-test-flag",
DistinctId: "test-user",
PersonProperties: NewProperties().Set("email", "[email protected]"),
},
)
if variant != "first-condition" {
t.Error("Expected first-condition variant for [email protected] user, but got", variant)
}
// Test 3: Any other user should also get "first-condition" variant
variant, _ = client.GetFeatureFlag(
FeatureFlagPayload{
Key: "order-test-flag",
DistinctId: "random-user",
PersonProperties: NewProperties().Set("email", "[email protected]"),
},
)
if variant != "first-condition" {
t.Error("Expected first-condition variant for random user, but got", variant)
}
}
func TestCaptureIsCalled(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-simple-flag-person-prop.json")))
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
variant, _ := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "test-get-feature",
DistinctId: "distinct_id",
},
)
if variant != "variant-1" {
t.Error("Should match")
}
}
func TestSimpleFlagConsistency(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fixture("feature_flag/test-simple-flag.json")))
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
results := []bool{
false,
true,
true,
false,
true,
false,
false,
true,
false,
true,
false,
true,
true,
false,
true,
false,
false,
false,
true,
true,
false,
true,
false,
false,
true,
false,
true,
true,
false,
false,
false,
true,
true,
true,
true,
false,
false,
false,
false,
false,
false,
true,
true,
false,
true,
true,
false,
false,
false,
true,
true,
false,
false,
false,
false,
true,
false,
true,
false,
true,
false,
true,
true,
false,
true,
false,
true,
false,
true,
true,
false,
false,
true,
false,
false,
true,
false,
true,
false,
false,
true,
false,
false,
false,
true,
true,
false,
true,
true,
false,
true,
true,
true,
true,
true,
false,
true,
true,
false,
false,
true,
true,
true,
true,
false,
false,
true,
false,
true,
true,
true,
false,
false,
false,
false,
false,
true,
false,
false,
true,
true,
true,
false,
false,
true,
false,
true,
false,
false,
true,
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
false,
false,
true,
false,
false,
true,
true,
false,
false,
true,
false,
true,
false,
true,
true,
true,
false,
false,
false,
true,
false,
false,
false,
false,
true,
true,
false,
true,
true,
false,
true,
false,
true,
true,
false,
true,
false,
true,
true,
true,
false,
true,
false,
false,
true,
true,
false,
true,
false,
true,
true,
false,
false,
true,
true,
true,
true,
false,
true,
true,
false,
false,
true,
false,
true,
false,
false,
true,
true,
false,
true,
false,
true,
false,
false,
false,
false,
false,
false,
false,
true,
false,
true,
true,
false,
false,
true,
false,
true,
false,
false,
false,
true,
false,
true,
false,
false,
false,
true,
false,
false,
true,
false,
true,
true,
false,
false,
false,
false,
true,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
false,
true,
false,
true,
true,
false,
true,
false,
true,
false,
false,
false,
true,
true,
true,
true,
false,
false,
false,
false,
false,
true,
true,
true,
false,
false,
true,
true,
false,
false,
false,
false,
false,
true,
false,
true,
true,
true,
true,
false,
true,
true,
true,
false,
false,
true,
false,
true,
false,
false,
true,
true,
true,
false,
true,
false,
false,
false,
true,
true,
false,
true,
false,
true,
false,
true,
true,
true,
true,
true,
false,
false,
true,
false,
true,
false,
true,
true,
true,
false,
true,
false,
true,
true,
false,
true,
true,
true,
true,
true,
false,
false,
false,
false,
false,
true,
false,
true,
false,
false,
true,
true,
false,
false,
false,
true,
false,
true,
true,
true,
true,
false,
false,
false,
false,
true,
true,
false,
false,
true,
true,
false,
true,
true,
true,
true,
false,
true,
true,
true,
false,
false,
true,
true,
false,
false,
true,
false,
false,
true,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
false,
false,
true,
false,
false,
true,
false,
true,
false,
false,
true,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
true,
false,
false,
false,
true,
false,
true,
false,
false,
false,
true,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
true,
false,
true,
true,
true,
false,
false,
false,
true,
true,
true,
false,
true,
false,
true,
true,
false,
false,
false,
true,
false,
false,
false,
false,
true,
false,
true,
false,
true,
true,
false,
true,
false,
false,
false,
true,
false,
false,
true,
true,
false,
true,
false,
false,
false,
false,
false,
false,
true,
true,
false,
false,
true,
false,
false,
true,
true,
true,
false,
false,
false,
true,
false,
false,
false,
false,
true,
false,
true,
false,
false,
false,
true,
false,
true,
true,
false,
true,
false,
true,
false,
true,
false,
false,
true,
false,
false,
true,
false,
true,
false,
true,
false,
true,
false,
false,
true,
true,
true,
true,
false,
true,
false,
false,
false,
false,
false,
true,
false,
false,
true,
false,
false,
true,
true,
false,
false,
false,
false,
true,
true,
true,
false,
false,
true,
false,
false,
true,
true,
true,
true,
false,
false,
false,
true,
false,
false,
false,
true,
false,
false,
true,
true,
true,
true,
false,
false,
true,
true,
false,
true,
false,
true,
false,
false,
true,
true,
false,
true,
true,
true,
true,
false,
false,
true,
false,
false,
true,
true,
false,
true,
false,
true,
false,
false,
true,
false,
false,
false,
false,
true,
true,
true,
false,
true,
false,
false,
true,
false,
false,
true,
false,
false,
false,
false,
true,
false,
true,
false,
true,
true,
false,
false,
true,
false,
true,
true,
true,
false,
false,
false,
false,
true,
true,
false,
true,
false,
false,
false,
true,
false,
false,
false,
false,
true,
true,
true,
false,
false,
false,
true,
true,
true,
true,
false,
true,
true,
false,
true,
true,
true,
false,
true,
false,
false,
true,
false,
true,
true,
true,
true,
false,
true,
false,
true,
false,
true,
false,
false,
true,
true,
false,
false,
true,
false,
true,
false,
false,
false,
false,
true,
false,
true,
false,
false,
false,
true,
true,
true,
false,
false,
false,
true,
false,
true,
true,
false,
false,
false,
false,
false,
true,
false,
true,
false,
false,
true,
true,
false,
true,
true,
true,
true,
false,
false,
true,
false,
false,
true,
false,
true,
false,
true,
true,
false,
false,
false,
true,
false,
true,
true,
false,
false,
false,
true,
false,
true,
false,
true,
true,
false,
true,
false,
false,
true,
false,
false,
false,
true,
true,
true,
false,
false,
false,
false,
false,
true,
false,
false,
true,
true,
true,
true,
true,
false,
false,
false,
false,
false,
false,
false,
false,
true,
true,
true,
false,
false,
true,
true,
false,
true,
true,
false,
true,
false,
true,
false,
false,
false,
true,
false,
false,
true,
false,
false,
true,
true,
true,
true,
false,
false,
true,
false,
true,
true,
false,
false,
true,
false,
false,
true,
true,
false,
true,
false,
false,
true,
true,
true,
false,
false,
false,
false,
false,
true,
false,
true,
false,
false,
false,
false,
false,
true,
true,
false,
true,
true,
true,
false,
false,
false,
false,
true,
true,
true,
true,
false,
true,
true,
false,
true,
false,
true,
false,
true,
false,
false,
false,
false,
true,
true,
true,
true,
false,
false,
true,
false,
true,
true,
false,
false,
false,
false,
false,
false,
true,
false,
true,
false,
true,
true,
false,
false,
true,
true,
true,
true,
false,
false,
true,
false,
true,
true,
false,
false,
true,
true,
true,
false,
true,
false,
false,
true,
true,
false,
false,
false,
true,
false,
false,
true,
false,
false,
false,
true,
true,
true,
true,
false,
true,
false,
true,
false,
true,
false,
true,
false,
false,
true,
false,
false,
true,
false,
true,
true,
}
for i := range 1000 {
isMatch, _ := client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "simple-flag",
DistinctId: fmt.Sprintf("%s%d", "distinct_id_", i),
},
)
if results[i] != isMatch {
t.Error("Match result is not consistent")
}
}
}
func TestMultivariateFlagConsistency(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fixture("feature_flag/test-multivariate-flag.json")))
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
results := []any{
"second-variant",
"second-variant",
"first-variant",
false,
false,
"second-variant",
"first-variant",
false,
false,
false,
"first-variant",
"third-variant",
false,
"first-variant",
"second-variant",
"first-variant",
false,
false,
"fourth-variant",
"first-variant",
false,
"third-variant",
false,
false,
false,
"first-variant",
"first-variant",
"first-variant",
"first-variant",
"first-variant",
"first-variant",
"third-variant",
false,
"third-variant",
"second-variant",
"first-variant",
false,
"third-variant",
false,
false,
"first-variant",
"second-variant",
false,
"first-variant",
"first-variant",
"second-variant",
false,
"first-variant",
false,
false,
"first-variant",
"first-variant",
"first-variant",
"second-variant",
"first-variant",
false,
"second-variant",
"second-variant",
"third-variant",
"second-variant",
"first-variant",
false,
"first-variant",
"second-variant",
"fourth-variant",
false,
"first-variant",
"first-variant",
"first-variant",
false,
"first-variant",
"second-variant",
false,
"third-variant",
false,
false,
false,
false,
false,
false,
"first-variant",
"fifth-variant",
false,
"second-variant",
"first-variant",
"second-variant",
false,
"third-variant",
"third-variant",
false,
false,
false,
false,
"third-variant",
false,
false,
"first-variant",
"first-variant",
false,
"third-variant",
"third-variant",
false,
"third-variant",
"second-variant",
"third-variant",
false,
false,
"second-variant",
"first-variant",
false,
false,
"first-variant",
false,
false,
false,
false,
"first-variant",
"first-variant",
"first-variant",
false,
false,
false,
"first-variant",
"first-variant",
false,
"first-variant",
"first-variant",
false,
false,
false,
false,
false,
false,
false,
false,
false,
"first-variant",
"first-variant",
"first-variant",
"first-variant",
"second-variant",
"first-variant",
"first-variant",
"first-variant",
"second-variant",
false,
"second-variant",
"first-variant",
"second-variant",
"first-variant",
false,
"second-variant",
"second-variant",
false,
"first-variant",
false,
false,
false,
"third-variant",
"first-variant",
false,
false,
"first-variant",
false,
false,
false,
false,
"first-variant",
false,
false,
false,
false,
false,
false,
false,
"first-variant",
"first-variant",
"third-variant",
"first-variant",
"first-variant",
false,
false,
"first-variant",
false,
false,
"fifth-variant",
"second-variant",
false,
"second-variant",
false,
"first-variant",
"third-variant",
"first-variant",
"fifth-variant",
"third-variant",
false,
false,
"fourth-variant",
false,
false,
false,
false,
"third-variant",
false,
false,
"third-variant",
false,
"first-variant",
"second-variant",
"second-variant",
"second-variant",
false,
"first-variant",
"third-variant",
"first-variant",
"first-variant",
false,
false,
false,
false,
false,
"first-variant",
"first-variant",
"first-variant",
"second-variant",
false,
false,
false,
"second-variant",
false,
false,
"first-variant",
false,
"first-variant",
false,
false,
"first-variant",
"first-variant",
"first-variant",
"first-variant",
"third-variant",
"first-variant",
"third-variant",
"first-variant",
"first-variant",
"second-variant",
"third-variant",
"third-variant",
false,
"second-variant",
"first-variant",
false,
"second-variant",
"first-variant",
false,
"first-variant",
false,
false,
"first-variant",
"fifth-variant",
"first-variant",
false,
false,
false,
false,
"first-variant",
"first-variant",
"second-variant",
false,
"second-variant",
"third-variant",
"third-variant",
false,
"first-variant",
"third-variant",
false,
false,
"first-variant",
false,
"third-variant",
"first-variant",
false,
"third-variant",
"first-variant",
"first-variant",
false,
"first-variant",
"second-variant",
"second-variant",
"first-variant",
false,
false,
false,
"second-variant",
false,
false,
"first-variant",
"first-variant",
false,
"third-variant",
false,
"first-variant",
false,
"third-variant",
false,
"third-variant",
"second-variant",
"first-variant",
false,
false,
"first-variant",
"third-variant",
"first-variant",
"second-variant",
"fifth-variant",
false,
false,
"first-variant",
false,
false,
false,
"third-variant",
false,
"second-variant",
"first-variant",
false,
false,
false,
false,
"third-variant",
false,
false,
"third-variant",
false,
false,
"first-variant",
"third-variant",
false,
false,
"first-variant",
false,
false,
"fourth-variant",
"fourth-variant",
"third-variant",
"second-variant",
"first-variant",
"third-variant",
"fifth-variant",
false,
"first-variant",
"fifth-variant",
false,
"first-variant",
"first-variant",
"first-variant",
false,
false,
false,
"second-variant",
"fifth-variant",
"second-variant",
"first-variant",
"first-variant",
"second-variant",
false,
false,
"third-variant",
false,
"second-variant",
"fifth-variant",
false,
"third-variant",
"first-variant",
false,
false,
"fourth-variant",
false,
false,
"second-variant",
false,
false,
"first-variant",
"fourth-variant",
"first-variant",
"second-variant",
false,
false,
false,
"first-variant",
"third-variant",
"third-variant",
false,
"first-variant",
"first-variant",
"first-variant",
false,
"first-variant",
false,
"first-variant",
"third-variant",
"third-variant",
false,
false,
"first-variant",
false,
false,
"second-variant",
"second-variant",
"first-variant",
"first-variant",
"first-variant",
false,
"fifth-variant",
"first-variant",
false,
false,
false,
"second-variant",
"third-variant",
"first-variant",
"fourth-variant",
"first-variant",
"third-variant",
false,
"first-variant",
"first-variant",
false,
"third-variant",
"first-variant",
"first-variant",
"third-variant",
false,
"fourth-variant",
"fifth-variant",
"first-variant",
"first-variant",
false,
false,
false,
"first-variant",
"first-variant",
"first-variant",
false,
"first-variant",
"first-variant",
"second-variant",
"first-variant",
false,
"first-variant",
"second-variant",
"first-variant",
false,
"first-variant",
"second-variant",
false,
"first-variant",
"first-variant",
false,
"first-variant",
false,
"first-variant",
false,
"first-variant",
false,
false,
false,
"third-variant",
"third-variant",
"first-variant",
false,
false,
"second-variant",
"third-variant",
"first-variant",
"first-variant",
false,
false,
false,
"second-variant",
"first-variant",
false,
"first-variant",
"third-variant",
false,
"first-variant",
false,
false,
false,
"first-variant",
"third-variant",
"third-variant",
false,
false,
false,
false,
"third-variant",
"fourth-variant",
"fourth-variant",
"first-variant",
"second-variant",
false,
"first-variant",
false,
"second-variant",
"first-variant",
"third-variant",
false,
"third-variant",
false,
"first-variant",
"first-variant",
"third-variant",
false,
false,
false,
"fourth-variant",
"second-variant",
"first-variant",
false,
false,
"first-variant",
"fourth-variant",
false,
"first-variant",
"third-variant",
"first-variant",
false,
false,
"third-variant",
false,
"first-variant",
false,
"first-variant",
"first-variant",
"third-variant",
"second-variant",
"fourth-variant",
false,
"first-variant",
false,
false,
false,
false,
"second-variant",
"first-variant",
"second-variant",
false,
"first-variant",
false,
"first-variant",
"first-variant",
false,
"first-variant",
"first-variant",
"second-variant",
"third-variant",
"first-variant",
"first-variant",
"first-variant",
false,
false,
false,
"third-variant",
false,
"first-variant",
"first-variant",
"first-variant",
"third-variant",
"first-variant",
"first-variant",
"second-variant",
"first-variant",
"fifth-variant",
"fourth-variant",
"first-variant",
"second-variant",
false,
"fourth-variant",
false,
false,
false,
"fourth-variant",
false,
false,
"third-variant",
false,
false,
false,
"first-variant",
"third-variant",
"third-variant",
"second-variant",
"first-variant",
"second-variant",
"first-variant",
false,
"first-variant",
false,
false,
false,
false,
false,
"first-variant",
"first-variant",
false,
"second-variant",
false,
false,
"first-variant",
false,
"second-variant",
"first-variant",
"first-variant",
"first-variant",
"third-variant",
"second-variant",
false,
false,
"fifth-variant",
"third-variant",
false,
false,
"first-variant",
false,
false,
false,
"first-variant",
"second-variant",
"third-variant",
"third-variant",
false,
false,
"first-variant",
false,
"third-variant",
"first-variant",
false,
false,
false,
false,
"fourth-variant",
"first-variant",
false,
false,
false,
"third-variant",
false,
false,
"second-variant",
"first-variant",
false,
false,
"second-variant",
"third-variant",
"first-variant",
"first-variant",
false,
"first-variant",
"first-variant",
false,
false,
"second-variant",
"third-variant",
"second-variant",
"third-variant",
false,
false,
"first-variant",
false,
false,
"first-variant",
false,
"second-variant",
false,
false,
false,
false,
"first-variant",
false,
"third-variant",
false,
"first-variant",
false,
false,
"second-variant",
"third-variant",
"second-variant",
"fourth-variant",
"first-variant",
"first-variant",
"first-variant",
false,
"first-variant",
false,
"second-variant",
false,
false,
false,
false,
false,
"first-variant",
false,
false,
false,
false,
false,
"first-variant",
false,
"second-variant",
false,
false,
false,
false,
"second-variant",
false,
"first-variant",
false,
"third-variant",
false,
false,
"first-variant",
"third-variant",
false,
"third-variant",
false,
false,
"second-variant",
false,
"first-variant",
"second-variant",
"first-variant",
false,
false,
false,
false,
false,
"second-variant",
false,
false,
"first-variant",
"third-variant",
false,
"first-variant",
false,
false,
false,
false,
false,
"first-variant",
"second-variant",
false,
false,
false,
"first-variant",
"first-variant",
"fifth-variant",
false,
false,
false,
"first-variant",
false,
"third-variant",
false,
false,
"second-variant",
false,
false,
false,
false,
false,
"fourth-variant",
"second-variant",
"first-variant",
"second-variant",
false,
"second-variant",
false,
"second-variant",
false,
"first-variant",
false,
"first-variant",
"first-variant",
false,
"second-variant",
false,
"first-variant",
false,
"fifth-variant",
false,
"first-variant",
"first-variant",
false,
false,
false,
"first-variant",
false,
"first-variant",
"third-variant",
false,
false,
"first-variant",
"first-variant",
false,
false,
"fifth-variant",
false,
false,
"third-variant",
false,
"third-variant",
"first-variant",
"first-variant",
"third-variant",
"third-variant",
false,
"first-variant",
false,
false,
false,
false,
false,
"first-variant",
false,
false,
false,
false,
"second-variant",
"first-variant",
"second-variant",
"first-variant",
false,
"fifth-variant",
"first-variant",
false,
false,
"fourth-variant",
"first-variant",
"first-variant",
false,
false,
"fourth-variant",
"first-variant",
false,
"second-variant",
"third-variant",
"third-variant",
"first-variant",
"first-variant",
false,
false,
false,
"first-variant",
"first-variant",
"first-variant",
false,
"third-variant",
"third-variant",
"third-variant",
false,
false,
"first-variant",
"first-variant",
false,
"second-variant",
false,
false,
"second-variant",
false,
"third-variant",
"first-variant",
"second-variant",
"fifth-variant",
"first-variant",
"first-variant",
false,
"first-variant",
"fifth-variant",
false,
false,
false,
"third-variant",
"first-variant",
"first-variant",
"second-variant",
"fourth-variant",
"first-variant",
"second-variant",
"first-variant",
false,
false,
false,
"second-variant",
"third-variant",
false,
false,
"first-variant",
false,
false,
false,
false,
false,
false,
"first-variant",
"first-variant",
false,
"third-variant",
false,
"first-variant",
false,
"third-variant",
"third-variant",
"first-variant",
"first-variant",
false,
"second-variant",
false,
"second-variant",
"first-variant",
false,
false,
false,
"second-variant",
false,
"third-variant",
false,
"first-variant",
"fifth-variant",
"first-variant",
"first-variant",
false,
false,
"first-variant",
false,
false,
false,
"first-variant",
"fourth-variant",
"first-variant",
"first-variant",
"first-variant",
"fifth-variant",
false,
false,
false,
"second-variant",
false,
false,
false,
"first-variant",
"first-variant",
false,
false,
"first-variant",
"first-variant",
"second-variant",
"first-variant",
"first-variant",
"first-variant",
"first-variant",
"first-variant",
"third-variant",
"first-variant",
false,
"second-variant",
false,
false,
"third-variant",
"second-variant",
"third-variant",
false,
"first-variant",
"third-variant",
"second-variant",
"first-variant",
"third-variant",
false,
false,
"first-variant",
"first-variant",
false,
false,
false,
"first-variant",
"third-variant",
"second-variant",
"first-variant",
"first-variant",
"first-variant",
false,
"third-variant",
"second-variant",
"third-variant",
false,
false,
"third-variant",
"first-variant",
false,
"first-variant",
}
for i := range 1000 {
variant, _ := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "multivariate-flag",
DistinctId: fmt.Sprintf("%s%d", "distinct_id_", i),
},
)
if results[i] != variant {
t.Error("Match result is not consistent")
}
}
}
func TestMultivariateFlagConsistencyPayload(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fixture("feature_flag/test-multivariate-flag.json")))
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
results := []string{
"{\"test\": 2}",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"",
"{\"test\": 4}",
"{\"test\": 1}",
"",
"{\"test\": 3}",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 3}",
"",
"{\"test\": 3}",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"{\"test\": 3}",
"",
"",
"{\"test\": 1}",
"{\"test\": 2}",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 2}",
"",
"{\"test\": 1}",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"{\"test\": 2}",
"{\"test\": 2}",
"{\"test\": 3}",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 4}",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"{\"test\": 2}",
"",
"{\"test\": 3}",
"",
"",
"",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 5}",
"",
"{\"test\": 2}",
"{\"test\": 1}",
"{\"test\": 2}",
"",
"{\"test\": 3}",
"{\"test\": 3}",
"",
"",
"",
"",
"{\"test\": 3}",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 3}",
"{\"test\": 3}",
"",
"{\"test\": 3}",
"{\"test\": 2}",
"{\"test\": 3}",
"",
"",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"",
"{\"test\": 1}",
"",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 2}",
"",
"{\"test\": 2}",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"{\"test\": 2}",
"{\"test\": 2}",
"",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 3}",
"{\"test\": 1}",
"",
"",
"{\"test\": 1}",
"",
"",
"",
"",
"{\"test\": 1}",
"",
"",
"",
"",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"",
"{\"test\": 1}",
"",
"",
"{\"test\": 5}",
"{\"test\": 2}",
"",
"{\"test\": 2}",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 5}",
"{\"test\": 3}",
"",
"",
"{\"test\": 4}",
"",
"",
"",
"",
"{\"test\": 3}",
"",
"",
"{\"test\": 3}",
"",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 2}",
"{\"test\": 2}",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 2}",
"",
"",
"",
"{\"test\": 2}",
"",
"",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 3}",
"{\"test\": 3}",
"",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"",
"",
"{\"test\": 1}",
"{\"test\": 5}",
"{\"test\": 1}",
"",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 2}",
"",
"{\"test\": 2}",
"{\"test\": 3}",
"{\"test\": 3}",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"",
"",
"{\"test\": 1}",
"",
"{\"test\": 3}",
"{\"test\": 1}",
"",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 2}",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 3}",
"",
"{\"test\": 1}",
"",
"{\"test\": 3}",
"",
"{\"test\": 3}",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 5}",
"",
"",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 3}",
"",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"",
"",
"",
"{\"test\": 3}",
"",
"",
"{\"test\": 3}",
"",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"",
"",
"{\"test\": 1}",
"",
"",
"{\"test\": 4}",
"{\"test\": 4}",
"{\"test\": 3}",
"{\"test\": 2}",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 5}",
"",
"{\"test\": 1}",
"{\"test\": 5}",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 2}",
"{\"test\": 5}",
"{\"test\": 2}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 2}",
"",
"",
"{\"test\": 3}",
"",
"{\"test\": 2}",
"{\"test\": 5}",
"",
"{\"test\": 3}",
"{\"test\": 1}",
"",
"",
"{\"test\": 4}",
"",
"",
"{\"test\": 2}",
"",
"",
"{\"test\": 1}",
"{\"test\": 4}",
"{\"test\": 1}",
"{\"test\": 2}",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 3}",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 3}",
"",
"",
"{\"test\": 1}",
"",
"",
"{\"test\": 2}",
"{\"test\": 2}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 5}",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 2}",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 4}",
"{\"test\": 1}",
"{\"test\": 3}",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 3}",
"",
"{\"test\": 4}",
"{\"test\": 5}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"{\"test\": 2}",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 3}",
"{\"test\": 3}",
"{\"test\": 1}",
"",
"",
"{\"test\": 2}",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 3}",
"",
"",
"",
"",
"{\"test\": 3}",
"{\"test\": 4}",
"{\"test\": 4}",
"{\"test\": 1}",
"{\"test\": 2}",
"",
"{\"test\": 1}",
"",
"{\"test\": 2}",
"{\"test\": 1}",
"{\"test\": 3}",
"",
"{\"test\": 3}",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 3}",
"",
"",
"",
"{\"test\": 4}",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"",
"{\"test\": 1}",
"{\"test\": 4}",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 1}",
"",
"",
"{\"test\": 3}",
"",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 2}",
"{\"test\": 4}",
"",
"{\"test\": 1}",
"",
"",
"",
"",
"{\"test\": 2}",
"{\"test\": 1}",
"{\"test\": 2}",
"",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 3}",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 1}",
"{\"test\": 5}",
"{\"test\": 4}",
"{\"test\": 1}",
"{\"test\": 2}",
"",
"{\"test\": 4}",
"",
"",
"",
"{\"test\": 4}",
"",
"",
"{\"test\": 3}",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 3}",
"{\"test\": 2}",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"",
"",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 2}",
"",
"",
"{\"test\": 1}",
"",
"{\"test\": 2}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 2}",
"",
"",
"{\"test\": 5}",
"{\"test\": 3}",
"",
"",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 3}",
"{\"test\": 3}",
"",
"",
"{\"test\": 1}",
"",
"{\"test\": 3}",
"{\"test\": 1}",
"",
"",
"",
"",
"{\"test\": 4}",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 3}",
"",
"",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"",
"{\"test\": 2}",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"",
"{\"test\": 2}",
"{\"test\": 3}",
"{\"test\": 2}",
"{\"test\": 3}",
"",
"",
"{\"test\": 1}",
"",
"",
"{\"test\": 1}",
"",
"{\"test\": 2}",
"",
"",
"",
"",
"{\"test\": 1}",
"",
"{\"test\": 3}",
"",
"{\"test\": 1}",
"",
"",
"{\"test\": 2}",
"{\"test\": 3}",
"{\"test\": 2}",
"{\"test\": 4}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"",
"{\"test\": 2}",
"",
"",
"",
"",
"",
"{\"test\": 1}",
"",
"",
"",
"",
"",
"{\"test\": 1}",
"",
"{\"test\": 2}",
"",
"",
"",
"",
"{\"test\": 2}",
"",
"{\"test\": 1}",
"",
"{\"test\": 3}",
"",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"",
"{\"test\": 3}",
"",
"",
"{\"test\": 2}",
"",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"",
"",
"",
"",
"{\"test\": 2}",
"",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"",
"{\"test\": 1}",
"",
"",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 2}",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 5}",
"",
"",
"",
"{\"test\": 1}",
"",
"{\"test\": 3}",
"",
"",
"{\"test\": 2}",
"",
"",
"",
"",
"",
"{\"test\": 4}",
"{\"test\": 2}",
"{\"test\": 1}",
"{\"test\": 2}",
"",
"{\"test\": 2}",
"",
"{\"test\": 2}",
"",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 2}",
"",
"{\"test\": 1}",
"",
"{\"test\": 5}",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"",
"{\"test\": 5}",
"",
"",
"{\"test\": 3}",
"",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 3}",
"",
"{\"test\": 1}",
"",
"",
"",
"",
"",
"{\"test\": 1}",
"",
"",
"",
"",
"{\"test\": 2}",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"{\"test\": 5}",
"{\"test\": 1}",
"",
"",
"{\"test\": 4}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"",
"{\"test\": 4}",
"{\"test\": 1}",
"",
"{\"test\": 2}",
"{\"test\": 3}",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 3}",
"{\"test\": 3}",
"{\"test\": 3}",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 2}",
"",
"",
"{\"test\": 2}",
"",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 5}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
"{\"test\": 5}",
"",
"",
"",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 4}",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 2}",
"{\"test\": 3}",
"",
"",
"{\"test\": 1}",
"",
"",
"",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 3}",
"",
"{\"test\": 1}",
"",
"{\"test\": 3}",
"{\"test\": 3}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 2}",
"",
"{\"test\": 2}",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 2}",
"",
"{\"test\": 3}",
"",
"{\"test\": 1}",
"{\"test\": 5}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 4}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 5}",
"",
"",
"",
"{\"test\": 2}",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 2}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 1}",
"",
"{\"test\": 2}",
"",
"",
"{\"test\": 3}",
"{\"test\": 2}",
"{\"test\": 3}",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 2}",
"{\"test\": 1}",
"{\"test\": 3}",
"",
"",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"",
"",
"{\"test\": 1}",
"{\"test\": 3}",
"{\"test\": 2}",
"{\"test\": 1}",
"{\"test\": 1}",
"{\"test\": 1}",
"",
"{\"test\": 3}",
"{\"test\": 2}",
"{\"test\": 3}",
"",
"",
"{\"test\": 3}",
"{\"test\": 1}",
"",
"{\"test\": 1}",
}
for i := range 1000 {
variant, _ := client.GetFeatureFlagPayload(
FeatureFlagPayload{
Key: "multivariate-flag",
DistinctId: fmt.Sprintf("%s%d", "distinct_id_", i),
},
)
if results[i] != variant {
t.Errorf("Match result is not consistent, expected %s, got %s", results[i], variant)
}
}
}
// Note: Cohort tests (TestComplexCohorts*) have been moved to feature_flags_cohorts_test.go
func TestFlagsFetchFail(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.WriteHeader(http.StatusInternalServerError)
} else if strings.HasPrefix(r.URL.Path, "/batch/") {
// ignore batch requests
} else {
t.Errorf("Unknown request made by library: %s", r.URL.String())
}
}))
defer server.Close()
client, err := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
FeatureFlagRequestTimeout: 10 * time.Millisecond,
})
require.NoError(t, err)
defer client.Close()
_, err = client.GetFeatureFlag(FeatureFlagPayload{
Key: "enabled-flag",
DistinctId: "123",
OnlyEvaluateLocally: true,
})
require.EqualError(t, err, "flags were not successfully fetched yet")
}
func TestFlagWithTimeoutExceeded(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
time.Sleep(1 * time.Second)
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-flag-group-properties.json")))
} else if strings.HasPrefix(r.URL.Path, "/batch/") {
// Ignore batch requests
} else {
t.Error("Unknown request made by library")
}
}))
defer server.Close()
client, err := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
FeatureFlagRequestTimeout: 10 * time.Millisecond,
})
require.NoError(t, err)
defer client.Close()
isMatch, err := client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "enabled-flag",
DistinctId: "-",
},
)
require.Error(t, err)
if !strings.Contains(err.Error(), "context deadline exceeded") {
t.Error("Expected context deadline exceeded error")
}
require.Nil(t, isMatch)
// get all flags with no local evaluation possible
variants, err := client.GetAllFlags(
FeatureFlagPayloadNoKey{
DistinctId: "-",
Groups: Groups{"company": "posthog1"},
},
)
require.Error(t, err)
require.ErrorContains(t, err, "context deadline exceeded")
require.Empty(t, variants)
// get all flags with partial local evaluation possible
variants, err = client.GetAllFlags(
FeatureFlagPayloadNoKey{
DistinctId: "-",
Groups: Groups{"company": "posthog1"},
PersonProperties: NewProperties().Set("region", "USA"),
},
)
require.Error(t, err)
if !strings.Contains(err.Error(), "context deadline exceeded") {
t.Error("Expected context deadline exceeded error")
}
require.EqualValues(t, map[string]any{"simple-flag": true}, variants)
// get all flags with full local evaluation possible
variants, err = client.GetAllFlags(
FeatureFlagPayloadNoKey{
DistinctId: "-",
Groups: Groups{"company": "posthog1"},
PersonProperties: NewProperties().Set("region", "USA"),
GroupProperties: map[string]Properties{"company": NewProperties().Set("name", "Project Name 1")},
},
)
require.NoError(t, err)
fmt.Println(variants)
require.EqualValues(t, map[string]any{"simple-flag": true, "group-flag": true}, variants)
}
func TestFlagDefinitionsWithTimeoutExceeded(t *testing.T) {
// Not run in parallel - timeout-based tests are sensitive to system load
// create buffer to write logs to
var buf bytes.Buffer
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
w.Write([]byte(fixture("test-flags-v3.json")))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
// Sleep longer than client timeout (100ms) to trigger timeout
time.Sleep(1 * time.Second)
w.Write([]byte(fixture("feature_flag/test-flag-group-properties.json")))
} else if strings.HasPrefix(r.URL.Path, "/batch/") {
// Ignore batch requests
} else {
t.Error("Unknown request made by library")
}
}))
defer server.Close()
client, clientErr := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
FeatureFlagRequestTimeout: 100 * time.Millisecond,
Logger: StdLogger(log.New(&buf, "insights-test", log.LstdFlags), false),
})
if clientErr != nil {
t.Fatalf("Client creation failed: %v", clientErr)
}
defer client.Close()
_, err := client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "enabled-flag",
DistinctId: "-",
},
)
if err != nil {
t.Error("Unexpected error")
}
output := buf.String()
if !strings.Contains(output, "Unable to fetch feature flags") {
t.Errorf("Expected error fetching flags, got: %q", output)
}
if !strings.Contains(output, "context deadline exceeded") {
t.Errorf("Expected timeout error fetching flags, got: %q", output)
}
}
func TestFetchFlagsFails(t *testing.T) {
// This test verifies that even in presence of HTTP errors flags continue to be fetched.
var called atomic.Uint32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if called.Load() == 0 {
// Load initial flags successfully
w.Write([]byte(fixture("feature_flag/test-simple-flag.json")))
} else {
// Fail all next requests
w.WriteHeader(http.StatusInternalServerError)
}
called.Add(1)
}))
defer server.Close()
cli, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer cli.Close()
_, err := cli.(*client).featureFlagsPoller.GetFeatureFlags()
if err != nil {
t.Error("Should not fail", err)
}
cli.ReloadFeatureFlags()
cli.ReloadFeatureFlags()
_, err = cli.GetAllFlags(FeatureFlagPayloadNoKey{
DistinctId: "my-id",
})
if err != nil {
t.Error("Should not fail", err)
}
// Wait for the last request to complete
<-time.After(50 * time.Millisecond)
const expectedCalls = 3
actualCalls := called.Load()
if actualCalls != expectedCalls {
t.Error("Expected to be called", expectedCalls, "times but got", actualCalls)
}
}
// The reference implementation of overrides may be found in Rust feature-flag implementation:
//
// Check link: https://github.com/PostHog/posthog/blob/0bb3ed063c37f5be280e4283a0d2a6a6683a9534/rust/feature-flags/src/api/request_handler.rs#L1412
func TestFeatureFlagWithOverrides(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-group-props.json")))
} else if strings.HasPrefix(r.URL.Path, "/batch/") {
// ignore
} else {
t.Errorf("Unknown request made by library: %s", r.URL.String())
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
cases := []struct {
name string
req FeatureFlagPayload
want bool
err error
}{
{
"matches group_key",
FeatureFlagPayload{
Key: "flag-with-groups",
DistinctId: "test-id",
Groups: Groups{"company": "1"},
},
true,
nil,
},
{
"distinct id does not match",
FeatureFlagPayload{
Key: "flag-with-groups",
DistinctId: "test-id",
Groups: Groups{"company": "2"},
},
false,
nil,
},
{
"matching group_key property is not overridden",
FeatureFlagPayload{
Key: "flag-with-groups",
DistinctId: "test-id",
Groups: Groups{"company": "2", "project": "7"},
GroupProperties: map[string]Properties{"company": {
"$group_key": "1",
}},
},
true,
nil,
},
{
"no match group_key property is not overridden",
FeatureFlagPayload{
Key: "flag-with-groups",
DistinctId: "test-id",
Groups: Groups{"company": "1", "project": "7"},
GroupProperties: map[string]Properties{"company": {
"$group_key": "2",
}},
},
false,
nil,
},
}
for _, c := range cases {
t.Run(c.name, func(tt *testing.T) {
enabled, err := client.GetFeatureFlag(c.req)
require.Equal(tt, c.err, err)
require.Equal(tt, c.want, enabled)
})
}
}
func TestFeatureFlagDistinctIDOverride(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-distinct-id-local.json")))
} else if strings.HasPrefix(r.URL.Path, "/batch/") {
// ignore
} else {
t.Errorf("Unknown request made by library: %s", r.URL.String())
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
cases := []struct {
name string
req FeatureFlagPayload
want bool
err error
}{
{
"matches distinct id",
FeatureFlagPayload{
Key: "test-boolean-flag-with-rollout-conditions",
DistinctId: "1",
},
true,
nil,
},
{
"distinct id does not match",
FeatureFlagPayload{
Key: "test-boolean-flag-with-rollout-conditions",
DistinctId: "2",
},
false,
nil,
},
{
"distinct id property is not override",
FeatureFlagPayload{
Key: "test-boolean-flag-with-rollout-conditions",
DistinctId: "2",
PersonProperties: NewProperties().Set("distinct_id", "1"),
},
true,
nil,
},
}
for _, c := range cases {
t.Run(c.name, func(tt *testing.T) {
enabled, err := client.GetFeatureFlag(c.req)
require.Equal(tt, c.err, err)
require.Equal(tt, c.want, enabled)
})
}
}
func TestFeatureFlagDeviceIDBucketingLocalEvaluation(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-device-id-bucketing.json")))
} else if strings.HasPrefix(r.URL.Path, "/batch/") {
// ignore
} else {
t.Errorf("Unknown request made by library: %s", r.URL.String())
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
distinctId := "distinct-123"
deviceId := "device-456"
expectedDevice := checkIfSimpleFlagEnabled("device-bucket-flag", deviceId, 50)
expectedDistinct := checkIfSimpleFlagEnabled("device-bucket-flag", distinctId, 50)
require.NotEqual(t, expectedDistinct, expectedDevice)
enabled, err := client.GetFeatureFlag(FeatureFlagPayload{
Key: "device-bucket-flag",
DistinctId: distinctId,
DeviceId: &deviceId,
OnlyEvaluateLocally: true,
})
require.NoError(t, err)
require.Equal(t, expectedDevice, enabled)
}
func TestFeatureFlagWithFalseVariant(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
w.Write([]byte(fixture("feature_flag/test-false-variant.json")))
} else if strings.HasPrefix(r.URL.Path, "/batch/") {
// ignore
} else {
t.Errorf("Unknown request made by library: %s", r.URL.String())
}
}))
defer server.Close()
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
})
defer client.Close()
// Test GetFeatureFlag - should return the variant name "false"
variant, err := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "test-flag",
DistinctId: "test-id",
},
)
require.NoError(t, err)
require.Equal(t, "false", variant)
// Test IsFeatureEnabled - should return the variant name "false"
// This will fail with the current implementation because it incorrectly converts "false" to false
isEnabled, err := client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "test-flag",
DistinctId: "test-id",
},
)
require.NoError(t, err)
require.Equal(t, "false", isEnabled)
}
func TestBasicEmailPropertyMatching(t *testing.T) {
// Test basic property matching for icontains
property := FlagProperty{
Key: "email",
Operator: "icontains",
Value: "@example.com",
Type: "person",
}
// Should match
properties1 := NewProperties().Set("email", "[email protected]")
match1, err1 := matchProperty(property, properties1)
require.NoError(t, err1)
require.True(t, match1, "Should match email containing @example.com")
// Should not match
properties2 := NewProperties().Set("email", "[email protected]")
match2, err2 := matchProperty(property, properties2)
require.NoError(t, err2)
require.False(t, match2, "Should not match email not containing @example.com")
}
// Note: Dependency tests (TestFlagDependencies*, TestMultiLevelMultivariateDependencyChain) have been moved to feature_flags_dependencies_test.go
func TestProductionStyleMultivariateDependencyChain(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
response := `{
"flags": [
{
"id": 451,
"key": "multivariate-leaf-flag",
"name": "Multivariate Leaf Flag (Base)",
"active": true,
"deleted": false,
"is_simple_flag": false,
"rollout_percentage": null,
"filters": {
"multivariate": {
"variants": [
{
"key": "pineapple",
"rollout_percentage": 25
},
{
"key": "mango",
"rollout_percentage": 25
},
{
"key": "papaya",
"rollout_percentage": 25
},
{
"key": "kiwi",
"rollout_percentage": 25
}
]
},
"groups": [
{
"variant": "pineapple",
"properties": [
{
"key": "email",
"type": "person",
"value": ["[email protected]"],
"operator": "exact"
}
],
"rollout_percentage": 100
},
{
"variant": "mango",
"properties": [
{
"key": "email",
"type": "person",
"value": ["[email protected]"],
"operator": "exact"
}
],
"rollout_percentage": 100
},
{
"variant": "papaya",
"properties": [
{
"key": "email",
"type": "person",
"value": ["[email protected]"],
"operator": "exact"
}
],
"rollout_percentage": 100
},
{
"variant": "kiwi",
"properties": [
{
"key": "email",
"type": "person",
"value": ["[email protected]"],
"operator": "exact"
}
],
"rollout_percentage": 100
},
{
"properties": [],
"rollout_percentage": 0
}
]
}
},
{
"id": 467,
"key": "multivariate-intermediate-flag",
"name": "Multivariate Intermediate Flag (Depends on fruit)",
"active": true,
"deleted": false,
"is_simple_flag": false,
"rollout_percentage": null,
"filters": {
"multivariate": {
"variants": [
{
"key": "blue",
"rollout_percentage": 100
},
{
"key": "red",
"rollout_percentage": 0
},
{
"key": "green",
"rollout_percentage": 0
},
{
"key": "black",
"rollout_percentage": 0
}
]
},
"groups": [
{
"variant": "blue",
"properties": [
{
"key": "multivariate-leaf-flag",
"type": "flag",
"value": "pineapple",
"operator": "flag_evaluates_to",
"dependency_chain": ["multivariate-leaf-flag"]
}
],
"rollout_percentage": 100
},
{
"variant": "red",
"properties": [
{
"key": "multivariate-leaf-flag",
"type": "flag",
"value": "mango",
"operator": "flag_evaluates_to",
"dependency_chain": ["multivariate-leaf-flag"]
}
],
"rollout_percentage": 100
}
]
}
},
{
"id": 468,
"key": "multivariate-root-flag",
"name": "Multivariate Root Flag (Depends on color)",
"active": true,
"deleted": false,
"is_simple_flag": false,
"rollout_percentage": null,
"filters": {
"multivariate": {
"variants": [
{
"key": "breaking-bad",
"rollout_percentage": 100
},
{
"key": "the-wire",
"rollout_percentage": 0
},
{
"key": "game-of-thrones",
"rollout_percentage": 0
},
{
"key": "the-expanse",
"rollout_percentage": 0
}
]
},
"groups": [
{
"variant": "breaking-bad",
"properties": [
{
"key": "multivariate-intermediate-flag",
"type": "flag",
"value": "blue",
"operator": "flag_evaluates_to",
"dependency_chain": ["multivariate-leaf-flag", "multivariate-intermediate-flag"]
}
],
"rollout_percentage": 100
},
{
"variant": "the-wire",
"properties": [
{
"key": "multivariate-intermediate-flag",
"type": "flag",
"value": "red",
"operator": "flag_evaluates_to",
"dependency_chain": ["multivariate-leaf-flag", "multivariate-intermediate-flag"]
}
],
"rollout_percentage": 100
}
]
}
}
]
}`
w.Write([]byte(response))
}))
defer server.Close()
client, _ := NewWithConfig("test-api-key", Config{
PersonalApiKey: "test-personal-key",
Endpoint: server.URL,
})
defer client.Close()
// Test successful pineapple -> blue -> breaking-bad chain
leafResult, err := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "multivariate-leaf-flag",
DistinctId: "test-user",
PersonProperties: NewProperties().
Set("email", "[email protected]"),
},
)
require.NoError(t, err)
require.Equal(t, "pineapple", leafResult)
intermediateResult, err := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "multivariate-intermediate-flag",
DistinctId: "test-user",
PersonProperties: NewProperties().
Set("email", "[email protected]"),
},
)
require.NoError(t, err)
require.Equal(t, "blue", intermediateResult)
rootResult, err := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "multivariate-root-flag",
DistinctId: "test-user",
PersonProperties: NewProperties().
Set("email", "[email protected]"),
},
)
require.NoError(t, err)
require.Equal(t, "breaking-bad", rootResult)
// Test successful mango -> red -> the-wire chain
mangoLeafResult, err := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "multivariate-leaf-flag",
DistinctId: "test-user",
PersonProperties: NewProperties().
Set("email", "[email protected]"),
},
)
require.NoError(t, err)
require.Equal(t, "mango", mangoLeafResult)
mangoIntermediateResult, err := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "multivariate-intermediate-flag",
DistinctId: "test-user",
PersonProperties: NewProperties().
Set("email", "[email protected]"),
},
)
require.NoError(t, err)
require.Equal(t, "red", mangoIntermediateResult)
mangoRootResult, err := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "multivariate-root-flag",
DistinctId: "test-user",
PersonProperties: NewProperties().
Set("email", "[email protected]"),
},
)
require.NoError(t, err)
require.Equal(t, "the-wire", mangoRootResult)
// Test broken chain - user without matching email gets default/false results
unknownLeafResult, err := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "multivariate-leaf-flag",
DistinctId: "test-user",
PersonProperties: NewProperties().
Set("email", "[email protected]"),
},
)
require.NoError(t, err)
require.Equal(t, false, unknownLeafResult) // No matching email -> null variant -> false
unknownIntermediateResult, err := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "multivariate-intermediate-flag",
DistinctId: "test-user",
PersonProperties: NewProperties().
Set("email", "[email protected]"),
},
)
require.NoError(t, err)
require.Equal(t, false, unknownIntermediateResult) // Dependency not satisfied
unknownRootResult, err := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "multivariate-root-flag",
DistinctId: "test-user",
PersonProperties: NewProperties().
Set("email", "[email protected]"),
},
)
require.NoError(t, err)
require.Equal(t, false, unknownRootResult) // Dependency chain broken
}
func TestFallbackToAPIWhenFlagHasStaticCohortInMultiCondition(t *testing.T) {
// When a flag has multiple conditions and one contains a static cohort,
// the SDK should fallback to API for the entire flag, not just skip that
// condition and evaluate the next one locally.
//
// This prevents returning wrong variants when later conditions could match
// locally but the user is actually in the static cohort.
var flagsAPICalled bool
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
// Return API response indicating user is in the static cohort
flagsAPICalled = true
w.Write([]byte(`{"featureFlags": {"multi-condition-flag": "set-1"}}`))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
// Return local evaluation data WITHOUT cohort 999 (making it a static cohort)
w.Write([]byte(`{
"flags": [
{
"id": 1,
"key": "multi-condition-flag",
"active": true,
"filters": {
"groups": [
{
"properties": [
{"key": "id", "value": 999, "type": "cohort"}
],
"rollout_percentage": 100,
"variant": "set-1"
},
{
"properties": [
{
"key": "$geoip_country_code",
"operator": "exact",
"value": ["DE"],
"type": "person"
}
],
"rollout_percentage": 100,
"variant": "set-8"
}
],
"multivariate": {
"variants": [
{"key": "set-1", "rollout_percentage": 50},
{"key": "set-8", "rollout_percentage": 50}
]
}
}
}
],
"cohorts": {}
}`))
}
}))
defer server.Close()
client, _ := NewWithConfig("test_api_key", Config{
Endpoint: server.URL,
PersonalApiKey: "test_personal_api_key",
})
defer client.Close()
// Wait for local evaluation to load
time.Sleep(100 * time.Millisecond)
result, err := client.GetFeatureFlag(FeatureFlagPayload{
Key: "multi-condition-flag",
DistinctId: "test-user",
PersonProperties: NewProperties().
Set("$geoip_country_code", "DE"),
})
if err != nil {
t.Fatalf("GetFeatureFlag returned error: %v", err)
}
// Should return API result (set-1), not local evaluation result (set-8)
if !flagsAPICalled {
t.Errorf("FLAGS API was not called - local evaluation did not fallback")
}
if result != "set-1" {
t.Errorf("Expected 'set-1' from API, got %v", result)
}
}
func TestGetFeatureFlagPayloadFallbackToAPIWhenFlagHasStaticCohort(t *testing.T) {
// When a flag has a static cohort and GetFeatureFlagPayload is called,
// the SDK should fallback to API and return the API payload, not the local payload.
//
// This test verifies the fix for the bug where GetFeatureFlagPayload
// didn't properly handle static cohort errors like GetFeatureFlag did.
var flagsAPICalled bool
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/flags" || r.URL.Path == "/flags/" {
// Return API response with payload
flagsAPICalled = true
w.Write([]byte(`{"featureFlags": {"flag-with-cohort": "variant-a"}, "featureFlagPayloads": {"flag-with-cohort": "{\"message\": \"from-api\"}"}}`))
} else if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
// Return local evaluation data WITHOUT cohort 999 (making it a static cohort)
w.Write([]byte(`{
"flags": [
{
"id": 1,
"key": "flag-with-cohort",
"active": true,
"filters": {
"groups": [
{
"properties": [
{"key": "id", "value": 999, "type": "cohort"}
],
"rollout_percentage": 100,
"variant": "variant-a"
}
],
"multivariate": {
"variants": [
{"key": "variant-a", "rollout_percentage": 100}
]
},
"payloads": {
"variant-a": "{\"message\": \"from-local\"}"
}
}
}
],
"cohorts": {}
}`))
}
}))
defer server.Close()
client, _ := NewWithConfig("test_api_key", Config{
Endpoint: server.URL,
PersonalApiKey: "test_personal_api_key",
})
defer client.Close()
// Wait for local evaluation to load
time.Sleep(100 * time.Millisecond)
result, err := client.GetFeatureFlagPayload(FeatureFlagPayload{
Key: "flag-with-cohort",
DistinctId: "test-user",
})
if err != nil {
t.Fatalf("GetFeatureFlagPayload returned error: %v", err)
}
// Should call FLAGS API since cohort is static (not in local cohorts map)
if !flagsAPICalled {
t.Errorf("FLAGS API was not called - local evaluation did not fallback")
}
// Should return API payload, not local payload
expectedPayload := "{\"message\": \"from-api\"}"
if result != expectedPayload {
t.Errorf("Expected payload '%s' from API, got '%s'", expectedPayload, result)
}
}
func TestDateBeforeOperatorAbsolute(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/flags/definitions") {
response := `{
"flags": [
{
"id": 1,
"key": "test-flag",
"active": true,
"filters": {
"groups": [
{
"properties": [
{
"key": "created_at",
"value": "2024-12-31T23:59:59Z",
"operator": "is_date_before",
"type": "person"
}
],
"rollout_percentage": 100
}
]
}
}
],
"group_type_mapping": {},
"cohorts": {}
}`
w.Write([]byte(response))
}
}))
defer server.Close()
client, _ := NewWithConfig("test-api-key", Config{
PersonalApiKey: "test-personal-key",
Endpoint: server.URL,
})
defer client.Close()
// Person has created_at "2024-01-01T00:00:00Z" which is before "2024-12-31T23:59:59Z"
result, err := client.GetFeatureFlag(
FeatureFlagPayload{
Key: "test-flag",
DistinctId: "user-123",
PersonProperties: NewProperties().Set("created_at", "2024-01-01T00:00:00Z"),
OnlyEvaluateLocally: true,
SendFeatureFlagEvents: nil,
},
)
require.NoError(t, err)
require.Equal(t, true, result)
}