Skip to content

Commit

Permalink
add support for fetching feature flag payloads (#64)
Browse files Browse the repository at this point in the history
* add support for fetching feature flag payloads

* improve mock server handler readability

* drying code

* update decide fixtures

* pr feedback
  • Loading branch information
jvitoroc authored Aug 13, 2024
1 parent 1d9acb0 commit a26a569
Show file tree
Hide file tree
Showing 13 changed files with 1,710 additions and 65 deletions.
1,242 changes: 1,217 additions & 25 deletions feature_flags_test.go

Large diffs are not rendered by default.

106 changes: 80 additions & 26 deletions featureflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -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()
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"properties": [{"key": "id", "value": 98, "operator": null, "type": "cohort"}],
"rollout_percentage": 100
}
]
],
"payloads": { "true": "{\"test\": 1}" }
},
"deleted": false,
"active": true,
Expand Down
7 changes: 7 additions & 0 deletions fixtures/feature_flag/test-multivariate-flag.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion fixtures/feature_flag/test-variant-override-clashing.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion fixtures/feature_flag/test-variant-override-invalid.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion fixtures/feature_flag/test-variant-override-multiple.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion fixtures/feature_flag/test-variant-override.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
25 changes: 23 additions & 2 deletions fixtures/test-api-feature-flag.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand All @@ -30,7 +50,8 @@
"properties": [],
"rollout_percentage": null
}
]
],
"payloads": { "true": "{\"test\": 1}", "false": "{\"test\": 0}" }
},
"deleted": false,
"active": true,
Expand Down
40 changes: 40 additions & 0 deletions fixtures/test-decide-v3.json
Original file line number Diff line number Diff line change
@@ -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": []
}
2 changes: 1 addition & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Loading

0 comments on commit a26a569

Please sign in to comment.