diff --git a/CHANGES.txt b/CHANGES.txt index c2e18cf..84ab00e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +6.10.0 (Nov 26, 2025) +- Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs. + 6.9.0 (Nov 26, 2025) - Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs. diff --git a/go.mod b/go.mod index b77c829..d04fdf6 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/splitio/go-client/v6 go 1.18 require ( - github.com/splitio/go-split-commons/v9 v9.0.0 + github.com/splitio/go-split-commons/v9 v9.1.0 github.com/splitio/go-toolkit/v5 v5.4.1 github.com/stretchr/testify v1.11.1 ) diff --git a/go.sum b/go.sum index 85200b0..ef7242b 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/v9 v9.0.0 h1:6uHEkBMUUZNhIiop9dyN04gXQUrMXp+X/0uXSytbp+Q= -github.com/splitio/go-split-commons/v9 v9.0.0/go.mod h1:gJuaKo04Swlh4w9C1b2jBAqAdFxEd/Vpd8jnFINOeDY= +github.com/splitio/go-split-commons/v9 v9.1.0 h1:sfmPMuEDTtbIOJ+MeWNbfYl2/xKB/25d4/J95OUD+X0= +github.com/splitio/go-split-commons/v9 v9.1.0/go.mod h1:gJuaKo04Swlh4w9C1b2jBAqAdFxEd/Vpd8jnFINOeDY= github.com/splitio/go-toolkit/v5 v5.4.1 h1:srTyvDBJZMUcJ/KiiQDMyjCuELVgTBh2TGRVn0sOXEE= github.com/splitio/go-toolkit/v5 v5.4.1/go.mod h1:SifzysrOVDbzMcOE8zjX02+FG5az4FrR3Us/i5SeStw= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= diff --git a/splitio/client/client.go b/splitio/client/client.go index 659a1f7..84bf4c7 100644 --- a/splitio/client/client.go +++ b/splitio/client/client.go @@ -1,6 +1,7 @@ package client import ( + "encoding/json" "errors" "fmt" "runtime/debug" @@ -54,6 +55,22 @@ type TreatmentResult struct { Config *string `json:"config"` } +type options struct { + evaluationOptions *dtos.EvaluationOptions +} + +type OptFn = func(o *options) + +func (c *SplitClient) WithEvaluationOptions(e *dtos.EvaluationOptions) OptFn { + return func(o *options) { o.evaluationOptions = e } +} + +func defaultOpts() options { + return options{ + evaluationOptions: nil, + } +} + // getEvaluationResult calls evaluation for one particular feature flag func (c *SplitClient) getEvaluationResult(matchingKey string, bucketingKey *string, featureFlag string, attributes map[string]interface{}, operation string) *evaluator.Result { if c.isReady() { @@ -95,7 +112,7 @@ func (c *SplitClient) getEvaluationsResult(matchingKey string, bucketingKey *str } // createImpression creates impression to be stored and used by listener -func (c *SplitClient) createImpression(featureFlag string, bucketingKey *string, evaluationLabel string, matchingKey string, treatment string, changeNumber int64, disabled bool) dtos.Impression { +func (c *SplitClient) createImpression(featureFlag string, bucketingKey *string, evaluationLabel string, matchingKey string, treatment string, changeNumber int64, disabled bool, properties string) dtos.Impression { var label string if c.factory.cfg.LabelsEnabled { label = evaluationLabel @@ -115,6 +132,7 @@ func (c *SplitClient) createImpression(featureFlag string, bucketingKey *string, Treatment: treatment, Time: time.Now().UTC().UnixNano() / int64(time.Millisecond), // Convert standard timestamp to java's ms timestamps Disabled: disabled, + Properties: properties, } } @@ -140,7 +158,7 @@ func (c *SplitClient) storeData(impressions []dtos.Impression, attributes map[st } // doTreatmentCall retrieves treatments of an specific feature flag with configurations object if it is present for a certain key and set of attributes -func (c *SplitClient) doTreatmentCall(key interface{}, featureFlag string, attributes map[string]interface{}, operation string, metricsLabel string) (t TreatmentResult) { +func (c *SplitClient) doTreatmentCall(key interface{}, featureFlag string, attributes map[string]interface{}, operation string, metricsLabel string, evaluationOptions *dtos.EvaluationOptions) (t TreatmentResult) { controlTreatment := TreatmentResult{ Treatment: evaluator.Control, Config: nil, @@ -184,7 +202,7 @@ func (c *SplitClient) doTreatmentCall(key interface{}, featureFlag string, attri } c.storeData( - []dtos.Impression{c.createImpression(featureFlag, bucketingKey, evaluationResult.Label, matchingKey, evaluationResult.Treatment, evaluationResult.SplitChangeNumber, evaluationResult.ImpressionsDisabled)}, + []dtos.Impression{c.createImpression(featureFlag, bucketingKey, evaluationResult.Label, matchingKey, evaluationResult.Treatment, evaluationResult.SplitChangeNumber, evaluationResult.ImpressionsDisabled, serializeProperties(evaluationOptions))}, attributes, metricsLabel, evaluationResult.EvaluationTime, @@ -196,16 +214,42 @@ func (c *SplitClient) doTreatmentCall(key interface{}, featureFlag string, attri } } +func serializeProperties(opts *dtos.EvaluationOptions) string { + if opts == nil { + return "" + } + if len(opts.Properties) == 0 { + return "" + } + + properties, err := json.Marshal(opts.Properties) + if err != nil { + return "" + } + + return string(properties) +} + // Treatment implements the main functionality of split. Retrieve treatments of a specific feature flag // for a certain key and set of attributes -func (c *SplitClient) Treatment(key interface{}, featureFlagName string, attributes map[string]interface{}) string { - return c.doTreatmentCall(key, featureFlagName, attributes, treatment, telemetry.Treatment).Treatment +func (c *SplitClient) Treatment(key interface{}, featureFlagName string, attributes map[string]interface{}, optFns ...OptFn) string { + options := getOptions(optFns...) + return c.doTreatmentCall(key, featureFlagName, attributes, treatment, telemetry.Treatment, options.evaluationOptions).Treatment +} + +func getOptions(optFns ...OptFn) options { + options := defaultOpts() + for _, optFn := range optFns { + optFn(&options) + } + return options } // TreatmentWithConfig implements the main functionality of split. Retrieves the treatment of a specific feature flag // with the corresponding configuration if it is present -func (c *SplitClient) TreatmentWithConfig(key interface{}, featureFlagName string, attributes map[string]interface{}) TreatmentResult { - return c.doTreatmentCall(key, featureFlagName, attributes, treatmentWithConfig, telemetry.TreatmentWithConfig) +func (c *SplitClient) TreatmentWithConfig(key interface{}, featureFlagName string, attributes map[string]interface{}, optFns ...OptFn) TreatmentResult { + options := getOptions(optFns...) + return c.doTreatmentCall(key, featureFlagName, attributes, treatmentWithConfig, telemetry.TreatmentWithConfig, options.evaluationOptions) } // Generates control treatments @@ -224,7 +268,7 @@ 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) { +func (c *SplitClient) processResult(result evaluator.Results, operation string, bucketingKey *string, matchingKey string, attributes map[string]interface{}, metricsLabel string, evaluationOptions *dtos.EvaluationOptions) (t map[string]TreatmentResult) { var bulkImpressions []dtos.Impression treatments := make(map[string]TreatmentResult) for feature, evaluation := range result.Evaluations { @@ -234,8 +278,7 @@ func (c *SplitClient) processResult(result evaluator.Results, operation string, Config: nil, } } else { - bulkImpressions = append(bulkImpressions, c.createImpression(feature, bucketingKey, evaluation.Label, matchingKey, evaluation.Treatment, evaluation.SplitChangeNumber, evaluation.ImpressionsDisabled)) - + bulkImpressions = append(bulkImpressions, c.createImpression(feature, bucketingKey, evaluation.Label, matchingKey, evaluation.Treatment, evaluation.SplitChangeNumber, evaluation.ImpressionsDisabled, serializeProperties(evaluationOptions))) treatments[feature] = TreatmentResult{ Treatment: evaluation.Treatment, Config: evaluation.Config, @@ -247,7 +290,7 @@ func (c *SplitClient) processResult(result evaluator.Results, operation string, } // 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) { +func (c *SplitClient) doTreatmentsCall(key interface{}, featureFlagNames []string, attributes map[string]interface{}, operation string, metricsLabel string, evaluationOptions *dtos.EvaluationOptions) (t map[string]TreatmentResult) { // Set up a guard deferred function to recover if the SDK starts panicking defer func() { if r := recover(); r != nil { @@ -280,11 +323,11 @@ func (c *SplitClient) doTreatmentsCall(key interface{}, featureFlagNames []strin evaluationsResult := c.getEvaluationsResult(matchingKey, bucketingKey, filteredFeatures, attributes, operation) - return c.processResult(evaluationsResult, operation, bucketingKey, matchingKey, attributes, metricsLabel) + return c.processResult(evaluationsResult, operation, bucketingKey, matchingKey, attributes, metricsLabel, evaluationOptions) } // 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) { +func (c *SplitClient) doTreatmentsCallByFlagSets(key interface{}, flagSets []string, attributes map[string]interface{}, operation string, metricsLabel string, evaluationOptions *dtos.EvaluationOptions) (t map[string]TreatmentResult) { treatments := make(map[string]TreatmentResult) // Set up a guard deferred function to recover if the SDK starts panicking @@ -312,15 +355,16 @@ func (c *SplitClient) doTreatmentsCallByFlagSets(key interface{}, flagSets []str if c.isReady() { evaluationsResult := c.evaluator.EvaluateFeatureByFlagSets(matchingKey, bucketingKey, flagSets, attributes) - treatments = c.processResult(evaluationsResult, operation, bucketingKey, matchingKey, attributes, metricsLabel) + treatments = c.processResult(evaluationsResult, operation, bucketingKey, matchingKey, attributes, metricsLabel, evaluationOptions) } return treatments } // Treatments evaluates multiple feature flag names for a single user and set of attributes at once -func (c *SplitClient) Treatments(key interface{}, featureFlagNames []string, attributes map[string]interface{}) map[string]string { +func (c *SplitClient) Treatments(key interface{}, featureFlagNames []string, attributes map[string]interface{}, optFns ...OptFn) map[string]string { + options := getOptions(optFns...) treatmentsResult := map[string]string{} - result := c.doTreatmentsCall(key, featureFlagNames, attributes, treatments, telemetry.Treatments) + result := c.doTreatmentsCall(key, featureFlagNames, attributes, treatments, telemetry.Treatments, options.evaluationOptions) for feature, treatmentResult := range result { treatmentsResult[feature] = treatmentResult.Treatment } @@ -348,13 +392,14 @@ func (c *SplitClient) validateSets(flagSets []string) []string { } // 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 { +func (c *SplitClient) TreatmentsByFlagSet(key interface{}, flagSet string, attributes map[string]interface{}, optFns ...OptFn) map[string]string { + options := getOptions(optFns...) treatmentsResult := map[string]string{} sets := c.validateSets([]string{flagSet}) if sets == nil { return treatmentsResult } - result := c.doTreatmentsCallByFlagSets(key, sets, attributes, treatmentsByFlagSet, telemetry.TreatmentsByFlagSet) + result := c.doTreatmentsCallByFlagSets(key, sets, attributes, treatmentsByFlagSet, telemetry.TreatmentsByFlagSet, options.evaluationOptions) for feature, treatmentResult := range result { treatmentsResult[feature] = treatmentResult.Treatment } @@ -362,13 +407,14 @@ func (c *SplitClient) TreatmentsByFlagSet(key interface{}, flagSet string, attri } // 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 { +func (c *SplitClient) TreatmentsByFlagSets(key interface{}, flagSets []string, attributes map[string]interface{}, optFns ...OptFn) map[string]string { + options := getOptions(optFns...) treatmentsResult := map[string]string{} flagSets = c.validateSets(flagSets) if flagSets == nil { return treatmentsResult } - result := c.doTreatmentsCallByFlagSets(key, flagSets, attributes, treatmentsByFlagSets, telemetry.TreatmentsByFlagSets) + result := c.doTreatmentsCallByFlagSets(key, flagSets, attributes, treatmentsByFlagSets, telemetry.TreatmentsByFlagSets, options.evaluationOptions) for feature, treatmentResult := range result { treatmentsResult[feature] = treatmentResult.Treatment } @@ -388,28 +434,31 @@ func (c *SplitClient) filterSetsAreInConfig(flagSets []string) []string { } // 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) +func (c *SplitClient) TreatmentsWithConfig(key interface{}, featureFlagNames []string, attributes map[string]interface{}, optFns ...OptFn) map[string]TreatmentResult { + options := getOptions(optFns...) + return c.doTreatmentsCall(key, featureFlagNames, attributes, treatmentsWithConfig, telemetry.TreatmentsWithConfig, options.evaluationOptions) } // 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 { +func (c *SplitClient) TreatmentsWithConfigByFlagSet(key interface{}, flagSet string, attributes map[string]interface{}, optFns ...OptFn) map[string]TreatmentResult { + options := getOptions(optFns...) treatmentsResult := make(map[string]TreatmentResult) sets := c.validateSets([]string{flagSet}) if sets == nil { return treatmentsResult } - return c.doTreatmentsCallByFlagSets(key, sets, attributes, treatmentsWithConfigByFlagSet, telemetry.TreatmentsWithConfigByFlagSet) + return c.doTreatmentsCallByFlagSets(key, sets, attributes, treatmentsWithConfigByFlagSet, telemetry.TreatmentsWithConfigByFlagSet, options.evaluationOptions) } // 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 { +func (c *SplitClient) TreatmentsWithConfigByFlagSets(key interface{}, flagSets []string, attributes map[string]interface{}, optFns ...OptFn) map[string]TreatmentResult { + options := getOptions(optFns...) treatmentsResult := make(map[string]TreatmentResult) flagSets = c.validateSets(flagSets) if flagSets == nil { return treatmentsResult } - return c.doTreatmentsCallByFlagSets(key, flagSets, attributes, treatmentsWithConfigByFlagSets, telemetry.TreatmentsWithConfigByFlagSets) + return c.doTreatmentsCallByFlagSets(key, flagSets, attributes, treatmentsWithConfigByFlagSets, telemetry.TreatmentsWithConfigByFlagSets, options.evaluationOptions) } // isDestroyed returns true if the client has been destroyed diff --git a/splitio/client/client_test.go b/splitio/client/client_test.go index 7cfaf06..13c6eb0 100644 --- a/splitio/client/client_test.go +++ b/splitio/client/client_test.go @@ -18,6 +18,7 @@ import ( "github.com/splitio/go-client/v6/splitio/conf" impressionlistener "github.com/splitio/go-client/v6/splitio/impressionListener" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" commonsCfg "github.com/splitio/go-split-commons/v9/conf" "github.com/splitio/go-split-commons/v9/dtos" @@ -229,31 +230,52 @@ func TestClientGetTreatment(t *testing.T) { } } +func TestClientGetTreatmentWithEvaluationProperties(t *testing.T) { + factory := getFactory() + client := factory.Client() + client.evaluator = &mockEvaluator{} + factory.status.Store(sdkStatusReady) + opts := dtos.EvaluationOptions{ + Properties: map[string]interface{}{ + "userId": "123", + "age": 30, + "premium": true, + "balance": 99.5, + }, + } + expectedTreatment(client.Treatment("key", "feature", nil, client.WithEvaluationOptions(&opts)), "TreatmentA", t) + impressionsQueue := client.impressions.(storage.ImpressionStorage) + impressions, _ := impressionsQueue.PopN(5000) + impression := impressions[0] + + assert.Equal(t, "aLabel", impression.Label, "Impression should have label when labelsEnabled is true") + assert.Equal(t, "{\"age\":30,\"balance\":99.5,\"premium\":true,\"userId\":\"123\"}", impression.Properties, "Should have properties") + + client.factory.cfg.LabelsEnabled = false + expectedTreatment(client.Treatment("key", "feature2", nil), "TreatmentB", t) + + impressions, _ = impressionsQueue.PopN(5000) + impression = impressions[0] + assert.Equal(t, "", impression.Label, "Impression should have label when labelsEnabled is true") +} + 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 - }, + evaluatorMock := evaluatorMock.MockEvaluator{} + results := evaluator.Results{ + Evaluations: make(map[string]evaluator.Result), + EvaluationTime: 0, + } + results.Evaluations["feature"] = evaluator.Result{ + EvaluationTime: 0, + Label: "aLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentA", } + evaluatorMock.On("EvaluateFeatureByFlagSets", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Once().Return(results) + client.evaluator = evaluatorMock + factory.status.Store(sdkStatusReady) res := client.TreatmentsByFlagSet("user1", "set1", nil) @@ -261,38 +283,68 @@ func TestClientGetTreatmentByFlagSet(t *testing.T) { expectedTreatment(res["feature"], "TreatmentA", t) } +func TestClientGetTreatmentByFlagSetWithEvaluationProperties(t *testing.T) { + factory := getFactoryByFlagSets() + client := factory.Client() + evaluatorMock := evaluatorMock.MockEvaluator{} + results := evaluator.Results{ + Evaluations: make(map[string]evaluator.Result), + EvaluationTime: 0, + } + results.Evaluations["feature"] = evaluator.Result{ + EvaluationTime: 0, + Label: "aLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentA", + } + evaluatorMock.On("EvaluateFeatureByFlagSets", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Once().Return(results) + client.evaluator = evaluatorMock + + factory.status.Store(sdkStatusReady) + + opts := dtos.EvaluationOptions{ + Properties: map[string]interface{}{ + "userId": "123", + "age": 30, + "premium": true, + "balance": 99.5, + }, + } + + res := client.TreatmentsByFlagSet("user1", "set1", nil, client.WithEvaluationOptions(&opts)) + + expectedTreatment(res["feature"], "TreatmentA", t) + impressionsQueue := client.impressions.(storage.ImpressionStorage) + impressions, _ := impressionsQueue.PopN(5000) + impression := impressions[0] + + assert.Equal(t, "aLabel", impression.Label, "Impression should have label when labelsEnabled is true") + assert.Equal(t, "{\"age\":30,\"balance\":99.5,\"premium\":true,\"userId\":\"123\"}", impression.Properties, "Should have properties") +} + 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 - }, + evaluatorMock := evaluatorMock.MockEvaluator{} + + results := evaluator.Results{ + Evaluations: make(map[string]evaluator.Result), + EvaluationTime: 0, } + results.Evaluations["feature"] = evaluator.Result{ + EvaluationTime: 0, + Label: "aLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentA", + } + results.Evaluations["feature2"] = evaluator.Result{ + EvaluationTime: 0, + Label: "bLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentB", + } + evaluatorMock.On("EvaluateFeatureByFlagSets", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Once().Return(results) + client.evaluator = evaluatorMock factory.status.Store(sdkStatusReady) res := client.TreatmentsByFlagSets("user1", []string{"set1", "set2"}, nil) @@ -301,31 +353,71 @@ func TestClientGetTreatmentByFlagSets(t *testing.T) { expectedTreatment(res["feature2"], "TreatmentB", t) } -func TestClientGetTreatmentWithConfigByFlagSet(t *testing.T) { +func TestClientGetTreatmentByFlagSetsWithEvaluationProperties(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 + evaluatorMock := evaluatorMock.MockEvaluator{} + + results := evaluator.Results{ + Evaluations: make(map[string]evaluator.Result), + EvaluationTime: 0, + } + results.Evaluations["feature"] = evaluator.Result{ + EvaluationTime: 0, + Label: "aLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentA", + } + results.Evaluations["feature2"] = evaluator.Result{ + EvaluationTime: 0, + Label: "bLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentB", + } + evaluatorMock.On("EvaluateFeatureByFlagSets", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Once().Return(results) + client.evaluator = evaluatorMock + factory.status.Store(sdkStatusReady) + + opts := dtos.EvaluationOptions{ + Properties: map[string]interface{}{ + "userId": "123", + "age": 30, + "premium": true, + "balance": 99.5, }, } + + res := client.TreatmentsByFlagSets("user1", []string{"set1", "set2"}, nil, client.WithEvaluationOptions(&opts)) + + expectedTreatment(res["feature"], "TreatmentA", t) + expectedTreatment(res["feature2"], "TreatmentB", t) + + impressionsQueue := client.impressions.(storage.ImpressionStorage) + impressions, _ := impressionsQueue.PopN(5000) + for i := range impressions { + if impressions[i].FeatureName == "feature" { + assert.Equal(t, "aLabel", impressions[i].Label, "Impression should have label when labelsEnabled is true") + assert.Equal(t, "{\"age\":30,\"balance\":99.5,\"premium\":true,\"userId\":\"123\"}", impressions[i].Properties, "Should have properties") + } + } +} + +func TestClientGetTreatmentWithConfigByFlagSet(t *testing.T) { + factory := getFactory() + client := factory.Client() + evaluatorMock := evaluatorMock.MockEvaluator{} + results := evaluator.Results{ + Evaluations: make(map[string]evaluator.Result), + EvaluationTime: 0, + } + results.Evaluations["feature"] = evaluator.Result{ + EvaluationTime: 0, + Label: "aLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentA", + } + evaluatorMock.On("EvaluateFeatureByFlagSets", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Once().Return(results) + client.evaluator = evaluatorMock factory.status.Store(sdkStatusReady) res := client.TreatmentsWithConfigByFlagSet("user1", "set1", nil) @@ -333,38 +425,67 @@ func TestClientGetTreatmentWithConfigByFlagSet(t *testing.T) { expectedTreatment(res["feature"].Treatment, "TreatmentA", t) } -func TestClientGetTreatmentWithConfigByFlagSets(t *testing.T) { +func TestClientGetTreatmentWithConfigByFlagSetAndEvaluationProperties(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 + evaluatorMock := evaluatorMock.MockEvaluator{} + results := evaluator.Results{ + Evaluations: make(map[string]evaluator.Result), + EvaluationTime: 0, + } + results.Evaluations["feature"] = evaluator.Result{ + EvaluationTime: 0, + Label: "aLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentA", + } + evaluatorMock.On("EvaluateFeatureByFlagSets", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Once().Return(results) + client.evaluator = evaluatorMock + factory.status.Store(sdkStatusReady) + opts := dtos.EvaluationOptions{ + Properties: map[string]interface{}{ + "userId": "123", + "age": 30, + "premium": true, + "balance": 99.5, }, } + + res := client.TreatmentsWithConfigByFlagSet("user1", "set1", nil, client.WithEvaluationOptions(&opts)) + + expectedTreatment(res["feature"].Treatment, "TreatmentA", t) + impressionsQueue := client.impressions.(storage.ImpressionStorage) + impressions, _ := impressionsQueue.PopN(5000) + for i := range impressions { + if impressions[i].FeatureName == "feature" { + assert.Equal(t, "aLabel", impressions[i].Label, "Impression should have label when labelsEnabled is true") + assert.Equal(t, "{\"age\":30,\"balance\":99.5,\"premium\":true,\"userId\":\"123\"}", impressions[i].Properties, "Should have properties") + } + } +} + +func TestClientGetTreatmentWithConfigByFlagSets(t *testing.T) { + factory := getFactory() + client := factory.Client() + evaluatorMock := evaluatorMock.MockEvaluator{} + results := evaluator.Results{ + Evaluations: make(map[string]evaluator.Result), + EvaluationTime: 0, + } + results.Evaluations["feature"] = evaluator.Result{ + EvaluationTime: 0, + Label: "aLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentA", + } + results.Evaluations["feature2"] = evaluator.Result{ + EvaluationTime: 0, + Label: "bLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentB", + } + evaluatorMock.On("EvaluateFeatureByFlagSets", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Once().Return(results) + client.evaluator = evaluatorMock factory.status.Store(sdkStatusReady) res := client.TreatmentsWithConfigByFlagSets("user1", []string{"set1", "set2"}, nil) @@ -373,6 +494,52 @@ func TestClientGetTreatmentWithConfigByFlagSets(t *testing.T) { expectedTreatment(res["feature2"].Treatment, "TreatmentB", t) } +func TestClientGetTreatmentWithConfigByFlagSetsAndEvaluationOptions(t *testing.T) { + factory := getFactory() + client := factory.Client() + evaluatorMock := evaluatorMock.MockEvaluator{} + results := evaluator.Results{ + Evaluations: make(map[string]evaluator.Result), + EvaluationTime: 0, + } + results.Evaluations["feature"] = evaluator.Result{ + EvaluationTime: 0, + Label: "aLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentA", + } + results.Evaluations["feature2"] = evaluator.Result{ + EvaluationTime: 0, + Label: "bLabel", + SplitChangeNumber: 123, + Treatment: "TreatmentB", + } + evaluatorMock.On("EvaluateFeatureByFlagSets", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Once().Return(results) + client.evaluator = evaluatorMock + factory.status.Store(sdkStatusReady) + opts := dtos.EvaluationOptions{ + Properties: map[string]interface{}{ + "userId": "123", + "age": 30, + "premium": true, + "balance": 99.5, + }, + } + + res := client.TreatmentsWithConfigByFlagSets("user1", []string{"set1", "set2"}, nil, client.WithEvaluationOptions(&opts)) + + expectedTreatment(res["feature"].Treatment, "TreatmentA", t) + expectedTreatment(res["feature2"].Treatment, "TreatmentB", t) + impressionsQueue := client.impressions.(storage.ImpressionStorage) + impressions, _ := impressionsQueue.PopN(5000) + for i := range impressions { + if impressions[i].FeatureName == "feature" { + assert.Equal(t, "aLabel", impressions[i].Label, "Impression should have label when labelsEnabled is true") + assert.Equal(t, "{\"age\":30,\"balance\":99.5,\"premium\":true,\"userId\":\"123\"}", impressions[i].Properties, "Should have properties") + } + } +} + func TestTreatments(t *testing.T) { factory := getFactory() client := factory.Client() @@ -385,6 +552,36 @@ func TestTreatments(t *testing.T) { expectedTreatment(res["notFeature"], evaluator.Control, t) } +func TestTreatmentsWithEvaluationOptions(t *testing.T) { + factory := getFactory() + client := factory.Client() + client.evaluator = &mockEvaluator{} + factory.status.Store(sdkStatusReady) + + opts := dtos.EvaluationOptions{ + Properties: map[string]interface{}{ + "userId": "123", + "age": 30, + "premium": true, + "balance": 99.5, + }, + } + + res := client.Treatments("user1", []string{"feature", "notFeature"}, nil, client.WithEvaluationOptions(&opts)) + + expectedTreatment(res["feature"], "TreatmentA", t) + expectedTreatment(res["notFeature"], evaluator.Control, t) + + impressionsQueue := client.impressions.(storage.ImpressionStorage) + impressions, _ := impressionsQueue.PopN(5000) + for i := range impressions { + if impressions[i].FeatureName == "feature" { + assert.Equal(t, "aLabel", impressions[i].Label, "Impression should have label when labelsEnabled is true") + assert.Equal(t, "{\"age\":30,\"balance\":99.5,\"premium\":true,\"userId\":\"123\"}", impressions[i].Properties, "Should have properties") + } + } +} + func TestLocalhostMode(t *testing.T) { file, err := ioutil.TempFile("", "splitio_tests") if err != nil { @@ -490,14 +687,12 @@ func TestClientPanicking(t *testing.T) { } client := factory.Client() - client.evaluator = evaluatorMock.MockEvaluator{ - EvaluateFeatureCall: func(key string, bucketingKey *string, feature string, attributes map[string]interface{}) *evaluator.Result { - panic("Testing panicking") - }, - EvaluateFeaturesCall: func(key string, bucketingKey *string, features []string, attributes map[string]interface{}) evaluator.Results { - panic("Testing panicking") - }, - } + evaluatorMock := evaluatorMock.MockEvaluator{} + evaluatorMock.On("EvaluateFeatureByFlagSets", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Once().Return(evaluator.Results{ + Evaluations: make(map[string]evaluator.Result), + EvaluationTime: 0, + }) + client.evaluator = evaluatorMock factory.status.Store(sdkStatusReady) expectedTreatment(client.Treatment("key", "some", nil), evaluator.Control, t) @@ -612,11 +807,12 @@ func (i *ImpressionListenerTest) LogImpression(data impressionlistener.ILObject) ilTest["Version"] = data.SDKLanguageVersion ilTest["InstanceName"] = data.InstanceID ilTest["Pt"] = data.Impression.Pt + ilTest["Properties"] = data.Impression.Properties ilResult[data.Impression.FeatureName] = ilTest } -func compareListener(ilTest map[string]interface{}, f string, k string, l string, t string, c int64, b string, a string, i string, v string) bool { +func compareListener(ilTest map[string]interface{}, f string, k string, l string, t string, c int64, b string, a string, i string, v string, p string) bool { if ilTest["FeatureName"] != f || ilTest["KeyName"] != k || ilTest["Label"] != l || ilTest["Treatment"] != t || ilTest["ChangeNumber"] != c || ilTest["BucketingKey"] != b { return false } @@ -626,6 +822,9 @@ func compareListener(ilTest map[string]interface{}, f string, k string, l string if ilTest["InstanceName"] != i { return false } + if ilTest["Properties"] != p { + return false + } attr1, _ := ilTest["Attributes"].(map[string]interface{}) return attr1["One"] == a } @@ -690,7 +889,7 @@ func TestImpressionListener(t *testing.T) { expectedTreatment(client.Treatment("user1", "feature", attributes), "TreatmentA", t) expectedVersion := "go-" + splitio.Version - if !compareListener(ilResult["feature"].(map[string]interface{}), "feature", "user1", "aLabel", "TreatmentA", int64(123), "", "test", "ip-123-123-123-123", expectedVersion) { + if !compareListener(ilResult["feature"].(map[string]interface{}), "feature", "user1", "aLabel", "TreatmentA", int64(123), "", "test", "ip-123-123-123-123", expectedVersion, "") { t.Error("Impression should match") } ilResult = make(map[string]interface{}) @@ -715,11 +914,49 @@ func TestImpressionListenerForTreatments(t *testing.T) { expectedVersion := "go-" + splitio.Version - if !compareListener(ilResult["feature"].(map[string]interface{}), "feature", "user1", "aLabel", "TreatmentA", int64(123), "", "test", "ip-123-123-123-123", expectedVersion) { + if !compareListener(ilResult["feature"].(map[string]interface{}), "feature", "user1", "aLabel", "TreatmentA", int64(123), "", "test", "ip-123-123-123-123", expectedVersion, "") { t.Error("Impression should match") } - if !compareListener(ilResult["feature2"].(map[string]interface{}), "feature2", "user1", "bLabel", "TreatmentB", int64(123), "", "test", "ip-123-123-123-123", expectedVersion) { + if !compareListener(ilResult["feature2"].(map[string]interface{}), "feature2", "user1", "bLabel", "TreatmentB", int64(123), "", "test", "ip-123-123-123-123", expectedVersion, "") { + t.Error("Impression should match") + } + ilResult = make(map[string]interface{}) + + delete(ilResult, "feature") + delete(ilResult, "feature2") +} + +func TestImpressionListenerForTreatmentsWithEvaluationOptions(t *testing.T) { + client := getClientForListener() + + attributes := make(map[string]interface{}) + attributes["One"] = "test" + opts := dtos.EvaluationOptions{ + Properties: map[string]interface{}{ + "userId": "123", + "age": 30, + "premium": true, + "balance": 99.5, + }, + } + + res := client.Treatments("user1", []string{"feature", "feature2"}, attributes, client.WithEvaluationOptions(&opts)) + + expectedTreatment(res["feature"], "TreatmentA", t) + expectedTreatment(res["feature2"], "TreatmentB", t) + + if len(ilResult) != 2 { + t.Error("Error on ImpressionListener") + } + + expectedVersion := "go-" + splitio.Version + + if !compareListener(ilResult["feature"].(map[string]interface{}), "feature", "user1", "aLabel", "TreatmentA", int64(123), "", "test", "ip-123-123-123-123", expectedVersion, "{\"age\":30,\"balance\":99.5,\"premium\":true,\"userId\":\"123\"}") { + t.Error("Impression should match") + } + + if !compareListener(ilResult["feature2"].(map[string]interface{}), "feature2", "user1", "bLabel", "TreatmentB", int64(123), "", "test", "ip-123-123-123-123", expectedVersion, "{\"age\":30,\"balance\":99.5,\"premium\":true,\"userId\":\"123\"}") { t.Error("Impression should match") } ilResult = make(map[string]interface{}) @@ -946,6 +1183,7 @@ func TestBlockUntilReadyInMemoryError(t *testing.T) { "", "test", sdkConf.InstanceName, expectedVersion, + "", ) { t.Error("Impression should match") } @@ -1108,13 +1346,13 @@ func TestBlockUntilReadyInMemoryOk(t *testing.T) { } expectedTreatment(client.Treatment("not_ready2", "not_ready2", attributes), evaluator.Control, t) - if !compareListener(ilResult["not_ready2"].(map[string]interface{}), "not_ready2", "not_ready2", "not ready", "control", int64(0), "", "test", sdkConf.InstanceName, expectedVersion) { + if !compareListener(ilResult["not_ready2"].(map[string]interface{}), "not_ready2", "not_ready2", "not ready", "control", int64(0), "", "test", sdkConf.InstanceName, expectedVersion, "") { t.Error("Impression should match") } result := client.Treatments("not_ready3", []string{"not_ready3"}, attributes) expectedTreatment(result["not_ready3"], evaluator.Control, t) - if !compareListener(ilResult["not_ready3"].(map[string]interface{}), "not_ready3", "not_ready3", "not ready", "control", int64(0), "", "test", sdkConf.InstanceName, expectedVersion) { + if !compareListener(ilResult["not_ready3"].(map[string]interface{}), "not_ready3", "not_ready3", "not ready", "control", int64(0), "", "test", sdkConf.InstanceName, expectedVersion, "") { t.Error("Impression should match") } ilResult = make(map[string]interface{}) diff --git a/splitio/version.go b/splitio/version.go index cc3782f..5527fcc 100644 --- a/splitio/version.go +++ b/splitio/version.go @@ -1,4 +1,4 @@ package splitio // Version contains a string with the split sdk version -const Version = "6.9.0" +const Version = "6.10.0"