diff --git a/go.mod b/go.mod index 499d039..cd4331f 100644 --- a/go.mod +++ b/go.mod @@ -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.0.1-0.20231114174555-e7fa17527a05 + github.com/splitio/go-toolkit/v5 v5.3.2-0.20231106173125-49e72b9823dc ) require ( @@ -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 ) diff --git a/go.sum b/go.sum index 917f006..907f417 100644 --- a/go.sum +++ b/go.sum @@ -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.0.1-0.20231114174555-e7fa17527a05 h1:rDWd6xVhU/XCmWH28+8CnWkJOZ+8yoKxm2+rbX3HahI= +github.com/splitio/go-split-commons/v5 v5.0.1-0.20231114174555-e7fa17527a05/go.mod h1:PSkBLDXQW7NAhZ7JO1va7QJyTeDvpE7MEDnTdn5evRM= +github.com/splitio/go-toolkit/v5 v5.3.2-0.20231106173125-49e72b9823dc h1:14jdJE/rBEYfs1CO8kOQrj/8azszRFU4yw5FQIGpoJg= +github.com/splitio/go-toolkit/v5 v5.3.2-0.20231106173125-49e72b9823dc/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= diff --git a/splitio/client/client.go b/splitio/client/client.go index 813c790..e4dfd39 100644 --- a/splitio/client/client.go +++ b/splitio/client/client.go @@ -2,6 +2,7 @@ package client import ( "errors" + "fmt" "runtime/debug" "time" @@ -11,6 +12,7 @@ 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" @@ -18,10 +20,14 @@ import ( ) 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. @@ -37,6 +43,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 @@ -207,6 +214,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) @@ -241,26 +270,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 } @@ -274,11 +321,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() diff --git a/splitio/client/client_test.go b/splitio/client/client_test.go index e6bff95..46c9231 100644 --- a/splitio/client/client_test.go +++ b/splitio/client/client_test.go @@ -44,6 +44,11 @@ import ( type mockEvaluator struct{} +// EvaluateFeatureByFlagSets implements evaluator.Interface. +func (*mockEvaluator) EvaluateFeatureByFlagSets(key string, bucketingKey *string, flagSets []string, attributes map[string]interface{}) evaluator.Results { + panic("unimplemented") +} + func (e *mockEvaluator) EvaluateFeature( key string, bucketingKey *string, @@ -151,6 +156,32 @@ func getFactory() SplitFactory { } } +func getFactoryByFlagSets() SplitFactory { + telemetryStorage, _ := inmemory.NewTelemetryStorage() + cfg := conf.Default() + cfg.LabelsEnabled = true + cfg.Advanced.FlagSetFilter = []string{"set1", "set2"} + logger := logging.NewLogger(nil) + + impressionObserver, _ := strategy.NewImpressionObserver(500) + impressionsCounter := strategy.NewImpressionsCounter() + impressionsStrategy := strategy.NewOptimizedImpl(impressionObserver, impressionsCounter, telemetryStorage, false) + impressionManager := provisional.NewImpressionManager(impressionsStrategy) + + return SplitFactory{ + cfg: cfg, + storages: sdkStorages{ + impressions: mutexqueue.NewMQImpressionsStorage(cfg.Advanced.ImpressionsQueueSize, make(chan string, 1), logger, telemetryStorage), + events: mocks.MockEventStorage{}, + initTelemetry: telemetryStorage, + runtimeTelemetry: telemetryStorage, + evaluationTelemetry: telemetryStorage, + }, + impressionManager: impressionManager, + logger: logger, + } +} + func expectedTreatment(treatment string, expectedTreatment string, t *testing.T) { if treatment != expectedTreatment { t.Error("Expected: " + expectedTreatment + " actual: " + treatment) @@ -197,6 +228,150 @@ func TestClientGetTreatment(t *testing.T) { } } +func TestClientGetTreatmentByFlagSet(t *testing.T) { + factory := getFactoryByFlagSets() + client := factory.Client() + client.evaluator = evaluatorMock.MockEvaluator{ + EvaluateFeatureByFlagSetsCall: func(key string, bucketingKey *string, flagSets []string, attributes map[string]interface{}) evaluator.Results { + results := evaluator.Results{ + Evaluations: make(map[string]evaluator.Result), + EvaluationTime: 0, + } + for _, flagSet := range flagSets { + switch flagSet { + case "set1": + results.Evaluations["feature"] = evaluator.Result{ + EvaluationTime: 0, + Label: "aLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentA", + } + default: + t.Error("Should be set1 or set2") + } + } + return results + }, + } + factory.status.Store(sdkStatusReady) + + res := client.TreatmentsByFlagSet("user1", "set1", nil) + + expectedTreatment(res["feature"], "TreatmentA", t) +} + +func TestClientGetTreatmentByFlagSets(t *testing.T) { + factory := getFactory() + client := factory.Client() + client.evaluator = evaluatorMock.MockEvaluator{ + EvaluateFeatureByFlagSetsCall: func(key string, bucketingKey *string, flagSets []string, attributes map[string]interface{}) evaluator.Results { + results := evaluator.Results{ + Evaluations: make(map[string]evaluator.Result), + EvaluationTime: 0, + } + for _, flagSet := range flagSets { + switch flagSet { + case "set1": + results.Evaluations["feature"] = evaluator.Result{ + EvaluationTime: 0, + Label: "aLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentA", + } + case "set2": + results.Evaluations["feature2"] = evaluator.Result{ + EvaluationTime: 0, + Label: "bLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentB", + } + default: + t.Error("Should be set1 or set2") + } + } + return results + }, + } + factory.status.Store(sdkStatusReady) + + res := client.TreatmentsByFlagSets("user1", []string{"set1", "set2"}, nil) + + expectedTreatment(res["feature"], "TreatmentA", t) + expectedTreatment(res["feature2"], "TreatmentB", t) +} + +func TestClientGetTreatmentWithConfigByFlagSet(t *testing.T) { + factory := getFactory() + client := factory.Client() + client.evaluator = evaluatorMock.MockEvaluator{ + EvaluateFeatureByFlagSetsCall: func(key string, bucketingKey *string, flagSets []string, attributes map[string]interface{}) evaluator.Results { + results := evaluator.Results{ + Evaluations: make(map[string]evaluator.Result), + EvaluationTime: 0, + } + for _, flagSet := range flagSets { + switch flagSet { + case "set1": + results.Evaluations["feature"] = evaluator.Result{ + EvaluationTime: 0, + Label: "aLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentA", + } + default: + t.Error("Should be set1 or set2") + } + } + return results + }, + } + factory.status.Store(sdkStatusReady) + + res := client.TreatmentsWithConfigByFlagSet("user1", "set1", nil) + + expectedTreatment(res["feature"].Treatment, "TreatmentA", t) +} + +func TestClientGetTreatmentWithConfigByFlagSets(t *testing.T) { + factory := getFactory() + client := factory.Client() + client.evaluator = evaluatorMock.MockEvaluator{ + EvaluateFeatureByFlagSetsCall: func(key string, bucketingKey *string, flagSets []string, attributes map[string]interface{}) evaluator.Results { + results := evaluator.Results{ + Evaluations: make(map[string]evaluator.Result), + EvaluationTime: 0, + } + for _, flagSet := range flagSets { + switch flagSet { + case "set1": + results.Evaluations["feature"] = evaluator.Result{ + EvaluationTime: 0, + Label: "aLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentA", + } + case "set2": + results.Evaluations["feature2"] = evaluator.Result{ + EvaluationTime: 0, + Label: "bLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentB", + } + default: + t.Error("Should be set1 or set2") + } + } + return results + }, + } + factory.status.Store(sdkStatusReady) + + res := client.TreatmentsWithConfigByFlagSets("user1", []string{"set1", "set2"}, nil) + + expectedTreatment(res["feature"].Treatment, "TreatmentA", t) + expectedTreatment(res["feature2"].Treatment, "TreatmentB", t) +} + func TestTreatments(t *testing.T) { factory := getFactory() client := factory.Client() diff --git a/splitio/client/factory.go b/splitio/client/factory.go index aca1750..48d35ed 100644 --- a/splitio/client/factory.go +++ b/splitio/client/factory.go @@ -18,6 +18,7 @@ import ( "github.com/splitio/go-split-commons/v5/dtos" "github.com/splitio/go-split-commons/v5/engine" "github.com/splitio/go-split-commons/v5/engine/evaluator" + "github.com/splitio/go-split-commons/v5/flagsets" "github.com/splitio/go-split-commons/v5/healthcheck/application" "github.com/splitio/go-split-commons/v5/provisional" "github.com/splitio/go-split-commons/v5/provisional/strategy" @@ -283,13 +284,16 @@ func setupInMemoryFactory( logger logging.LoggerInterface, metadata dtos.Metadata, ) (*SplitFactory, error) { - advanced := conf.NormalizeSDKConf(cfg.Advanced) + advanced, warnings := conf.NormalizeSDKConf(cfg.Advanced) + printWarnings(logger, warnings) if strings.TrimSpace(cfg.SplitSyncProxyURL) != "" { advanced.StreamingEnabled = false } inMememoryFullQueue := make(chan string, 2) // Size 2: So that it's able to accept one event from each resource simultaneously. - splitsStorage := mutexmap.NewMMSplitStorage() + + flagSetFilter := flagsets.NewFlagSetFilter(advanced.FlagSetsFilter) + splitsStorage := mutexmap.NewMMSplitStorage(flagSetFilter) segmentsStorage := mutexmap.NewMMSegmentStorage() telemetryStorage, err := inmemory.NewTelemetryStorage() impressionsStorage := mutexqueue.NewMQImpressionsStorage(cfg.Advanced.ImpressionsQueueSize, inMememoryFullQueue, logger, telemetryStorage) @@ -302,7 +306,7 @@ func setupInMemoryFactory( splitAPI := api.NewSplitAPI(apikey, advanced, logger, metadata) workers := synchronizer.Workers{ - SplitUpdater: split.NewSplitUpdater(splitsStorage, splitAPI.SplitFetcher, logger, telemetryStorage, dummyHC), + SplitUpdater: split.NewSplitUpdater(splitsStorage, splitAPI.SplitFetcher, logger, telemetryStorage, dummyHC, flagSetFilter), SegmentUpdater: segment.NewSegmentUpdater(splitsStorage, segmentsStorage, splitAPI.SegmentFetcher, logger, telemetryStorage, dummyHC), EventRecorder: event.NewEventRecorderSingle(eventsStorage, splitAPI.EventRecorder, logger, metadata, telemetryStorage), TelemetryRecorder: telemetry.NewTelemetrySynchronizer(telemetryStorage, splitAPI.TelemetryRecorder, splitsStorage, segmentsStorage, logger, metadata, telemetryStorage), @@ -396,8 +400,13 @@ func setupRedisFactory(apikey string, cfg *conf.SplitSdkConfig, logger logging.L } inMememoryFullQueue := make(chan string, 2) // Size 2: So that it's able to accept one event from each resource simultaneously. impressionStorage := redis.NewImpressionStorage(redisClient, metadata, logger) + + flagSets, errs := flagsets.SanitizeMany(cfg.Advanced.FlagSetFilter) + printWarnings(logger, errs) + flagSetFilter := flagsets.NewFlagSetFilter(flagSets) + storages := sdkStorages{ - splits: redis.NewSplitStorage(redisClient, logger), + splits: redis.NewSplitStorage(redisClient, logger, flagSetFilter), segments: redis.NewSegmentStorage(redisClient, logger), impressionsConsumer: impressionStorage, impressions: impressionStorage, @@ -458,7 +467,10 @@ func setupLocalhostFactory( logger logging.LoggerInterface, metadata dtos.Metadata, ) (*SplitFactory, error) { - splitStorage := mutexmap.NewMMSplitStorage() + flagSets, errs := flagsets.SanitizeMany(cfg.Advanced.FlagSetFilter) + printWarnings(logger, errs) + flagSetFilter := flagsets.NewFlagSetFilter(flagSets) + splitStorage := mutexmap.NewMMSplitStorage(flagSetFilter) segmentStorage := mutexmap.NewMMSegmentStorage() telemetryStorage, err := inmemory.NewTelemetryStorage() if err != nil { @@ -640,3 +652,13 @@ func buildImpressionManager( return provisional.NewImpressionManager(impressionsStrategy), nil } } + +func printWarnings(logger logging.LoggerInterface, errs []error) { + if len(errs) != 0 { + for _, err := range errs { + if errType, ok := err.(dtos.FlagSetValidatonError); ok { + logger.Warning(errType.Message) + } + } + } +} diff --git a/splitio/client/factory_test.go b/splitio/client/factory_test.go new file mode 100644 index 0000000..a751260 --- /dev/null +++ b/splitio/client/factory_test.go @@ -0,0 +1,35 @@ +package client + +import ( + "testing" + + "github.com/splitio/go-split-commons/v5/flagsets" +) + +func TestPrintWarnings(t *testing.T) { + + flagSets, warnings := flagsets.SanitizeMany([]string{"set1", " set2"}) + if len(flagSets) != 2 { + t.Error("flag set size should be 2") + } + printWarnings(getMockedLogger(), warnings) + if !mW.Matches("Flag Set name set2 has extra whitespace, trimming") { + t.Error("Wrong message") + } + flagSets, warnings = flagsets.SanitizeMany([]string{"set1", "Set2"}) + if len(flagSets) != 2 { + t.Error("flag set size should be 2") + } + printWarnings(getMockedLogger(), warnings) + if !mW.Matches("Flag Set name Set2 should be all lowercase - converting string to lowercase") { + t.Error("Wrong message") + } + flagSets, warnings = flagsets.SanitizeMany([]string{"set1", "@set4"}) + if len(flagSets) != 1 { + t.Error("flag set size should be 1") + } + printWarnings(getMockedLogger(), warnings) + if !mW.Matches("you passed @set4, Flag Set must adhere to the regular expressions ^[a-z0-9][_a-z0-9]{0,49}$. This means a Flag Set must start with a letter or number, be in lowercase, alphanumeric and have a max length of 50 characters. @set4 was discarded.") { + t.Error("Wrong message") + } +} diff --git a/splitio/client/input_validator_test.go b/splitio/client/input_validator_test.go index 4f41539..73e401a 100644 --- a/splitio/client/input_validator_test.go +++ b/splitio/client/input_validator_test.go @@ -12,6 +12,7 @@ import ( "github.com/splitio/go-client/v6/splitio/conf" spConf "github.com/splitio/go-split-commons/v5/conf" "github.com/splitio/go-split-commons/v5/dtos" + "github.com/splitio/go-split-commons/v5/flagsets" "github.com/splitio/go-split-commons/v5/healthcheck/application" "github.com/splitio/go-split-commons/v5/provisional" "github.com/splitio/go-split-commons/v5/provisional/strategy" @@ -542,11 +543,12 @@ func TestNotReadyYet(t *testing.T) { initTelemetry: telemetryStorage, evaluationTelemetry: telemetryStorage, } + flagSetFilter := flagsets.NewFlagSetFilter([]string{}) maganerNotReady := SplitManager{ initTelemetry: telemetryStorage, factory: factoryNotReady, logger: logger, - splitStorage: mutexmap.NewMMSplitStorage(), + splitStorage: mutexmap.NewMMSplitStorage(flagSetFilter), } factoryNotReady.status.Store(sdkStatusInitializing) @@ -597,7 +599,8 @@ func TestNotReadyYet(t *testing.T) { } func TestManagerWithEmptySplit(t *testing.T) { - splitStorage := mutexmap.NewMMSplitStorage() + flagSetFilter := flagsets.NewFlagSetFilter([]string{}) + splitStorage := mutexmap.NewMMSplitStorage(flagSetFilter) factory := SplitFactory{} manager := SplitManager{ splitStorage: splitStorage, diff --git a/splitio/client/manager_test.go b/splitio/client/manager_test.go index d95df99..12d03ac 100644 --- a/splitio/client/manager_test.go +++ b/splitio/client/manager_test.go @@ -4,13 +4,15 @@ import ( "testing" "github.com/splitio/go-split-commons/v5/dtos" + "github.com/splitio/go-split-commons/v5/flagsets" "github.com/splitio/go-split-commons/v5/storage/inmemory/mutexmap" "github.com/splitio/go-toolkit/v5/datastructures/set" "github.com/splitio/go-toolkit/v5/logging" ) func TestSplitManager(t *testing.T) { - splitStorage := mutexmap.NewMMSplitStorage() + flagSetFilter := flagsets.NewFlagSetFilter([]string{}) + splitStorage := mutexmap.NewMMSplitStorage(flagSetFilter) splitStorage.Update([]dtos.SplitDTO{ { ChangeNumber: 123, @@ -89,7 +91,8 @@ func TestSplitManager(t *testing.T) { } func TestSplitManagerWithConfigs(t *testing.T) { - splitStorage := mutexmap.NewMMSplitStorage() + flagSetFilter := flagsets.NewFlagSetFilter([]string{}) + splitStorage := mutexmap.NewMMSplitStorage(flagSetFilter) splitStorage.Update([]dtos.SplitDTO{*valid, *killed, *noConfig}, nil, 123) logger := logging.NewLogger(nil) diff --git a/splitio/conf/sdkconf.go b/splitio/conf/sdkconf.go index e94cb76..1f3ab62 100644 --- a/splitio/conf/sdkconf.go +++ b/splitio/conf/sdkconf.go @@ -94,6 +94,7 @@ type AdvancedConfig struct { ImpressionsQueueSize int ImpressionsBulkSize int64 StreamingEnabled bool + FlagSetFilter []string } // Default returns a config struct with all the default values diff --git a/splitio/conf/util.go b/splitio/conf/util.go index 5e89f55..378d4e9 100644 --- a/splitio/conf/util.go +++ b/splitio/conf/util.go @@ -4,10 +4,11 @@ import ( "strings" "github.com/splitio/go-split-commons/v5/conf" + "github.com/splitio/go-split-commons/v5/flagsets" ) // NormalizeSDKConf compares against SDK Config to set defaults -func NormalizeSDKConf(sdkConfig AdvancedConfig) conf.AdvancedConfig { +func NormalizeSDKConf(sdkConfig AdvancedConfig) (conf.AdvancedConfig, []error) { config := conf.GetDefaultAdvancedConfig() if sdkConfig.HTTPTimeout > 0 { config.HTTPTimeout = sdkConfig.HTTPTimeout @@ -47,5 +48,7 @@ func NormalizeSDKConf(sdkConfig AdvancedConfig) conf.AdvancedConfig { } config.StreamingEnabled = sdkConfig.StreamingEnabled - return config + flagSets, errs := flagsets.SanitizeMany(sdkConfig.FlagSetFilter) + config.FlagSetsFilter = flagSets + return config, errs }