From f6b47c4e421c9a57252a64a35ba47ca219a01efb Mon Sep 17 00:00:00 2001 From: Sam Jansen Date: Thu, 2 May 2024 12:05:08 +0100 Subject: [PATCH] Add feature flag payloads --- feature_flags_test.go | 48 +++++++++++++++++++ featureflags.go | 30 ++++++++++-- .../test-simple-flag-person-prop.json | 41 +++++++++++++++- posthog.go | 17 +++++++ 4 files changed, 130 insertions(+), 6 deletions(-) diff --git a/feature_flags_test.go b/feature_flags_test.go index 58d56a4..515d5fb 100644 --- a/feature_flags_test.go +++ b/feature_flags_test.go @@ -3427,3 +3427,51 @@ func TestFetchFlagsFails(t *testing.T) { t.Error("Expected to be called", expectedCalls, "times but got", actualCalls) } } + +func TestFlagWithPayload(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"))) + } 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() + + t.Run("Enabled flag with payload should return payload", func(t *testing.T) { + payload, err := client.GetFeatureFlagPayload( + FeatureFlagPayload{ + Key: "payload-flag", + DistinctId: "foobar", + }, + ) + + if err != nil { + t.Error("Should not fail", err) + } + + if payload != "{ \"key\": \"value\" }" { + t.Error("Should match", payload) + } + }) + + t.Run("Enabled flag without payload should return error", func(t *testing.T) { + _, err := client.GetFeatureFlagPayload( + FeatureFlagPayload{ + Key: "no-payload-flag", + DistinctId: "foobar", + }, + ) + + if err == nil { + t.Error("should fail", err) + } + }) +} diff --git a/featureflags.go b/featureflags.go index 87c04a8..b9870c9 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 { @@ -200,15 +201,15 @@ func (poller *FeatureFlagsPoller) fetchNewFeatureFlags() { poller.mutex.Unlock() } -func (poller *FeatureFlagsPoller) GetFeatureFlag(flagConfig FeatureFlagPayload) (interface{}, error) { +func (poller *FeatureFlagsPoller) getFeatureFlagInternal(flagConfig FeatureFlagPayload) (FeatureFlag, interface{}, error) { + var featureFlag FeatureFlag + featureFlags, err := poller.GetFeatureFlags() if err != nil { - return nil, err + return featureFlag, 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 { @@ -238,13 +239,32 @@ func (poller *FeatureFlagsPoller) GetFeatureFlag(flagConfig FeatureFlagPayload) result, err = poller.getFeatureFlagVariant(featureFlag, flagConfig.Key, flagConfig.DistinctId, flagConfig.Groups, flagConfig.PersonProperties, flagConfig.GroupProperties) if err != nil { - return nil, err + return featureFlag, nil, err } } + return featureFlag, result, err +} + +func (poller *FeatureFlagsPoller) GetFeatureFlag(flagConfig FeatureFlagPayload) (interface{}, error) { + _, result, err := poller.getFeatureFlagInternal(flagConfig) return result, err } +func (poller *FeatureFlagsPoller) GetFeatureFlagPayload(flagConfig FeatureFlagPayload) (string, error) { + ff, result, err := poller.getFeatureFlagInternal(flagConfig) + if err != nil { + return "", err + } + + payload, ok := ff.Filters.Payloads[fmt.Sprintf("%v", result)] + if !ok { + return "", errors.New("payload not found") + } + + return payload, nil +} + func (poller *FeatureFlagsPoller) GetAllFlags(flagConfig FeatureFlagPayloadNoKey) (map[string]interface{}, error) { response := map[string]interface{}{} featureFlags, err := poller.GetFeatureFlags() diff --git a/fixtures/feature_flag/test-simple-flag-person-prop.json b/fixtures/feature_flag/test-simple-flag-person-prop.json index 6458c94..a923616 100644 --- a/fixtures/feature_flag/test-simple-flag-person-prop.json +++ b/fixtures/feature_flag/test-simple-flag-person-prop.json @@ -26,6 +26,45 @@ "active": true, "is_simple_flag": true, "rollout_percentage": null - } + }, + { + "active" : true, + "deleted" : false, + "ensure_experience_continuity" : false, + "filters" : { + "groups" : [ + { + "properties" : [], + "rollout_percentage" : 100, + "variant" : null + } + ], + "multivariate" : null, + "payloads" : { + "true" : "{ \"key\": \"value\" }" + } + }, + "id" : 44289, + "key" : "payload-flag", + "team_id" : 12345 + }, + { + "active" : true, + "deleted" : false, + "ensure_experience_continuity" : false, + "filters" : { + "groups" : [ + { + "properties" : [], + "rollout_percentage" : 100, + "variant" : null + } + ], + "multivariate" : null + }, + "id" : 44290, + "key" : "no-payload-flag", + "team_id" : 12345 + } ] } \ No newline at end of file diff --git a/posthog.go b/posthog.go index 8af24ea..e2ea12d 100644 --- a/posthog.go +++ b/posthog.go @@ -44,6 +44,10 @@ type Client interface { // if the given flag is on or off for the user GetFeatureFlag(FeatureFlagPayload) (interface{}, error) // + // Method returns the feature flag payload, if the feature flag is active and has a + // payload. If it is not active, or has no payload, returns an error. + GetFeatureFlagPayload(FeatureFlagPayload) (string, error) + // // Method forces a reload of feature flags ReloadFeatureFlags() error // @@ -299,6 +303,19 @@ func (c *client) GetFeatureFlag(flagConfig FeatureFlagPayload) (interface{}, err return flagValue, err } +func (c *client) GetFeatureFlagPayload(flagConfig FeatureFlagPayload) (string, error) { + if err := flagConfig.validate(); err != nil { + return "", err + } + + if c.featureFlagsPoller == nil { + errorMessage := "specifying a PersonalApiKey is required for using feature flags" + c.Errorf(errorMessage) + return "", errors.New(errorMessage) + } + return c.featureFlagsPoller.GetFeatureFlagPayload(flagConfig) +} + func (c *client) GetFeatureFlags() ([]FeatureFlag, error) { if c.featureFlagsPoller == nil { errorMessage := "specifying a PersonalApiKey is required for using feature flags"