Skip to content

Commit a7b01d7

Browse files
authored
feat(decide): add UserContext. (#297)
A part of multiple PRs for Decide API - added OptimizelyUserContext - added setUserContext API
1 parent c496a50 commit a7b01d7

10 files changed

+592
-17
lines changed

pkg/client/client.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"strconv"
2727

2828
"github.com/optimizely/go-sdk/pkg/config"
29+
"github.com/optimizely/go-sdk/pkg/decide"
2930
"github.com/optimizely/go-sdk/pkg/decision"
3031
"github.com/optimizely/go-sdk/pkg/entities"
3132
"github.com/optimizely/go-sdk/pkg/event"
@@ -39,12 +40,19 @@ import (
3940

4041
// OptimizelyClient is the entry point to the Optimizely SDK
4142
type OptimizelyClient struct {
42-
ConfigManager config.ProjectConfigManager
43-
DecisionService decision.Service
44-
EventProcessor event.Processor
45-
notificationCenter notification.Center
46-
execGroup *utils.ExecGroup
47-
logger logging.OptimizelyLogProducer
43+
ConfigManager config.ProjectConfigManager
44+
DecisionService decision.Service
45+
EventProcessor event.Processor
46+
notificationCenter notification.Center
47+
execGroup *utils.ExecGroup
48+
logger logging.OptimizelyLogProducer
49+
defaultDecideOptions decide.OptimizelyDecideOptions
50+
}
51+
52+
// CreateUserContext creates a context of the user for which decision APIs will be called.
53+
// A user context will be created successfully even when the SDK is not fully configured yet.
54+
func (o *OptimizelyClient) CreateUserContext(userID string, attributes map[string]interface{}) OptimizelyUserContext {
55+
return newOptimizelyUserContext(o, userID, attributes)
4856
}
4957

5058
// Activate returns the key of the variation the user is bucketed into and queues up an impression event to be sent to

pkg/client/client_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2447,6 +2447,60 @@ func (s *ClientTestSuiteFM) TestGetEnabledFeaturesErrorCases() {
24472447
s.mockDecisionService.AssertNotCalled(s.T(), "GetFeatureDecision")
24482448
}
24492449

2450+
func TestCreateUserContext(t *testing.T) {
2451+
client := OptimizelyClient{}
2452+
userID := "1212121"
2453+
userAttributes := map[string]interface{}{"key": 1212}
2454+
optimizelyUserContext := client.CreateUserContext(userID, userAttributes)
2455+
2456+
assert.Equal(t, client, *optimizelyUserContext.GetOptimizely())
2457+
assert.Equal(t, userID, optimizelyUserContext.GetUserID())
2458+
assert.Equal(t, userAttributes, optimizelyUserContext.GetUserAttributes())
2459+
}
2460+
2461+
func TestChangingAttributesDoesntEffectUserContext(t *testing.T) {
2462+
client := OptimizelyClient{}
2463+
userID := "1"
2464+
userAttributes := map[string]interface{}{"key": 1212}
2465+
optimizelyUserContext := client.CreateUserContext(userID, userAttributes)
2466+
assert.Equal(t, client, *optimizelyUserContext.GetOptimizely())
2467+
2468+
// Changing original values
2469+
userID = "2"
2470+
userAttributes["key"] = 1213
2471+
// Verifying that no changes were reflected in the user context
2472+
assert.Equal(t, "1", optimizelyUserContext.GetUserID())
2473+
assert.Equal(t, map[string]interface{}{"key": 1212}, optimizelyUserContext.GetUserAttributes())
2474+
}
2475+
2476+
func TestCreateUserContextNoAttributes(t *testing.T) {
2477+
client := OptimizelyClient{}
2478+
var attributes map[string]interface{}
2479+
userID := "testUser1"
2480+
optimizelyUserContext := client.CreateUserContext(userID, attributes)
2481+
2482+
assert.Equal(t, client, *optimizelyUserContext.GetOptimizely())
2483+
assert.Equal(t, attributes, optimizelyUserContext.GetUserAttributes())
2484+
}
2485+
2486+
func TestCreateUserContextMultiple(t *testing.T) {
2487+
client := OptimizelyClient{}
2488+
userID1 := "testUser1"
2489+
userID2 := "testUser2"
2490+
userAttributes1 := map[string]interface{}{"key": 1212}
2491+
userAttributes2 := map[string]interface{}{"key": 1213}
2492+
2493+
optimizelyUserContext1 := client.CreateUserContext(userID1, userAttributes1)
2494+
optimizelyUserContext2 := client.CreateUserContext(userID2, userAttributes2)
2495+
2496+
assert.Equal(t, client, *optimizelyUserContext1.GetOptimizely())
2497+
assert.Equal(t, client, *optimizelyUserContext2.GetOptimizely())
2498+
assert.Equal(t, userID1, optimizelyUserContext1.GetUserID())
2499+
assert.Equal(t, userID2, optimizelyUserContext2.GetUserID())
2500+
assert.Equal(t, userAttributes1, optimizelyUserContext1.GetUserAttributes())
2501+
assert.Equal(t, userAttributes2, optimizelyUserContext2.GetUserAttributes())
2502+
}
2503+
24502504
func TestClose(t *testing.T) {
24512505
mockProcessor := &MockProcessor{}
24522506
mockDecisionService := new(MockDecisionService)

pkg/client/factory.go

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"time"
2424

2525
"github.com/optimizely/go-sdk/pkg/config"
26+
"github.com/optimizely/go-sdk/pkg/decide"
2627
"github.com/optimizely/go-sdk/pkg/decision"
2728
"github.com/optimizely/go-sdk/pkg/event"
2829
"github.com/optimizely/go-sdk/pkg/logging"
@@ -37,14 +38,15 @@ type OptimizelyFactory struct {
3738
Datafile []byte
3839
DatafileAccessToken string
3940

40-
configManager config.ProjectConfigManager
41-
ctx context.Context
42-
decisionService decision.Service
43-
eventDispatcher event.Dispatcher
44-
eventProcessor event.Processor
45-
userProfileService decision.UserProfileService
46-
overrideStore decision.ExperimentOverrideStore
47-
metricsRegistry metrics.Registry
41+
configManager config.ProjectConfigManager
42+
ctx context.Context
43+
decisionService decision.Service
44+
defaultDecideOptions decide.OptimizelyDecideOptions
45+
eventDispatcher event.Dispatcher
46+
eventProcessor event.Processor
47+
userProfileService decision.UserProfileService
48+
overrideStore decision.ExperimentOverrideStore
49+
metricsRegistry metrics.Registry
4850
}
4951

5052
// OptionFunc is used to provide custom client configuration to the OptimizelyFactory.
@@ -76,9 +78,12 @@ func (f *OptimizelyFactory) Client(clientOptions ...OptionFunc) (*OptimizelyClie
7678
}
7779

7880
eg := utils.NewExecGroup(ctx, logging.GetLogger(f.SDKKey, "ExecGroup"))
79-
appClient := &OptimizelyClient{execGroup: eg,
80-
notificationCenter: registry.GetNotificationCenter(f.SDKKey),
81-
logger: logging.GetLogger(f.SDKKey, "OptimizelyClient")}
81+
appClient := &OptimizelyClient{
82+
defaultDecideOptions: f.defaultDecideOptions,
83+
execGroup: eg,
84+
notificationCenter: registry.GetNotificationCenter(f.SDKKey),
85+
logger: logging.GetLogger(f.SDKKey, "OptimizelyClient"),
86+
}
8287

8388
if f.configManager != nil {
8489
appClient.ConfigManager = f.configManager
@@ -167,6 +172,13 @@ func WithDecisionService(decisionService decision.Service) OptionFunc {
167172
}
168173
}
169174

175+
// WithDefaultDecideOptions sets default decide options on a client.
176+
func WithDefaultDecideOptions(decideOptions decide.OptimizelyDecideOptions) OptionFunc {
177+
return func(f *OptimizelyFactory) {
178+
f.defaultDecideOptions = decideOptions
179+
}
180+
}
181+
170182
// WithUserProfileService sets the user profile service on the decision service.
171183
func WithUserProfileService(userProfileService decision.UserProfileService) OptionFunc {
172184
return func(f *OptimizelyFactory) {

pkg/client/factory_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"time"
2626

2727
"github.com/optimizely/go-sdk/pkg/config"
28+
"github.com/optimizely/go-sdk/pkg/decide"
2829
"github.com/optimizely/go-sdk/pkg/decision"
2930
"github.com/optimizely/go-sdk/pkg/event"
3031
"github.com/optimizely/go-sdk/pkg/metrics"
@@ -201,3 +202,35 @@ func TestClientWithDatafileAccessToken(t *testing.T) {
201202

202203
assert.Equal(t, accessToken, factory.DatafileAccessToken)
203204
}
205+
206+
func TestClientWithDefaultDecideOptions(t *testing.T) {
207+
decideOptions := decide.OptimizelyDecideOptions{
208+
DisableDecisionEvent: true,
209+
EnabledFlagsOnly: true,
210+
}
211+
factory := OptimizelyFactory{SDKKey: "1212"}
212+
optimizelyClient, err := factory.Client(WithDefaultDecideOptions(decideOptions))
213+
assert.NoError(t, err)
214+
assert.Equal(t, decideOptions, optimizelyClient.defaultDecideOptions)
215+
216+
// Verify that defaultDecideOptions are initialized as empty by default
217+
factory = OptimizelyFactory{SDKKey: "1212"}
218+
optimizelyClient, err = factory.Client()
219+
assert.NoError(t, err)
220+
assert.Equal(t, decide.OptimizelyDecideOptions{}, optimizelyClient.defaultDecideOptions)
221+
}
222+
223+
func TestModifyingDecideOptionsOutsideClient(t *testing.T) {
224+
decideOptions := decide.OptimizelyDecideOptions{
225+
DisableDecisionEvent: true,
226+
EnabledFlagsOnly: true,
227+
}
228+
factory := OptimizelyFactory{SDKKey: "1212"}
229+
optimizelyClient, err := factory.Client(WithDefaultDecideOptions(decideOptions))
230+
assert.NoError(t, err)
231+
decideOptions.IgnoreUserProfileService = true
232+
assert.Equal(t, decide.OptimizelyDecideOptions{
233+
DisableDecisionEvent: true,
234+
EnabledFlagsOnly: true,
235+
}, optimizelyClient.defaultDecideOptions)
236+
}

pkg/client/optimizely_decision.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/****************************************************************************
2+
* Copyright 2020, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
// Package client //
18+
package client
19+
20+
import (
21+
"github.com/optimizely/go-sdk/pkg/optimizelyjson"
22+
)
23+
24+
// OptimizelyDecision defines the decision returned by decide api.
25+
type OptimizelyDecision struct {
26+
variationKey string
27+
enabled bool
28+
variables *optimizelyjson.OptimizelyJSON
29+
ruleKey string
30+
flagKey string
31+
userContext OptimizelyUserContext
32+
reasons []string
33+
}
34+
35+
// NewOptimizelyDecision creates and returns a new instance of OptimizelyDecision
36+
func NewOptimizelyDecision(variationKey, ruleKey, flagKey string, enabled bool, variables *optimizelyjson.OptimizelyJSON, userContext OptimizelyUserContext, reasons []string) OptimizelyDecision {
37+
return OptimizelyDecision{
38+
variationKey: variationKey,
39+
enabled: enabled,
40+
variables: variables,
41+
ruleKey: ruleKey,
42+
flagKey: flagKey,
43+
userContext: userContext,
44+
reasons: reasons,
45+
}
46+
}
47+
48+
// NewErrorDecision returns a decision with error
49+
func NewErrorDecision(key string, user OptimizelyUserContext, err error) OptimizelyDecision {
50+
return OptimizelyDecision{
51+
flagKey: key,
52+
userContext: user,
53+
variables: &optimizelyjson.OptimizelyJSON{},
54+
reasons: []string{err.Error()},
55+
}
56+
}
57+
58+
// GetVariationKey returns variation key for optimizely decision.
59+
func (o OptimizelyDecision) GetVariationKey() string {
60+
return o.variationKey
61+
}
62+
63+
// GetEnabled returns the boolean value indicating if the flag is enabled or not.
64+
func (o OptimizelyDecision) GetEnabled() bool {
65+
return o.enabled
66+
}
67+
68+
// GetVariables returns the collection of variables associated with the decision.
69+
func (o OptimizelyDecision) GetVariables() *optimizelyjson.OptimizelyJSON {
70+
return o.variables
71+
}
72+
73+
// GetRuleKey returns the rule key of the decision.
74+
func (o OptimizelyDecision) GetRuleKey() string {
75+
return o.ruleKey
76+
}
77+
78+
// GetFlagKey returns the flag key for which the decision was made.
79+
func (o OptimizelyDecision) GetFlagKey() string {
80+
return o.flagKey
81+
}
82+
83+
// GetUserContext returns the user context for which the decision was made.
84+
func (o OptimizelyDecision) GetUserContext() OptimizelyUserContext {
85+
return o.userContext
86+
}
87+
88+
// GetReasons returns an array of error/info/debug messages describing why the decision has been made.
89+
func (o OptimizelyDecision) GetReasons() []string {
90+
return o.reasons
91+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/****************************************************************************
2+
* Copyright 2020, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package client
18+
19+
import (
20+
"errors"
21+
"testing"
22+
23+
"github.com/optimizely/go-sdk/pkg/optimizelyjson"
24+
25+
"github.com/stretchr/testify/suite"
26+
)
27+
28+
type OptimizelyDecisionTestSuite struct {
29+
suite.Suite
30+
*OptimizelyClient
31+
}
32+
33+
func (s *OptimizelyDecisionTestSuite) SetupTest() {
34+
factory := OptimizelyFactory{SDKKey: "1212"}
35+
s.OptimizelyClient, _ = factory.Client()
36+
}
37+
38+
func (s *OptimizelyDecisionTestSuite) TestOptimizelyDecision() {
39+
variationKey := "var1"
40+
enabled := true
41+
variables, _ := optimizelyjson.NewOptimizelyJSONfromString(`{"k1":"v1"}`)
42+
var ruleKey string
43+
flagKey := "flag1"
44+
reasons := []string{}
45+
userID := "testUser1"
46+
attributes := map[string]interface{}{"key": 1212}
47+
48+
optimizelyUserContext := s.OptimizelyClient.CreateUserContext(userID, attributes)
49+
decision := NewOptimizelyDecision(variationKey, ruleKey, flagKey, enabled, variables, optimizelyUserContext, reasons)
50+
51+
s.Equal(variationKey, decision.GetVariationKey())
52+
s.Equal(enabled, decision.GetEnabled())
53+
s.Equal(variables, decision.GetVariables())
54+
s.Equal(ruleKey, decision.GetRuleKey())
55+
s.Equal(flagKey, decision.GetFlagKey())
56+
s.Equal(reasons, decision.GetReasons())
57+
s.Equal(optimizelyUserContext, decision.GetUserContext())
58+
}
59+
60+
func (s *OptimizelyDecisionTestSuite) TestNewErrorDecision() {
61+
flagKey := "flag1"
62+
errorString := "SDK has an error"
63+
userID := "testUser1"
64+
attributes := map[string]interface{}{"key": 1212}
65+
optimizelyUserContext := s.OptimizelyClient.CreateUserContext(userID, attributes)
66+
decision := NewErrorDecision(flagKey, optimizelyUserContext, errors.New(errorString))
67+
68+
s.Equal("", decision.GetVariationKey())
69+
s.Equal(false, decision.GetEnabled())
70+
s.Equal(&optimizelyjson.OptimizelyJSON{}, decision.GetVariables())
71+
s.Equal("", decision.GetRuleKey())
72+
s.Equal(flagKey, decision.GetFlagKey())
73+
s.Equal(1, len(decision.GetReasons()))
74+
s.Equal(optimizelyUserContext, decision.GetUserContext())
75+
s.Equal(errorString, decision.GetReasons()[0])
76+
}
77+
78+
func TestOptimizelyDecisionTestSuite(t *testing.T) {
79+
suite.Run(t, new(OptimizelyDecisionTestSuite))
80+
}

0 commit comments

Comments
 (0)