diff --git a/pkg/client/client.go b/pkg/client/client.go index 26eafe2d6..72b8b81a5 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..0a956fab8 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -171,6 +171,50 @@ func (TestConfig) GetClientVersion() string { return "1.0.0" } +func TestSetUserContext(t *testing.T) { + client := OptimizelyClient{ + ConfigManager: ValidProjectConfigManager(), + } + + 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) +} + +func TestSetUserContextReplace(t *testing.T) { + client := OptimizelyClient{ + ConfigManager: ValidProjectConfigManager(), + } + + 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.NewUserContext("1212121", map[string]interface{}{}) + userContext2.SetDefaultDecideOptions(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.NewUserContext("1212121", map[string]interface{}{}) + userContext.SetDefaultDecideOptions(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..1332749b1 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. * @@ -20,6 +20,7 @@ package entities import ( "fmt" + guuid "github.com/google/uuid" "github.com/optimizely/go-sdk/pkg/utils" ) @@ -27,8 +28,29 @@ 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 +} + +// 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...) } // 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..4303325b6 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,17 @@ import ( "github.com/stretchr/testify/assert" ) +func TestSetDefaultDecideOptions(t *testing.T) { + 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{}{