diff --git a/feature_flags_test.go b/feature_flags_test.go index fc0c927..a55320d 100644 --- a/feature_flags_test.go +++ b/feature_flags_test.go @@ -255,7 +255,7 @@ func TestFlagPersonProperty(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-simple-flag-person-prop.json"))) } @@ -329,7 +329,7 @@ func TestFlagGroup(t *testing.T) { if !groupPropertiesEquality { t.Errorf("Expected groupProperties to be map[company:map[name:Project Name 1]], got %s", reqBody.GroupProperties) } - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-flag-group-properties.json"))) } else if strings.HasPrefix(r.URL.Path, "/batch/") { @@ -415,7 +415,7 @@ func TestFlagGroupProperty(t *testing.T) { func TestComplexDefinition(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-complex-definition.json"))) // Don't return anything for local eval } @@ -457,7 +457,7 @@ func TestComplexDefinition(t *testing.T) { func TestFallbackToDecide(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte("{}")) // Don't return anything for local eval } @@ -486,7 +486,7 @@ func TestFallbackToDecide(t *testing.T) { func TestFeatureFlagsDontFallbackToDecideWhenOnlyLocalEvaluationIsTrue(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte("test-decide-v2.json")) + w.Write([]byte("test-decide-v3.json")) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-feature-flags-dont-fallback-to-decide-when-only-local-evaluation-is-true.json"))) } @@ -499,6 +499,18 @@ func TestFeatureFlagsDontFallbackToDecideWhenOnlyLocalEvaluationIsTrue(t *testin }) 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", @@ -551,7 +563,7 @@ func TestFeatureFlagsDontFallbackToDecideWhenOnlyLocalEvaluationIsTrue(t *testin func TestFeatureFlagDefaultsDontHinderEvaluation(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-false.json"))) } @@ -623,6 +635,17 @@ func TestFeatureFlagNullComeIntoPlayOnlyWhenDecideErrorsOut(t *testing.T) { }) 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", @@ -649,7 +672,7 @@ func TestFeatureFlagNullComeIntoPlayOnlyWhenDecideErrorsOut(t *testing.T) { func TestExperienceContinuityOverride(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-simple-flag.json"))) } @@ -673,12 +696,23 @@ func TestExperienceContinuityOverride(t *testing.T) { 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 strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-multiple-flags.json"))) } @@ -704,7 +738,7 @@ func TestGetAllFlags(t *testing.T) { func TestGetAllFlagsEmptyLocal(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte("{}")) } @@ -730,7 +764,7 @@ func TestGetAllFlagsEmptyLocal(t *testing.T) { func TestGetAllFlagsNoDecide(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-multiple-flags-valid.json"))) } @@ -756,7 +790,7 @@ func TestGetAllFlagsNoDecide(t *testing.T) { func TestGetAllFlagsOnlyLocalEvaluationSet(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-get-all-flags-with-fallback-but-only-local-evaluation-set.json"))) } @@ -783,7 +817,7 @@ func TestGetAllFlagsOnlyLocalEvaluationSet(t *testing.T) { func TestComputeInactiveFlagsLocally(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-compute-inactive-flags-locally.json"))) } @@ -807,7 +841,7 @@ func TestComputeInactiveFlagsLocally(t *testing.T) { server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-compute-inactive-flags-locally-2.json"))) } @@ -856,7 +890,7 @@ func TestFeatureEnabledSimpleIsTrueWhenRolloutUndefined(t *testing.T) { func TestGetFeatureFlag(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-simple-flag-person-prop.json"))) } @@ -882,11 +916,40 @@ func TestGetFeatureFlag(t *testing.T) { } } +func TestGetFeatureFlagPayload(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/decide") { + w.Write([]byte(fixture("test-decide-v3.json"))) + } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { + 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 TestFlagWithVariantOverrides(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-variant-override.json"))) } @@ -912,6 +975,18 @@ func TestFlagWithVariantOverrides(t *testing.T) { t.Error("Should match", variant, "second-variant") } + payload, _ := client.GetFeatureFlagPayload( + FeatureFlagPayload{ + Key: "beta-feature", + DistinctId: "test_id", + PersonProperties: NewProperties().Set("email", "test@posthog.com"), + }, + ) + + if payload != "{\"test\": 2}" { + t.Error("Should match", payload, "{\"test\": 2}") + } + variant, _ = client.GetFeatureFlag( FeatureFlagPayload{ Key: "beta-feature", @@ -922,13 +997,23 @@ func TestFlagWithVariantOverrides(t *testing.T) { 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 strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-variant-override-clashing.json"))) } @@ -954,6 +1039,18 @@ func TestFlagWithClashingVariantOverrides(t *testing.T) { t.Error("Should match", variant, "second-variant") } + payload, _ := client.GetFeatureFlagPayload( + FeatureFlagPayload{ + Key: "beta-feature", + DistinctId: "test_id", + PersonProperties: NewProperties().Set("email", "test@posthog.com"), + }, + ) + + if payload != "{\"test\": 2}" { + t.Error("Should match", payload, "{\"test\": 2}") + } + variant, _ = client.GetFeatureFlag( FeatureFlagPayload{ Key: "beta-feature", @@ -965,13 +1062,24 @@ func TestFlagWithClashingVariantOverrides(t *testing.T) { 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", "test@posthog.com"), + }, + ) + + 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 strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-variant-override-invalid.json"))) } @@ -997,6 +1105,18 @@ func TestFlagWithInvalidVariantOverrides(t *testing.T) { t.Error("Should match", variant, "third-variant") } + payload, _ := client.GetFeatureFlagPayload( + FeatureFlagPayload{ + Key: "beta-feature", + DistinctId: "test_id", + PersonProperties: NewProperties().Set("email", "test@posthog.com"), + }, + ) + + if payload != "{\"test\": 3}" { + t.Error("Should match", payload, "{\"test\": 3}") + } + variant, _ = client.GetFeatureFlag( FeatureFlagPayload{ Key: "beta-feature", @@ -1005,15 +1125,25 @@ func TestFlagWithInvalidVariantOverrides(t *testing.T) { ) if variant != "second-variant" { - t.Error("Should match", variant, "third-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 TestFlagWithMultipleVariantOverrides(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-variant-override-multiple.json"))) } @@ -1039,6 +1169,18 @@ func TestFlagWithMultipleVariantOverrides(t *testing.T) { t.Error("Should match", variant, "second-variant") } + payload, _ := client.GetFeatureFlagPayload( + FeatureFlagPayload{ + Key: "beta-feature", + DistinctId: "test_id", + PersonProperties: NewProperties().Set("email", "test@posthog.com"), + }, + ) + + if payload != "{\"test\": 2}" { + t.Error("Should match", payload, "{\"test\": 2}") + } + variant, _ = client.GetFeatureFlag( FeatureFlagPayload{ Key: "beta-feature", @@ -1050,6 +1192,17 @@ func TestFlagWithMultipleVariantOverrides(t *testing.T) { t.Error("Should match", variant, "third-variant") } + payload, _ = client.GetFeatureFlagPayload( + FeatureFlagPayload{ + Key: "beta-feature", + DistinctId: "example_id", + }, + ) + + if payload != "{\"test\": 3}" { + t.Error("Should match", payload, "{\"test\": 3}") + } + variant, _ = client.GetFeatureFlag( FeatureFlagPayload{ Key: "beta-feature", @@ -1060,12 +1213,23 @@ func TestFlagWithMultipleVariantOverrides(t *testing.T) { if variant != "second-variant" { t.Error("Should match", variant, "second-variant") } + + payload, _ = client.GetFeatureFlagPayload( + FeatureFlagPayload{ + Key: "beta-feature", + DistinctId: "another_id", + }, + ) + + if payload != "{\"test\": 2}" { + t.Error("Should match", payload, "{\"test\": 2}") + } } func TestCaptureIsCalled(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-simple-flag-person-prop.json"))) } @@ -3150,6 +3314,1034 @@ func TestMultivariateFlagConsistency(t *testing.T) { } } +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 := 0; i < 1000; i++ { + 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) + } + } +} + func TestComplexCohortsLocally(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fixture("feature_flag/test-complex-cohorts-locally.json"))) // Don't return anything for local eval @@ -3241,7 +4433,7 @@ func TestFlagWithTimeoutExceeded(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { time.Sleep(1 * time.Second) - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("feature_flag/test-flag-group-properties.json"))) } else if strings.HasPrefix(r.URL.Path, "/batch/") { @@ -3342,7 +4534,7 @@ func TestFlagDefinitionsWithTimeoutExceeded(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { time.Sleep(11 * time.Second) w.Write([]byte(fixture("feature_flag/test-flag-group-properties.json"))) diff --git a/featureflags.go b/featureflags.go index b24839a..ab8d6b6 100644 --- a/featureflags.go +++ b/featureflags.go @@ -50,6 +50,7 @@ type Filter struct { AggregationGroupTypeIndex *uint8 `json:"aggregation_group_type_index"` Groups []FeatureFlagCondition `json:"groups"` Multivariate *Variants `json:"multivariate"` + Payloads map[string]string `json:"payloads"` } type Variants struct { @@ -103,7 +104,8 @@ type DecideRequestData struct { } type DecideResponse struct { - FeatureFlags map[string]interface{} `json:"featureFlags"` + FeatureFlags map[string]interface{} `json:"featureFlags"` + FeatureFlagPayloads map[string]string `json:"featureFlagPayloads"` } type InconclusiveMatchError struct { @@ -205,42 +207,28 @@ func (poller *FeatureFlagsPoller) fetchNewFeatureFlags() { } func (poller *FeatureFlagsPoller) GetFeatureFlag(flagConfig FeatureFlagPayload) (interface{}, error) { - featureFlags, err := poller.GetFeatureFlags() - if err != nil { - return nil, err - } - cohorts := poller.cohorts - - featureFlag := FeatureFlag{Key: ""} - - // avoid using flag for conflicts with Golang's stdlib `flag` - for _, storedFlag := range featureFlags { - if flagConfig.Key == storedFlag.Key { - featureFlag = storedFlag - break - } - } + flag, err := poller.getFeatureFlag(flagConfig) var result interface{} - if featureFlag.Key != "" { + if flag.Key != "" { result, err = poller.computeFlagLocally( - featureFlag, + flag, flagConfig.DistinctId, flagConfig.Groups, flagConfig.PersonProperties, flagConfig.GroupProperties, - cohorts, + poller.cohorts, ) } if err != nil { - poller.Errorf("Unable to compute flag locally (%s) - %s", featureFlag.Key, err) + poller.Errorf("Unable to compute flag locally (%s) - %s", flag.Key, err) } if (err != nil || result == nil) && !flagConfig.OnlyEvaluateLocally { - result, err = poller.getFeatureFlagVariant(featureFlag, flagConfig.Key, flagConfig.DistinctId, flagConfig.Groups, flagConfig.PersonProperties, flagConfig.GroupProperties) + result, err = poller.getFeatureFlagVariant(flag, flagConfig.Key, flagConfig.DistinctId, flagConfig.Groups, flagConfig.PersonProperties, flagConfig.GroupProperties) if err != nil { return nil, err } @@ -249,6 +237,63 @@ func (poller *FeatureFlagsPoller) GetFeatureFlag(flagConfig FeatureFlagPayload) return result, err } +func (poller *FeatureFlagsPoller) GetFeatureFlagPayload(flagConfig FeatureFlagPayload) (string, error) { + flag, err := poller.getFeatureFlag(flagConfig) + + var variant interface{} + + if flag.Key != "" { + variant, err = poller.computeFlagLocally( + flag, + flagConfig.DistinctId, + flagConfig.Groups, + flagConfig.PersonProperties, + flagConfig.GroupProperties, + poller.cohorts, + ) + } + if err != nil { + poller.Errorf("Unable to compute flag locally (%s) - %s", flag.Key, err) + } + + if variant != nil { + payload, ok := flag.Filters.Payloads[fmt.Sprintf("%v", variant)] + if ok { + return payload, nil + } + } + + if (variant == nil || err != nil) && !flagConfig.OnlyEvaluateLocally { + result, err := poller.getFeatureFlagPayload(flagConfig.Key, flagConfig.DistinctId, flagConfig.Groups, flagConfig.PersonProperties, flagConfig.GroupProperties) + if err != nil { + return "", err + } + + return result, nil + } + + return "", errors.New("unable to compute flag locally") +} + +func (poller *FeatureFlagsPoller) getFeatureFlag(flagConfig FeatureFlagPayload) (FeatureFlag, error) { + featureFlags, err := poller.GetFeatureFlags() + if err != nil { + return FeatureFlag{}, err + } + + featureFlag := FeatureFlag{Key: ""} + + // avoid using flag for conflicts with Golang's stdlib `flag` + for _, storedFlag := range featureFlags { + if flagConfig.Key == storedFlag.Key { + featureFlag = storedFlag + break + } + } + + return featureFlag, nil +} + func (poller *FeatureFlagsPoller) GetAllFlags(flagConfig FeatureFlagPayloadNoKey) (map[string]interface{}, error) { response := map[string]interface{}{} featureFlags, err := poller.GetFeatureFlags() @@ -285,7 +330,7 @@ func (poller *FeatureFlagsPoller) GetAllFlags(flagConfig FeatureFlagPayloadNoKey if err != nil { return response, err } else { - for k, v := range result { + for k, v := range result.FeatureFlags { response[k] = v } } @@ -820,7 +865,7 @@ func (poller *FeatureFlagsPoller) GetFeatureFlags() ([]FeatureFlag, error) { } func (poller *FeatureFlagsPoller) decide(requestData []byte, headers [][2]string) (*http.Response, context.CancelFunc, error) { - decideEndpoint := "decide/?v=2" + decideEndpoint := "decide/?v=3" url, err := url.Parse(poller.Endpoint + "/" + decideEndpoint + "") if err != nil { @@ -879,7 +924,7 @@ func (poller *FeatureFlagsPoller) shutdownPoller() { poller.shutdown <- true } -func (poller *FeatureFlagsPoller) getFeatureFlagVariants(distinctId string, groups Groups, personProperties Properties, groupProperties map[string]Properties) (map[string]interface{}, error) { +func (poller *FeatureFlagsPoller) getFeatureFlagVariants(distinctId string, groups Groups, personProperties Properties, groupProperties map[string]Properties) (*DecideResponse, error) { errorMessage := "Failed when getting flag variants" requestDataBytes, err := json.Marshal(DecideRequestData{ ApiKey: poller.projectApiKey, @@ -919,7 +964,7 @@ func (poller *FeatureFlagsPoller) getFeatureFlagVariants(distinctId string, grou return nil, errors.New(errorMessage) } - return decideResponse.FeatureFlags, nil + return &decideResponse, nil } func (poller *FeatureFlagsPoller) getFeatureFlagVariant(featureFlag FeatureFlag, key string, distinctId string, groups Groups, personProperties Properties, groupProperties map[string]Properties) (interface{}, error) { @@ -948,7 +993,7 @@ func (poller *FeatureFlagsPoller) getFeatureFlagVariant(featureFlag FeatureFlag, return false, variantErr } - for flagKey, flagValue := range featureFlagVariants { + for flagKey, flagValue := range featureFlagVariants.FeatureFlags { flagValueString := fmt.Sprintf("%v", flagValue) if key == flagKey && flagValueString != "false" { result = flagValueString @@ -960,6 +1005,15 @@ func (poller *FeatureFlagsPoller) getFeatureFlagVariant(featureFlag FeatureFlag, return result, nil } +func (poller *FeatureFlagsPoller) getFeatureFlagPayload(key string, distinctId string, groups Groups, personProperties Properties, groupProperties map[string]Properties) (string, error) { + featureFlagVariants, err := poller.getFeatureFlagVariants(distinctId, groups, personProperties, groupProperties) + if err != nil { + return "", err + } + + return featureFlagVariants.FeatureFlagPayloads[key], nil +} + func getSafeProp[T any](properties map[string]any, key string) T { switch v := properties[key].(type) { case T: diff --git a/fixtures/feature_flag/test-feature-flags-dont-fallback-to-decide-when-only-local-evaluation-is-true.json b/fixtures/feature_flag/test-feature-flags-dont-fallback-to-decide-when-only-local-evaluation-is-true.json index 3c8c38f..bb52670 100644 --- a/fixtures/feature_flag/test-feature-flags-dont-fallback-to-decide-when-only-local-evaluation-is-true.json +++ b/fixtures/feature_flag/test-feature-flags-dont-fallback-to-decide-when-only-local-evaluation-is-true.json @@ -14,7 +14,8 @@ "properties": [{"key": "id", "value": 98, "operator": null, "type": "cohort"}], "rollout_percentage": 100 } - ] + ], + "payloads": { "true": "{\"test\": 1}" } }, "deleted": false, "active": true, diff --git a/fixtures/feature_flag/test-multivariate-flag.json b/fixtures/feature_flag/test-multivariate-flag.json index 5e0db4d..8957af9 100644 --- a/fixtures/feature_flag/test-multivariate-flag.json +++ b/fixtures/feature_flag/test-multivariate-flag.json @@ -17,6 +17,13 @@ {"key": "fourth-variant", "name": "Fourth Variant", "rollout_percentage": 5}, {"key": "fifth-variant", "name": "Fifth Variant", "rollout_percentage": 5} ] + }, + "payloads": { + "first-variant": "{\"test\": 1}", + "second-variant": "{\"test\": 2}", + "third-variant": "{\"test\": 3}", + "fourth-variant": "{\"test\": 4}", + "fifth-variant": "{\"test\": 5}" } }, "deleted": false, diff --git a/fixtures/feature_flag/test-variant-override-clashing.json b/fixtures/feature_flag/test-variant-override-clashing.json index 29afa5f..e380e0e 100644 --- a/fixtures/feature_flag/test-variant-override-clashing.json +++ b/fixtures/feature_flag/test-variant-override-clashing.json @@ -31,7 +31,8 @@ {"key": "second-variant", "name": "Second Variant", "rollout_percentage": 25}, {"key": "third-variant", "name": "Third Variant", "rollout_percentage": 25} ] - } + }, + "payloads": { "first-variant": "{\"test\": 1}", "second-variant": "{\"test\": 2}" } }, "deleted": false, "active": true, diff --git a/fixtures/feature_flag/test-variant-override-invalid.json b/fixtures/feature_flag/test-variant-override-invalid.json index 8984f4c..9560fb0 100644 --- a/fixtures/feature_flag/test-variant-override-invalid.json +++ b/fixtures/feature_flag/test-variant-override-invalid.json @@ -22,7 +22,8 @@ {"key": "second-variant", "name": "Second Variant", "rollout_percentage": 25}, {"key": "third-variant", "name": "Third Variant", "rollout_percentage": 25} ] - } + }, + "payloads": { "third-variant": "{\"test\": 3}", "second-variant": "{\"test\": 2}" } }, "deleted": false, "active": true, diff --git a/fixtures/feature_flag/test-variant-override-multiple.json b/fixtures/feature_flag/test-variant-override-multiple.json index 648bfa7..3ebba5a 100644 --- a/fixtures/feature_flag/test-variant-override-multiple.json +++ b/fixtures/feature_flag/test-variant-override-multiple.json @@ -25,7 +25,8 @@ {"key": "second-variant", "name": "Second Variant", "rollout_percentage": 25}, {"key": "third-variant", "name": "Third Variant", "rollout_percentage": 25} ] - } + }, + "payloads": { "third-variant": "{\"test\": 3}", "second-variant": "{\"test\": 2}" } }, "deleted": false, "active": true, diff --git a/fixtures/feature_flag/test-variant-override.json b/fixtures/feature_flag/test-variant-override.json index 57747d0..e3b30b2 100644 --- a/fixtures/feature_flag/test-variant-override.json +++ b/fixtures/feature_flag/test-variant-override.json @@ -22,7 +22,8 @@ {"key": "second-variant", "name": "Second Variant", "rollout_percentage": 25}, {"key": "third-variant", "name": "Third Variant", "rollout_percentage": 25} ] - } + }, + "payloads": { "first-variant": "{\"test\": 1}", "second-variant": "{\"test\": 2}" } }, "deleted": false, "active": true, diff --git a/fixtures/test-api-feature-flag.json b/fixtures/test-api-feature-flag.json index 2639671..ba12e7d 100644 --- a/fixtures/test-api-feature-flag.json +++ b/fixtures/test-api-feature-flag.json @@ -13,13 +13,33 @@ "properties": [], "rollout_percentage": null } - ] + ], + "payloads": { "true": "{\"test\": 1}" } }, "deleted": false, "active": true, "is_simple_flag": true, "rollout_percentage": null }, + { + "id": 719, + "name": "", + "key": "continuation-flag", + "filters": { + "groups": [ + { + "properties": [], + "rollout_percentage": null + } + ], + "payloads": { "true": "{\"test\": 1}" } + }, + "deleted": false, + "active": true, + "is_simple_flag": true, + "rollout_percentage": null, + "ensure_experience_continuity": true + }, { "id": 720, "name": "", @@ -30,7 +50,8 @@ "properties": [], "rollout_percentage": null } - ] + ], + "payloads": { "true": "{\"test\": 1}", "false": "{\"test\": 0}" } }, "deleted": false, "active": true, diff --git a/fixtures/test-decide-v3.json b/fixtures/test-decide-v3.json new file mode 100644 index 0000000..a12b333 --- /dev/null +++ b/fixtures/test-decide-v3.json @@ -0,0 +1,40 @@ +{ + "config": { + "enable_collect_everything": true + }, + "editorParams": {}, + "isAuthenticated": true, + "supportedCompression": ["gzip", "gzip-js", "lz64"], + "toolbarParams": {}, + "featureFlags": { + "enabled-flag": true, + "group-flag": true, + "disabled-flag": false, + "multi-variate-flag": "hello", + "simple-flag": true, + "beta-feature": "decide-fallback-value", + "beta-feature2": "variant-2", + "false-flag-2": false, + "test-get-feature": "variant-1", + "continuation-flag": true + }, + "featureFlagPayloads": { + "enabled-flag": "{\"foo\": 1}", + "simple-flag": "{\"bar\": 2}", + "continuation-flag": "{\"foo\": \"bar\"}", + "beta-feature": "{\"foo\": \"bar\"}", + "test-get-feature": "this is a string", + "multi-variate-flag": "this is the payload" + }, + "sessionRecording": false, + "errorsWhileComputingFlags": false, + "capturePerformance": false, + "autocapture_opt_out": true, + "autocaptureExceptions": false, + "analytics": { "endpoint": "/i/v0/e/" }, + "__preview_ingestion_endpoints": true, + "elementsChainAsString": true, + "surveys": false, + "heatmaps": false, + "siteApps": [] +} diff --git a/go.sum b/go.sum index 15093c4..03d0eb7 100644 --- a/go.sum +++ b/go.sum @@ -12,4 +12,4 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= \ No newline at end of file diff --git a/posthog.go b/posthog.go index f6ed9c5..f72a67c 100644 --- a/posthog.go +++ b/posthog.go @@ -45,6 +45,9 @@ type Client interface { // if the given flag is on or off for the user GetFeatureFlag(FeatureFlagPayload) (interface{}, error) // + // Method returns feature flag payload value matching key for user (supports multivariate flags). + GetFeatureFlagPayload(FeatureFlagPayload) (string, error) + // // Get all flags - returns all flags for a user GetAllFlags(FeatureFlagPayloadNoKey) (map[string]interface{}, error) // @@ -297,6 +300,27 @@ func (c *client) ReloadFeatureFlags() error { return nil } +func (c *client) GetFeatureFlagPayload(flagConfig FeatureFlagPayload) (string, error) { + if err := flagConfig.validate(); err != nil { + return "", err + } + + var payload string + var err error + + if c.featureFlagsPoller != nil { + // get feature flag from the poller, which uses the personal api key + // this is only available when using a PersonalApiKey + payload, err = c.featureFlagsPoller.GetFeatureFlagPayload(flagConfig) + } else { + // if there's no poller, get the feature flag from the decide endpoint + c.debugf("getting feature flag from decide endpoint") + payload, err = c.getFeatureFlagPayloadFromDecide(flagConfig.Key, flagConfig.DistinctId, flagConfig.Groups, flagConfig.PersonProperties, flagConfig.GroupProperties) + } + + return payload, err +} + func (c *client) GetFeatureFlag(flagConfig FeatureFlagPayload) (interface{}, error) { if err := flagConfig.validate(); err != nil { return false, err @@ -596,7 +620,7 @@ func (c *client) getFeatureVariants(distinctId string, groups Groups, personProp if err != nil { return nil, err } - return featureVariants, nil + return featureVariants.FeatureFlags, nil } func (c *client) makeDecideRequest(distinctId string, groups Groups, personProperties Properties, groupProperties map[string]Properties) (*DecideResponse, error) { @@ -613,7 +637,7 @@ func (c *client) makeDecideRequest(distinctId string, groups Groups, personPrope return nil, fmt.Errorf("unable to marshal decide endpoint request data: %v", err) } - decideEndpoint := "decide/?v=2" + decideEndpoint := "decide/?v=3" url, err := url.Parse(c.Endpoint + "/" + decideEndpoint) if err != nil { return nil, fmt.Errorf("creating url: %v", err) @@ -664,6 +688,19 @@ func (c *client) getFeatureFlagFromDecide(key string, distinctId string, groups return false, nil } +func (c *client) getFeatureFlagPayloadFromDecide(key string, distinctId string, groups Groups, personProperties Properties, groupProperties map[string]Properties) (string, error) { + decideResponse, err := c.makeDecideRequest(distinctId, groups, personProperties, groupProperties) + if err != nil { + return "", err + } + + if value, ok := decideResponse.FeatureFlagPayloads[key]; ok { + return value, nil + } + + return "", nil +} + func (c *client) getAllFeatureFlagsFromDecide(distinctId string, groups Groups, personProperties Properties, groupProperties map[string]Properties) (map[string]interface{}, error) { decideResponse, err := c.makeDecideRequest(distinctId, groups, personProperties, groupProperties) if err != nil { diff --git a/posthog_test.go b/posthog_test.go index ed56c76..2daf1c6 100644 --- a/posthog_test.go +++ b/posthog_test.go @@ -887,10 +887,196 @@ func TestIsFeatureEnabled(t *testing.T) { } } +func TestGetFeatureFlagPayloadWithNoPersonalApiKey(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/decide") { + w.Write([]byte(fixture("test-decide-v3.json"))) + } else if !strings.HasPrefix(r.URL.Path, "/batch") { + t.Errorf("client called an endpoint it shouldn't have: %s", r.URL.Path) + } + })) + defer server.Close() + + client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{ + Endpoint: server.URL, + Logger: testLogger{t.Logf, t.Logf}, + Callback: testCallback{ + func(m APIMessage) {}, + func(m APIMessage, e error) {}, + }, + }) + defer client.Close() + + // Test GetFeatureFlagPayload single scenario + payload, err := client.GetFeatureFlagPayload(FeatureFlagPayload{ + Key: "enabled-flag", + DistinctId: "test-user", + }) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + // Check that the flag payload is as expected (should match the value in the fixture) + expectedPayload := "{\"foo\": 1}" + if payload != expectedPayload { + t.Errorf("Expected flag payload %v, got: %v", expectedPayload, payload) + } + + // Test a bunch of GetFeatureFlagPayload scenarios + tests := []struct { + name string + flagConfig FeatureFlagPayload + mockResponse string + expectedValue string + expectedError string + }{ + { + name: "Flag exists and there is a payload", + flagConfig: FeatureFlagPayload{ + Key: "test-flag", + DistinctId: "user123", + }, + mockResponse: `{"featureFlags": {"test-flag": true}, "featureFlagPayloads": {"test-flag": "{\"test\": 1}"}}`, + expectedValue: "{\"test\": 1}", + }, + { + name: "Flag exists and payload object is not present", + flagConfig: FeatureFlagPayload{ + Key: "test-flag", + DistinctId: "user123", + }, + mockResponse: `{"featureFlags": {"test-flag": false}}`, + expectedValue: "", + }, + { + name: "Flag exists and there is no payload", + flagConfig: FeatureFlagPayload{ + Key: "test-flag", + DistinctId: "user123", + }, + mockResponse: `{"featureFlags": {"test-flag": false}, "featureFlagPayloads": {}}`, + expectedValue: "", + }, + + { + name: "Flag doesn't exist", + flagConfig: FeatureFlagPayload{ + Key: "non-existent-flag", + DistinctId: "user123", + }, + mockResponse: `{"featureFlags": {"other-flag": true}}`, + expectedValue: "", + }, + { + name: "Empty response", + flagConfig: FeatureFlagPayload{ + Key: "test-flag", + DistinctId: "user123", + }, + mockResponse: `{}`, + expectedValue: "", + }, + { + name: "Invalid JSON response", + flagConfig: FeatureFlagPayload{ + Key: "test-flag", + DistinctId: "user123", + }, + mockResponse: `{invalid-json}`, + expectedError: "error parsing response from /decide/", + }, + { + name: "Non-200 status code", + flagConfig: FeatureFlagPayload{ + Key: "test-flag", + DistinctId: "user123", + }, + mockResponse: ``, + expectedError: "unexpected status code from /decide/: 500", + }, + { + name: "With groups and properties", + flagConfig: FeatureFlagPayload{ + Key: "test-flag", + DistinctId: "user123", + Groups: Groups{ + "company": "test-company", + }, + PersonProperties: Properties{ + "plan": "enterprise", + }, + GroupProperties: map[string]Properties{ + "company": { + "size": "large", + }, + }, + }, + mockResponse: `{"featureFlags": {"test-flag": "enterprise-variant"}, "featureFlagPayloads": {"test-flag": "{\"test\": 3}"}}`, + expectedValue: "{\"test\": 3}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check request method and path + if r.Method != "POST" || r.URL.Path != "/decide/" { + t.Errorf("Expected POST /decide/, got %s %s", r.Method, r.URL.Path) + } + + // Check headers + if r.Header.Get("Content-Type") != "application/json" { + t.Errorf("Expected Content-Type: application/json, got %s", r.Header.Get("Content-Type")) + } + if !strings.HasPrefix(r.Header.Get("User-Agent"), "posthog-go (version: ") { + t.Errorf("Unexpected User-Agent: %s", r.Header.Get("User-Agent")) + } + + // Check request body + body, _ := ioutil.ReadAll(r.Body) + var requestData DecideRequestData + json.Unmarshal(body, &requestData) + if requestData.DistinctId != tt.flagConfig.DistinctId { + t.Errorf("Expected distinctId %s, got %s", tt.flagConfig.DistinctId, requestData.DistinctId) + } + + // Send mock response + if tt.expectedError == "unexpected status code from /decide/: 500" { + w.WriteHeader(http.StatusInternalServerError) + } else { + w.WriteHeader(http.StatusOK) + w.Write([]byte(tt.mockResponse)) + } + })) + defer server.Close() + + client, _ := NewWithConfig("test-api-key", Config{ + Endpoint: server.URL, + }) + + value, err := client.GetFeatureFlagPayload(tt.flagConfig) + + if tt.expectedError != "" { + if err == nil || !strings.Contains(err.Error(), tt.expectedError) { + t.Errorf("Expected error containing '%s', got '%v'", tt.expectedError, err) + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if value != tt.expectedValue { + t.Errorf("Expected value %v, got %v", tt.expectedValue, value) + } + } + }) + } +} + func TestGetFeatureFlagWithNoPersonalApiKey(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if !strings.HasPrefix(r.URL.Path, "/batch") { t.Errorf("client called an endpoint it shouldn't have: %s", r.URL.Path) } @@ -1225,6 +1411,76 @@ func TestGetAllFeatureFlagsWithNoPersonalApiKey(t *testing.T) { } } +func TestGetFeatureFlagPayloadWithPersonalKey(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/decide") { + t.Fatal("expected local evaluations endpoint to be called") + } + w.Write([]byte(fixture("test-api-feature-flag.json"))) + })) + defer server.Close() + + client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{ + PersonalApiKey: "some very secret key", + Endpoint: server.URL, + }) + defer client.Close() + + payload, checkErr := client.GetFeatureFlagPayload( + FeatureFlagPayload{ + Key: "simpleFlag", + DistinctId: "hey", + }, + ) + + expectedPayload := "{\"test\": 1}" + + if checkErr != nil || payload != expectedPayload { + t.Errorf("expected payload %v, got %v", expectedPayload, payload) + } +} + +func TestGetFeatureFlagPayloadWithPersonalKey_LocalComputationFailure(t *testing.T) { + apiCalls := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if apiCalls == 0 && strings.HasPrefix(r.URL.Path, "/decide") { + t.Fatal("expected local evaluations endpoint to be called first") + } else if apiCalls == 1 && strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { + t.Fatal("expected decide endpoint to be called second") + } + + if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { + w.Write([]byte(fixture("test-api-feature-flag.json"))) + } else { + w.Write([]byte(fixture("test-decide-v3.json"))) + } + apiCalls++ + })) + defer server.Close() + + client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{ + PersonalApiKey: "some very secret key", + Endpoint: server.URL, + }) + defer client.Close() + + payload, checkErr := client.GetFeatureFlagPayload( + FeatureFlagPayload{ + Key: "continuation-flag", + DistinctId: "hey", + }, + ) + if checkErr != nil { + t.Error("expected no error, got", checkErr) + } + + expectedPayload := "{\"foo\": \"bar\"}" + + if payload != expectedPayload { + t.Errorf("expected payload %v, got %v", expectedPayload, payload) + } +} + func TestSimpleFlagOld(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fixture("test-api-feature-flag.json"))) @@ -1264,7 +1520,7 @@ func TestSimpleFlagCalculation(t *testing.T) { func TestComplexFlag(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte(fixture("test-api-feature-flag.json"))) } else if !strings.HasPrefix(r.URL.Path, "/batch") { @@ -1300,12 +1556,23 @@ func TestComplexFlag(t *testing.T) { if valueErr != nil || flagValue != true { t.Errorf("flag listed in /decide/ response should be true") } + + payload, valueErr := client.GetFeatureFlagPayload( + FeatureFlagPayload{ + Key: "enabled-flag", + DistinctId: "hey", + }, + ) + + if valueErr != nil || payload != "{\"test\": 1}" { + t.Errorf(`flag listed in /decide/ response should be "{\"test\": 1}"`) + } } func TestMultiVariateFlag(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte("{}")) } else if !strings.HasPrefix(r.URL.Path, "/batch") { @@ -1341,12 +1608,23 @@ func TestMultiVariateFlag(t *testing.T) { if err != nil || flagValue != "hello" { t.Errorf("flag listed in /decide/ response should have value 'hello'") } + + payload, err := client.GetFeatureFlagPayload( + FeatureFlagPayload{ + Key: "multi-variate-flag", + DistinctId: "hey", + }, + ) + + if err != nil || payload != "this is the payload" { + t.Errorf("flag listed in /decide/ response should have value 'this is the payload'") + } } func TestDisabledFlag(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/decide") { - w.Write([]byte(fixture("test-decide-v2.json"))) + w.Write([]byte(fixture("test-decide-v3.json"))) } else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") { w.Write([]byte("{}")) } else if !strings.HasPrefix(r.URL.Path, "/batch") { @@ -1382,6 +1660,17 @@ func TestDisabledFlag(t *testing.T) { if err != nil || flagValue != false { t.Errorf("flag listed in /decide/ response should have value 'false'") } + + payload, err := client.GetFeatureFlagPayload( + FeatureFlagPayload{ + Key: "disabled-flag", + DistinctId: "hey", + }, + ) + + if err != nil || payload != "" { + t.Errorf("flag listed in /decide/ response should have value ''") + } } func TestCaptureSendFlags(t *testing.T) {