Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDKS-7679] #203

Merged
merged 7 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.0.1-0.20231114174555-e7fa17527a05
github.com/splitio/go-toolkit/v5 v5.3.2-0.20231106173125-49e72b9823dc
)

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.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=
Expand Down
163 changes: 145 additions & 18 deletions splitio/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package client

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

Expand All @@ -11,17 +12,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 +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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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{}, sets []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, sets, attributes)
treatments = c.processResult(evaluationsResult, operation, bucketingKey, matchingKey, attributes, metricsLabel)
}
return treatments
}

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

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

// 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{}, set string, attributes map[string]interface{}) map[string]string {
nmayorsplit marked this conversation as resolved.
Show resolved Hide resolved
treatmentsResult := map[string]string{}
sets := c.validateSets([]string{set})
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{}, sets []string, attributes map[string]interface{}) map[string]string {
treatmentsResult := map[string]string{}
sets = c.validateSets(sets)
if sets == nil {
return treatmentsResult
}
result := c.doTreatmentsCallByFlagSets(key, sets, attributes, treatmentsByFlagSets, telemetry.TreatmentsByFlagSets)
for feature, treatmentResult := range result {
treatmentsResult[feature] = treatmentResult.Treatment
}
return treatmentsResult
}

func (c *SplitClient) filterSetsAreInConfig(sets []string) []string {
toReturn := []string{}
for _, flagSet := range sets {
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{}, set string, attributes map[string]interface{}) map[string]TreatmentResult {
treatmentsResult := make(map[string]TreatmentResult)
sets := c.validateSets([]string{set})
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{}, sets []string, attributes map[string]interface{}) map[string]TreatmentResult {
treatmentsResult := make(map[string]TreatmentResult)
sets = c.validateSets(sets)
if sets == nil {
return treatmentsResult
}
return c.doTreatmentsCallByFlagSets(key, sets, 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