Skip to content

Commit

Permalink
Merge pull request #210 from splitio/task/FlagSets
Browse files Browse the repository at this point in the history
Task/flag sets
  • Loading branch information
nmayorsplit authored Nov 29, 2023
2 parents b72bd9b + aab3ed3 commit 89ed66a
Show file tree
Hide file tree
Showing 13 changed files with 638 additions and 68 deletions.
10 changes: 10 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
6.5.0 (Nov 29, 2023)
- Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation):
- Added new variations of the get treatment methods to support evaluating flags in given flag set/s.
- TreatmentsByFlagSet and TreatmentsByFlagSets
- TreatmentsWithConfigByFlagSet and TreatmentsWithConfigByFlagSets
- Added a new optional Flag Sets Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload.
- Note: Only applicable when the SDK is in charge of the rollout data synchronization. When not applicable, the SDK will log a warning on init.
- Updated the following SDK manager methods to expose flag sets on flag views.
- Added `DefaultTreatment` property to the `SplitView` object returned by the `Split` and `Splits` functions of the SDK manager.

6.4.0 (Jul 18, 2023)
- Improved streaming architecture implementation to apply feature flag updates from the notification received which is now enhanced, improving efficiency and reliability of the whole update system.
- Pointed to new version of go-split-commons v5.0.0.
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ module github.com/splitio/go-client/v6
go 1.18

require (
github.com/splitio/go-split-commons/v5 v5.0.0
github.com/splitio/go-toolkit/v5 v5.3.1
github.com/splitio/go-split-commons/v5 v5.1.0
github.com/splitio/go-toolkit/v5 v5.3.2
)

