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-9120] Impression per toggle implementation #225

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ module github.com/splitio/go-client/v6
go 1.18

require (
github.com/splitio/go-split-commons/v6 v6.0.0
github.com/splitio/go-split-commons/v6 v6.0.3-0.20241230154236-0290e3993f69
github.com/splitio/go-toolkit/v5 v5.4.0
)

require (
github.com/bits-and-blooms/bitset v1.3.1 // indirect
github.com/bits-and-blooms/bloom/v3 v3.3.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/redis/go-redis/v9 v9.0.4 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // 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
Expand Down
14 changes: 12 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,24 @@ github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/v6 v6.0.0 h1:qenr5qbXafjvM832C64CVpjtlShuQiWCwtR5I2h4ogM=
github.com/splitio/go-split-commons/v6 v6.0.0/go.mod h1:TsvIh3XP7yjc7ly4vpj06AkoBND36SodPs5qfhb8rHc=
github.com/splitio/go-split-commons/v6 v6.0.3-0.20241212182204-626e0eae79d0 h1:8udmFWKfa/vPyMSLqupxwHkXv1DxJum77XXY0AeNQZE=
github.com/splitio/go-split-commons/v6 v6.0.3-0.20241212182204-626e0eae79d0/go.mod h1:D/XIY/9Hmfk9ivWsRsJVp439kEdmHbzUi3PKzQQDOXY=
github.com/splitio/go-split-commons/v6 v6.0.3-0.20241230154236-0290e3993f69 h1:6jmMkAAP0mHWFSju2b5UBr/IwwC5JD2yCLO2WbcK0nc=
github.com/splitio/go-split-commons/v6 v6.0.3-0.20241230154236-0290e3993f69/go.mod h1:D/XIY/9Hmfk9ivWsRsJVp439kEdmHbzUi3PKzQQDOXY=
github.com/splitio/go-toolkit/v5 v5.4.0 h1:g5WFpRhQomnXCmvfsNOWV4s5AuUrWIZ+amM68G8NBKM=
github.com/splitio/go-toolkit/v5 v5.4.0/go.mod h1:xYhUvV1gga9/1029Wbp5pjnR6Cy8nvBpjw99wAbsMko=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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=
Expand Down
38 changes: 25 additions & 13 deletions splitio/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type SplitClient struct {
validator inputValidation
factory *SplitFactory
impressionListener *impressionlistener.WrapperImpressionListener
impressionManager provisional.ImpressionManager
impressionManager *provisional.ImpressionManagerImpl
sanzmauro marked this conversation as resolved.
Show resolved Hide resolved
initTelemetry storage.TelemetryConfigProducer
evaluationTelemetry storage.TelemetryEvaluationProducer
runtimeTelemetry storage.TelemetryRuntimeProducer
Expand All @@ -62,9 +62,10 @@ func (c *SplitClient) getEvaluationResult(matchingKey string, bucketingKey *stri
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,
Label: impressionlabels.ClientNotReady,
Config: nil,
Treatment: evaluator.Control,
Label: impressionlabels.ClientNotReady,
Config: nil,
ImpressionsDisabled: false,
}
}

Expand All @@ -82,9 +83,10 @@ func (c *SplitClient) getEvaluationsResult(matchingKey string, bucketingKey *str
}
for _, featureFlag := range featureFlags {
result.Evaluations[featureFlag] = evaluator.Result{
Treatment: evaluator.Control,
Label: impressionlabels.ClientNotReady,
Config: nil,
Treatment: evaluator.Control,
Label: impressionlabels.ClientNotReady,
Config: nil,
ImpressionsDisabled: false,
}
}
return result
Expand Down Expand Up @@ -114,14 +116,16 @@ func (c *SplitClient) createImpression(featureFlag string, bucketingKey *string,
}

// storeData stores impression, runs listener and stores metrics
func (c *SplitClient) storeData(impressions []dtos.Impression, attributes map[string]interface{}, metricsLabel string, evaluationTime time.Duration) {
func (c *SplitClient) storeData(impressions []dtos.ImpressionDecorated, attributes map[string]interface{}, metricsLabel string, evaluationTime time.Duration) {
// Store impression
if c.impressions != nil {
forLog, forListener := c.impressionManager.ProcessImpressions(impressions)
listenerEnabled := c.impressionListener != nil

forLog, forListener := c.impressionManager.Process(impressions, listenerEnabled)
c.impressions.LogImpressions(forLog)

// Custom Impression Listener
if c.impressionListener != nil {
if listenerEnabled {
c.impressionListener.SendDataToClient(forListener, attributes)
}
} else {
Expand Down Expand Up @@ -177,7 +181,12 @@ func (c *SplitClient) doTreatmentCall(key interface{}, featureFlag string, attri
}

c.storeData(
[]dtos.Impression{c.createImpression(featureFlag, bucketingKey, evaluationResult.Label, matchingKey, evaluationResult.Treatment, evaluationResult.SplitChangeNumber)},
[]dtos.ImpressionDecorated{
{
Impression: c.createImpression(featureFlag, bucketingKey, evaluationResult.Label, matchingKey, evaluationResult.Treatment, evaluationResult.SplitChangeNumber),
Disabled: evaluationResult.ImpressionsDisabled,
},
},
attributes,
metricsLabel,
evaluationResult.EvaluationTime,
Expand Down Expand Up @@ -218,7 +227,7 @@ func (c *SplitClient) generateControlTreatments(featureFlagNames []string, opera
}

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
var bulkImpressions []dtos.ImpressionDecorated
treatments := make(map[string]TreatmentResult)
for feature, evaluation := range result.Evaluations {
if !c.validator.IsSplitFound(evaluation.Label, feature, operation) {
Expand All @@ -227,7 +236,10 @@ 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))
bulkImpressions = append(bulkImpressions, dtos.ImpressionDecorated{
Impression: c.createImpression(feature, bucketingKey, evaluation.Label, matchingKey, evaluation.Treatment, evaluation.SplitChangeNumber),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createImpressionDecorated for both?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Disabled: evaluation.ImpressionsDisabled,
})

treatments[feature] = TreatmentResult{
Treatment: evaluation.Treatment,
Expand Down
111 changes: 105 additions & 6 deletions splitio/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func getFactory() SplitFactory {
impressionObserver, _ := strategy.NewImpressionObserver(500)
impressionsCounter := strategy.NewImpressionsCounter()
impressionsStrategy := strategy.NewOptimizedImpl(impressionObserver, impressionsCounter, telemetryStorage, false)
impressionManager := provisional.NewImpressionManager(impressionsStrategy)
impressionManager := provisional.NewImpressionManager(impressionsStrategy).(*provisional.ImpressionManagerImpl)

return SplitFactory{
cfg: cfg,
Expand All @@ -166,7 +166,7 @@ func getFactoryByFlagSets() SplitFactory {
impressionObserver, _ := strategy.NewImpressionObserver(500)
impressionsCounter := strategy.NewImpressionsCounter()
impressionsStrategy := strategy.NewOptimizedImpl(impressionObserver, impressionsCounter, telemetryStorage, false)
impressionManager := provisional.NewImpressionManager(impressionsStrategy)
impressionManager := provisional.NewImpressionManager(impressionsStrategy).(*provisional.ImpressionManagerImpl)

return SplitFactory{
cfg: cfg,
Expand Down Expand Up @@ -473,7 +473,7 @@ func TestClientPanicking(t *testing.T) {
impressionObserver, _ := strategy.NewImpressionObserver(500)
impressionsCounter := strategy.NewImpressionsCounter()
impressionsStrategy := strategy.NewOptimizedImpl(impressionObserver, impressionsCounter, telemetryMockedStorage, false)
impressionManager := provisional.NewImpressionManager(impressionsStrategy)
impressionManager := provisional.NewImpressionManager(impressionsStrategy).(*provisional.ImpressionManagerImpl)

factory := SplitFactory{
cfg: cfg,
Expand Down Expand Up @@ -650,7 +650,7 @@ func getClientForListener() SplitClient {
impressionObserver, _ := strategy.NewImpressionObserver(500)
impressionsCounter := strategy.NewImpressionsCounter()
impressionsStrategy := strategy.NewOptimizedImpl(impressionObserver, impressionsCounter, telemetryMockedStorage, true)
impressionManager := provisional.NewImpressionManager(impressionsStrategy)
impressionManager := provisional.NewImpressionManager(impressionsStrategy).(*provisional.ImpressionManagerImpl)

factory := &SplitFactory{
cfg: cfg,
Expand Down Expand Up @@ -1372,7 +1372,7 @@ func TestClient(t *testing.T) {

impressionObserver, _ := strategy.NewImpressionObserver(500)
impressionsStrategy := strategy.NewDebugImpl(impressionObserver, true)
impressionManager := provisional.NewImpressionManager(impressionsStrategy)
impressionManager := provisional.NewImpressionManager(impressionsStrategy).(*provisional.ImpressionManagerImpl)

factory := &SplitFactory{cfg: cfg, impressionManager: impressionManager}
client := SplitClient{
Expand Down Expand Up @@ -2114,6 +2114,105 @@ func TestClientDebug(t *testing.T) {
}
}

func TestClientDebugWithImpressionsDisabledTrue(t *testing.T) {
var isDestroyCalled = false
var splitsMock, _ = ioutil.ReadFile("../../testdata/splits_mock_2.json")

postChannel := make(chan string, 1)
var count int64
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/splitChanges":
fmt.Fprintln(w, string(splitsMock))
return
case "/testImpressions/bulk":
if r.Header.Get("SplitSDKImpressionsMode") != commonsCfg.ImpressionsModeDebug {
t.Error("Wrong header")
}

if isDestroyCalled {
rBody, _ := ioutil.ReadAll(r.Body)
var dataInPost []map[string]interface{}
err := json.Unmarshal(rBody, &dataInPost)
if err != nil {
t.Error(err)
return
}
if len(dataInPost) != 1 {
t.Error("It should send two impressions in optimized mode. Actual: ", len(dataInPost))
}
if len(dataInPost[0]["i"].([]interface{})) != 3 {
t.Error("It should send only one impression per featureName")
}
}

fmt.Fprintln(w, "ok")
postChannel <- "finished"
case "/testImpressions/count":
atomic.AddInt64(&count, 1)
case "/events/bulk":
fmt.Fprintln(w, "ok")
case "/segmentChanges":
fallthrough
default:
fmt.Fprintln(w, "ok")
}
}))
defer ts.Close()

impTest := &ImpressionListenerTest{}
cfg := conf.Default()
cfg.LabelsEnabled = true
cfg.Advanced.EventsURL = ts.URL
cfg.Advanced.SdkURL = ts.URL
cfg.Advanced.TelemetryServiceURL = ts.URL
cfg.Advanced.AuthServiceURL = ts.URL
cfg.Advanced.ImpressionListener = impTest
cfg.ImpressionsMode = "Debug"

factory, _ := NewSplitFactory("test", cfg)
client := factory.Client()
client.BlockUntilReady(2)

// Calls treatments to generate one valid impression
time.Sleep(300 * time.Millisecond) // Let's wait until first call of recorders have finished
client.Treatment("user1", "DEMO_MURMUR2", nil)
impL1, _ := ilResult["DEMO_MURMUR2"].(map[string]interface{})
if impL1["Pt"].(int64) != 0 {
t.Error("Pt should be 0")
}
client.Treatment("user1", "DEMO_MURMUR2", nil)
impL2, _ := ilResult["DEMO_MURMUR2"].(map[string]interface{})
if impL2["Pt"] != impL1["Time"] {
t.Error("Pt should be equal to previos imp1")
}
client.Treatments("user1", []string{"DEMO_MURMUR2"}, nil)
impL3, _ := ilResult["DEMO_MURMUR2"].(map[string]interface{})
if impL3["Pt"] != impL2["Time"] {
t.Error("Pt should be equal to previos imp2")
}

client.Treatment("user1", "IMPRESSION_TOGGLE_FLAG", nil)
impL4, _ := ilResult["IMPRESSION_TOGGLE_FLAG"].(map[string]interface{})
if impL4["Pt"].(int64) != 0 {
t.Error("Pt should be 0")
}

isDestroyCalled = true
client.Destroy()

select {
case <-postChannel:
if atomic.LoadInt64(&count) != 1 {
t.Error("Impression Count should be 1. Actual: ", atomic.LoadInt64(&count))
}
return
case <-time.After(4 * time.Second):
t.Error("The test couldn't send impressions to check headers")
return
}
}

func TestUnsupportedMatcherAndSemver(t *testing.T) {
var isDestroyCalled = false
var splitsMock, _ = ioutil.ReadFile("../../testdata/splits_mock_3.json")
Expand Down Expand Up @@ -2357,7 +2456,7 @@ func TestTelemetryMemory(t *testing.T) {
t.Error("It should queue one impression")
}
if dataInPost.ImpressionsDeduped != 1 {
t.Error("It should dedupe one impression")
t.Error("It should dedupe one impression. ", dataInPost.ImpressionsDeduped)
}
if dataInPost.EventsQueued != 1 {
t.Error("It should queue one event")
Expand Down
Loading
Loading