From caa2d0fee52adb412cd5c0d844ef668e17151108 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Mon, 27 Jul 2020 18:33:02 +0500 Subject: [PATCH 1/2] feat: Add user context --- pkg/client/client.go | 34 ++++++++++++++++++++++++++ pkg/client/client_test.go | 40 +++++++++++++++++++++++++++++++ pkg/decision/entities.go | 21 ++++++++++++++++ pkg/entities/decide_option.go | 34 ++++++++++++++++++++++++++ pkg/entities/user_context.go | 12 +++++++--- pkg/entities/user_context_test.go | 8 ++++++- 6 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 pkg/entities/decide_option.go diff --git a/pkg/client/client.go b/pkg/client/client.go index 26eafe2d6..64eff5865 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -24,6 +24,7 @@ import ( "reflect" "runtime/debug" "strconv" + "sync" "github.com/optimizely/go-sdk/pkg/config" "github.com/optimizely/go-sdk/pkg/decision" @@ -44,7 +45,40 @@ type OptimizelyClient struct { EventProcessor event.Processor notificationCenter notification.Center execGroup *utils.ExecGroup + userContext entities.UserContext logger logging.OptimizelyLogProducer + lock sync.Mutex +} + +// SetUserContext sets user context to be used by decision apis +func (o *OptimizelyClient) SetUserContext(user entities.UserContext) (err error) { + + defer func() { + if r := recover(); r != nil { + switch t := r.(type) { + case error: + err = t + case string: + err = errors.New(t) + default: + err = errors.New("unexpected error") + } + errorMessage := fmt.Sprintf("SetUserContext call, optimizely SDK is panicking with the error:") + o.logger.Error(errorMessage, err) + o.logger.Debug(string(debug.Stack())) + } + }() + + if _, err := o.getProjectConfig(); err != nil { + o.logger.Error("Error retrieving ProjectConfig", err) + return err + } + + o.lock.Lock() + defer o.lock.Unlock() + o.userContext = user + + return nil } // Activate returns the key of the variation the user is bucketed into and queues up an impression event to be sent to diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index bdbe6bc2c..91de7092b 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -171,6 +171,46 @@ func (TestConfig) GetClientVersion() string { return "1.0.0" } +func TestSetUserContext(t *testing.T) { + client := OptimizelyClient{ + ConfigManager: ValidProjectConfigManager(), + } + + userContext := entities.UserContext{ID: "1212121", Attributes: map[string]interface{}{}, DefaultDecideOptions: []entities.OptimizelyDecideOption{entities.DisableTracking}} + err := client.SetUserContext(userContext) + assert.NoError(t, err) + assert.Equal(t, userContext, client.userContext) +} + +func TestSetUserContextReplace(t *testing.T) { + client := OptimizelyClient{ + ConfigManager: ValidProjectConfigManager(), + } + + userContext := entities.UserContext{ID: "1212121", Attributes: map[string]interface{}{}, DefaultDecideOptions: []entities.OptimizelyDecideOption{entities.DisableTracking}} + err := client.SetUserContext(userContext) + assert.NoError(t, err) + assert.Equal(t, userContext, client.userContext) + + userContext2 := entities.UserContext{ID: "1212127", Attributes: map[string]interface{}{}, DefaultDecideOptions: []entities.OptimizelyDecideOption{entities.BypassUPS}} + err = client.SetUserContext(userContext2) + assert.NoError(t, err) + assert.Equal(t, userContext2, client.userContext) +} + +func TestSetUserContextWithoutConfig(t *testing.T) { + // ensure that we recover if the SDK panics + userContext := entities.UserContext{ID: "1212121", Attributes: map[string]interface{}{}, DefaultDecideOptions: []entities.OptimizelyDecideOption{entities.DisableTracking}} + client := OptimizelyClient{ + ConfigManager: new(PanickingConfigManager), + DecisionService: new(MockDecisionService), + logger: logging.GetLogger("", ""), + } + + err := client.SetUserContext(userContext) + assert.Error(t, err) +} + func TestTrack(t *testing.T) { mockProcessor := new(MockProcessor) mockDecisionService := new(MockDecisionService) diff --git a/pkg/decision/entities.go b/pkg/decision/entities.go index 3618912d0..0f939c576 100644 --- a/pkg/decision/entities.go +++ b/pkg/decision/entities.go @@ -21,8 +21,29 @@ import ( "github.com/optimizely/go-sdk/pkg/config" "github.com/optimizely/go-sdk/pkg/decision/reasons" "github.com/optimizely/go-sdk/pkg/entities" + "github.com/optimizely/go-sdk/pkg/optimizelyjson" ) +// OptimizelyDecision returns an optimizely api decision +type OptimizelyDecision struct { + VariationKey string + Enabled bool + Variables optimizelyjson.OptimizelyJSON + + Key string + User entities.UserContext + Reasons []reasons.Reason +} + +// ErrorDecision returns an error OptimizelyDecision +func ErrorDecision(key string, user entities.UserContext, reason reasons.Reason) OptimizelyDecision { + return OptimizelyDecision{ + Key: key, + User: user, + Reasons: []reasons.Reason{reason}, + } +} + // ExperimentDecisionContext contains the information needed to be able to make a decision for a given experiment type ExperimentDecisionContext struct { Experiment *entities.Experiment diff --git a/pkg/entities/decide_option.go b/pkg/entities/decide_option.go new file mode 100644 index 000000000..548b8e8d8 --- /dev/null +++ b/pkg/entities/decide_option.go @@ -0,0 +1,34 @@ +/**************************************************************************** + * Copyright 2020, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +// Package entities // +package entities + +// OptimizelyDecideOption represents Optimizely decide api options +type OptimizelyDecideOption int + +const ( + // DisableTracking when set, will not send decision (impression) event + DisableTracking OptimizelyDecideOption = iota + // EnabledOnly when set, will return decisions for enabled-flags only. + EnabledOnly + // BypassUPS when set, will bypass UPS (both lookup and save) for decision + BypassUPS + // ForExperiment when set, will specify that the key parameter(s) of decide and decideAll APIs should be for experiments (not flags) + ForExperiment + // IncludeReasons when set, will return decision debugging messages in reasons + IncludeReasons +) diff --git a/pkg/entities/user_context.go b/pkg/entities/user_context.go index df681988e..2d7fd26d2 100644 --- a/pkg/entities/user_context.go +++ b/pkg/entities/user_context.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -27,8 +27,14 @@ const bucketingIDAttributeName = "$opt_bucketing_id" // UserContext holds information about a user type UserContext struct { - ID string - Attributes map[string]interface{} + ID string + Attributes map[string]interface{} + DefaultDecideOptions []OptimizelyDecideOption +} + +// SetDefaultDecideOptions sets default decide options +func (u *UserContext) SetDefaultDecideOptions(options ...OptimizelyDecideOption) { + u.DefaultDecideOptions = append(u.DefaultDecideOptions, options...) } // CheckAttributeExists returns whether the specified attribute name exists in the attributes map. diff --git a/pkg/entities/user_context_test.go b/pkg/entities/user_context_test.go index da155c7ec..6f730db10 100644 --- a/pkg/entities/user_context_test.go +++ b/pkg/entities/user_context_test.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -23,6 +23,12 @@ import ( "github.com/stretchr/testify/assert" ) +func TestSetDefaultDecideOptions(t *testing.T) { + userContext := UserContext{} + userContext.SetDefaultDecideOptions(DisableTracking) + assert.Equal(t, []OptimizelyDecideOption{DisableTracking}, userContext.DefaultDecideOptions) +} + func TestUserAttributeExists(t *testing.T) { userContext := UserContext{ Attributes: map[string]interface{}{ From 530ec79b4d36d5ed7b55cabe48c9df194daf3de4 Mon Sep 17 00:00:00 2001 From: Yasir Ali Date: Tue, 28 Jul 2020 12:20:26 +0500 Subject: [PATCH 2/2] fixes. --- pkg/client/client.go | 4 ++-- pkg/client/client_test.go | 12 ++++++++---- pkg/entities/user_context.go | 16 ++++++++++++++++ pkg/entities/user_context_test.go | 7 ++++++- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index 64eff5865..72b8b81a5 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -45,13 +45,13 @@ type OptimizelyClient struct { EventProcessor event.Processor notificationCenter notification.Center execGroup *utils.ExecGroup - userContext entities.UserContext + userContext *entities.UserContext logger logging.OptimizelyLogProducer lock sync.Mutex } // SetUserContext sets user context to be used by decision apis -func (o *OptimizelyClient) SetUserContext(user entities.UserContext) (err error) { +func (o *OptimizelyClient) SetUserContext(user *entities.UserContext) (err error) { defer func() { if r := recover(); r != nil { diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 91de7092b..0a956fab8 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -176,7 +176,8 @@ func TestSetUserContext(t *testing.T) { ConfigManager: ValidProjectConfigManager(), } - userContext := entities.UserContext{ID: "1212121", Attributes: map[string]interface{}{}, DefaultDecideOptions: []entities.OptimizelyDecideOption{entities.DisableTracking}} + userContext := entities.NewUserContext("1212121", map[string]interface{}{}) + userContext.SetDefaultDecideOptions(entities.DisableTracking) err := client.SetUserContext(userContext) assert.NoError(t, err) assert.Equal(t, userContext, client.userContext) @@ -187,12 +188,14 @@ func TestSetUserContextReplace(t *testing.T) { ConfigManager: ValidProjectConfigManager(), } - userContext := entities.UserContext{ID: "1212121", Attributes: map[string]interface{}{}, DefaultDecideOptions: []entities.OptimizelyDecideOption{entities.DisableTracking}} + userContext := entities.NewUserContext("1212121", map[string]interface{}{}) + userContext.SetDefaultDecideOptions(entities.DisableTracking) err := client.SetUserContext(userContext) assert.NoError(t, err) assert.Equal(t, userContext, client.userContext) - userContext2 := entities.UserContext{ID: "1212127", Attributes: map[string]interface{}{}, DefaultDecideOptions: []entities.OptimizelyDecideOption{entities.BypassUPS}} + userContext2 := entities.NewUserContext("1212121", map[string]interface{}{}) + userContext2.SetDefaultDecideOptions(entities.BypassUPS) err = client.SetUserContext(userContext2) assert.NoError(t, err) assert.Equal(t, userContext2, client.userContext) @@ -200,7 +203,8 @@ func TestSetUserContextReplace(t *testing.T) { func TestSetUserContextWithoutConfig(t *testing.T) { // ensure that we recover if the SDK panics - userContext := entities.UserContext{ID: "1212121", Attributes: map[string]interface{}{}, DefaultDecideOptions: []entities.OptimizelyDecideOption{entities.DisableTracking}} + userContext := entities.NewUserContext("1212121", map[string]interface{}{}) + userContext.SetDefaultDecideOptions(entities.DisableTracking) client := OptimizelyClient{ ConfigManager: new(PanickingConfigManager), DecisionService: new(MockDecisionService), diff --git a/pkg/entities/user_context.go b/pkg/entities/user_context.go index 2d7fd26d2..1332749b1 100644 --- a/pkg/entities/user_context.go +++ b/pkg/entities/user_context.go @@ -20,6 +20,7 @@ package entities import ( "fmt" + guuid "github.com/google/uuid" "github.com/optimizely/go-sdk/pkg/utils" ) @@ -32,6 +33,21 @@ type UserContext struct { DefaultDecideOptions []OptimizelyDecideOption } +// NewUserContext creates and returns a new user context +func NewUserContext(userID string, attributes map[string]interface{}) *UserContext { + if userID == "" { + userID = guuid.New().String() + } + if attributes == nil { + attributes = map[string]interface{}{} + } + return &UserContext{ + ID: userID, + Attributes: attributes, + DefaultDecideOptions: []OptimizelyDecideOption{}, + } +} + // SetDefaultDecideOptions sets default decide options func (u *UserContext) SetDefaultDecideOptions(options ...OptimizelyDecideOption) { u.DefaultDecideOptions = append(u.DefaultDecideOptions, options...) diff --git a/pkg/entities/user_context_test.go b/pkg/entities/user_context_test.go index 6f730db10..4303325b6 100644 --- a/pkg/entities/user_context_test.go +++ b/pkg/entities/user_context_test.go @@ -24,11 +24,16 @@ import ( ) func TestSetDefaultDecideOptions(t *testing.T) { - userContext := UserContext{} + userContext := NewUserContext("1212", map[string]interface{}{}) userContext.SetDefaultDecideOptions(DisableTracking) assert.Equal(t, []OptimizelyDecideOption{DisableTracking}, userContext.DefaultDecideOptions) } +func TestSetEmptyUserID(t *testing.T) { + userContext := NewUserContext("", map[string]interface{}{}) + assert.NotEqual(t, "", userContext.ID) +} + func TestUserAttributeExists(t *testing.T) { userContext := UserContext{ Attributes: map[string]interface{}{