require (
Expand All @@ -13,6 +13,7 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/redis/go-redis/v9 v9.0.4 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/sync v0.3.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/redis/go-redis/v9 v9.0.4 h1:FC82T+CHJ/Q/PdyLW++GeCO+Ol59Y4T7R4jbgjvktgc=
github.com/redis/go-redis/v9 v9.0.4/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/splitio/go-split-commons/v5 v5.0.0 h1:bGRi0cf1JP5VNSi0a4BPQEWv/DACkeSKliazhPMVDPk=
github.com/splitio/go-split-commons/v5 v5.0.0/go.mod h1:lzoVmYJaCqB8UPSxWva0BZe7fF+bRJD+eP0rNi/lL7c=
github.com/splitio/go-toolkit/v5 v5.3.1 h1:9J/byd0fRxWj5/Zg0QZOnUxKBDIAMCGr7rySYzJKdJg=
github.com/splitio/go-toolkit/v5 v5.3.1/go.mod h1:xYhUvV1gga9/1029Wbp5pjnR6Cy8nvBpjw99wAbsMko=
github.com/splitio/go-split-commons/v5 v5.1.0 h1:mki1235gjXwuxcXdv/bKVduX1Lv09uXJogds+BspqSM=
github.com/splitio/go-split-commons/v5 v5.1.0/go.mod h1:9vAZrlhKvhensyRC11hyVFdgLIBrkX9D5vdYc9qB13w=
github.com/splitio/go-toolkit/v5 v5.3.2 h1:Yy9YBcHRmK5WVZjeA/klLGEdF38xpsL1ejnC3ro8a2M=
github.com/splitio/go-toolkit/v5 v5.3.2/go.mod h1:xYhUvV1gga9/1029Wbp5pjnR6Cy8nvBpjw99wAbsMko=
github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg=
github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Expand Down
170 changes: 150 additions & 20 deletions splitio/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package client

import (
"errors"
"fmt"
"runtime/debug"
"strings"
"time"

"github.com/splitio/go-client/v6/splitio/conf"
Expand All @@ -11,17 +13,22 @@ import (
"github.com/splitio/go-split-commons/v5/dtos"
"github.com/splitio/go-split-commons/v5/engine/evaluator"
"github.com/splitio/go-split-commons/v5/engine/evaluator/impressionlabels"
"github.com/splitio/go-split-commons/v5/flagsets"
"github.com/splitio/go-split-commons/v5/provisional"
"github.com/splitio/go-split-commons/v5/storage"
"github.com/splitio/go-split-commons/v5/telemetry"
"github.com/splitio/go-toolkit/v5/logging"
)

const (
treatment = "Treatment"
treatments = "Treatments"
treatmentWithConfig = "TreatmentWithConfig"
treatmentsWithConfig = "TreatmentsWithConfig"
treatment = "Treatment"
treatments = "Treatments"
treatmentsByFlagSet = "TreatmentsByFlagSet"
treatmentsByFlagSets = "TreatmentsByFlahSets"
treatmentWithConfig = "TreatmentWithConfig"
treatmentsWithConfig = "TreatmentsWithConfig"
treatmentsWithConfigByFlagSet = "TreatmentsWithConfigByFlagSet"
treatmentsWithConfigByFlagSets = "TrearmentsWithConfigByFlagSets"
)

// SplitClient is the entry-point of the split SDK.
Expand All @@ -37,6 +44,7 @@ type SplitClient struct {
initTelemetry storage.TelemetryConfigProducer
evaluationTelemetry storage.TelemetryEvaluationProducer
runtimeTelemetry storage.TelemetryRuntimeProducer
flagSetsFilter flagsets.FlagSetFilter
}

// TreatmentResult struct that includes the Treatment evaluation with the corresponding Config
Expand All @@ -50,7 +58,8 @@ func (c *SplitClient) getEvaluationResult(matchingKey string, bucketingKey *stri
if c.isReady() {
return c.evaluator.EvaluateFeature(matchingKey, bucketingKey, featureFlag, attributes)
}
c.logger.Warning(operation + ": the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method")

c.logger.Warning(fmt.Sprintf("%s: the SDK is not ready, results may be incorrect for feature flag %s. Make sure to wait for SDK readiness before using this method", operation, featureFlag))
c.initTelemetry.RecordNonReadyUsage()
return &evaluator.Result{
Treatment: evaluator.Control,
Expand All @@ -64,7 +73,8 @@ func (c *SplitClient) getEvaluationsResult(matchingKey string, bucketingKey *str
if c.isReady() {
return c.evaluator.EvaluateFeatures(matchingKey, bucketingKey, featureFlags, attributes)
}
c.logger.Warning(operation + ": the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method")
featureFlagsToPrint := strings.Join(featureFlags, ", ")
c.logger.Warning(fmt.Sprintf("%s: the SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", operation, featureFlagsToPrint))
c.initTelemetry.RecordNonReadyUsage()
result := evaluator.Results{
EvaluationTime: 0,
Expand Down Expand Up @@ -207,6 +217,28 @@ func (c *SplitClient) generateControlTreatments(featureFlagNames []string, opera
return treatments
}

func (c *SplitClient) processResult(result evaluator.Results, operation string, bucketingKey *string, matchingKey string, attributes map[string]interface{}, metricsLabel string) (t map[string]TreatmentResult) {
var bulkImpressions []dtos.Impression
treatments := make(map[string]TreatmentResult)
for feature, evaluation := range result.Evaluations {
if !c.validator.IsSplitFound(evaluation.Label, feature, operation) {
treatments[feature] = TreatmentResult{
Treatment: evaluator.Control,
Config: nil,
}
} else {
bulkImpressions = append(bulkImpressions, c.createImpression(feature, bucketingKey, evaluation.Label, matchingKey, evaluation.Treatment, evaluation.SplitChangeNumber))

treatments[feature] = TreatmentResult{
Treatment: evaluation.Treatment,
Config: evaluation.Config,
}
}
}
c.storeData(bulkImpressions, attributes, metricsLabel, result.EvaluationTime)
return treatments
}

// doTreatmentsCall retrieves treatments of an specific array of feature flag names with configurations object if it is present for a certain key and set of attributes
func (c *SplitClient) doTreatmentsCall(key interface{}, featureFlagNames []string, attributes map[string]interface{}, operation string, metricsLabel string) (t map[string]TreatmentResult) {
treatments := make(map[string]TreatmentResult)
Expand Down Expand Up @@ -241,26 +273,44 @@ func (c *SplitClient) doTreatmentsCall(key interface{}, featureFlagNames []strin
return map[string]TreatmentResult{}
}

var bulkImpressions []dtos.Impression
evaluationsResult := c.getEvaluationsResult(matchingKey, bucketingKey, filteredFeatures, attributes, operation)
for feature, evaluation := range evaluationsResult.Evaluations {
if !c.validator.IsSplitFound(evaluation.Label, feature, operation) {
treatments[feature] = TreatmentResult{
Treatment: evaluator.Control,
Config: nil,
}
} else {
bulkImpressions = append(bulkImpressions, c.createImpression(feature, bucketingKey, evaluation.Label, matchingKey, evaluation.Treatment, evaluation.SplitChangeNumber))

treatments[feature] = TreatmentResult{
Treatment: evaluation.Treatment,
Config: evaluation.Config,
}
treatments = c.processResult(evaluationsResult, operation, bucketingKey, matchingKey, attributes, metricsLabel)

return treatments
}

// doTreatmentsCallByFlagSets retrieves treatments of a specific array of feature flag names, that belong to flag sets, with configurations object if it is present for a certain key and set of attributes
func (c *SplitClient) doTreatmentsCallByFlagSets(key interface{}, flagSets []string, attributes map[string]interface{}, operation string, metricsLabel string) (t map[string]TreatmentResult) {
treatments := make(map[string]TreatmentResult)

// Set up a guard deferred function to recover if the SDK starts panicking
defer func() {
if r := recover(); r != nil {
// At this point we'll only trust that the logger isn't panicking trust
// that the logger isn't panicking
c.evaluationTelemetry.RecordException(metricsLabel)
c.logger.Error(
"SDK is panicking with the following error", r, "\n",
string(debug.Stack()), "\n")
t = treatments
}
}()

if c.isDestroyed() {
return treatments
}

c.storeData(bulkImpressions, attributes, metricsLabel, evaluationsResult.EvaluationTime)
matchingKey, bucketingKey, err := c.validator.ValidateTreatmentKey(key, operation)
if err != nil {
c.logger.Error(err.Error())
return treatments
}

if c.isReady() {
evaluationsResult := c.evaluator.EvaluateFeatureByFlagSets(matchingKey, bucketingKey, flagSets, attributes)
treatments = c.processResult(evaluationsResult, operation, bucketingKey, matchingKey, attributes, metricsLabel)
}
return treatments
}

Expand All @@ -274,11 +324,91 @@ func (c *SplitClient) Treatments(key interface{}, featureFlagNames []string, att
return treatmentsResult
}

func (c *SplitClient) validateSets(flagSets []string) []string {
if len(flagSets) == 0 {
c.logger.Warning("sets must be a non-empty array")
return nil
}
flagSets, errs := flagsets.SanitizeMany(flagSets)
if len(errs) != 0 {
for _, err := range errs {
if errType, ok := err.(*dtos.FlagSetValidatonError); ok {
c.logger.Warning(errType.Message)
}
}
}
flagSets = c.filterSetsAreInConfig(flagSets)
if len(flagSets) == 0 {
return nil
}
return flagSets
}

// Treatments evaluate multiple feature flag names belonging to a flag set for a single user and a set of attributes at once
func (c *SplitClient) TreatmentsByFlagSet(key interface{}, flagSet string, attributes map[string]interface{}) map[string]string {
treatmentsResult := map[string]string{}
sets := c.validateSets([]string{flagSet})
if sets == nil {
return treatmentsResult
}
result := c.doTreatmentsCallByFlagSets(key, sets, attributes, treatmentsByFlagSet, telemetry.TreatmentsByFlagSet)
for feature, treatmentResult := range result {
treatmentsResult[feature] = treatmentResult.Treatment
}
return treatmentsResult
}

// Treatments evaluate multiple feature flag names belonging to flag sets for a single user and a set of attributes at once
func (c *SplitClient) TreatmentsByFlagSets(key interface{}, flagSets []string, attributes map[string]interface{}) map[string]string {
treatmentsResult := map[string]string{}
flagSets = c.validateSets(flagSets)
if flagSets == nil {
return treatmentsResult
}
result := c.doTreatmentsCallByFlagSets(key, flagSets, attributes, treatmentsByFlagSets, telemetry.TreatmentsByFlagSets)
for feature, treatmentResult := range result {
treatmentsResult[feature] = treatmentResult.Treatment
}
return treatmentsResult
}

func (c *SplitClient) filterSetsAreInConfig(flagSets []string) []string {
toReturn := []string{}
for _, flagSet := range flagSets {
if !c.flagSetsFilter.IsPresent(flagSet) {
c.logger.Warning(fmt.Sprintf("you passed %s which is not part of the configured FlagSetsFilter, ignoring Flag Set.", flagSet))
continue
}
toReturn = append(toReturn, flagSet)
}
return toReturn
}

// TreatmentsWithConfig evaluates multiple feature flag names for a single user and set of attributes at once and returns configurations
func (c *SplitClient) TreatmentsWithConfig(key interface{}, featureFlagNames []string, attributes map[string]interface{}) map[string]TreatmentResult {
return c.doTreatmentsCall(key, featureFlagNames, attributes, treatmentsWithConfig, telemetry.TreatmentsWithConfig)
}

// TreatmentsWithConfigByFlagSet evaluates multiple feature flag names belonging to a flag set for a single user and set of attributes at once and returns configurations
func (c *SplitClient) TreatmentsWithConfigByFlagSet(key interface{}, flagSet string, attributes map[string]interface{}) map[string]TreatmentResult {
treatmentsResult := make(map[string]TreatmentResult)
sets := c.validateSets([]string{flagSet})
if sets == nil {
return treatmentsResult
}
return c.doTreatmentsCallByFlagSets(key, sets, attributes, treatmentsWithConfigByFlagSet, telemetry.TreatmentsByFlagSets)
}

// TreatmentsWithConfigByFlagSet evaluates multiple feature flag names belonging to a flag sets for a single user and set of attributes at once and returns configurations
func (c *SplitClient) TreatmentsWithConfigByFlagSets(key interface{}, flagSets []string, attributes map[string]interface{}) map[string]TreatmentResult {
treatmentsResult := make(map[string]TreatmentResult)
flagSets = c.validateSets(flagSets)
if flagSets == nil {
return treatmentsResult
}
return c.doTreatmentsCallByFlagSets(key, flagSets, attributes, treatmentsWithConfigByFlagSets, telemetry.TreatmentsByFlagSets)
}

// isDestroyed returns true if the client has been destroyed
func (c *SplitClient) isDestroyed() bool {
return c.factory.IsDestroyed()
Expand Down
Loading

0 comments on commit 89ed66a

Please sign in to comment.