Skip to content

Commit 5cbe8e7

Browse files
authored
feat(ForcedDecisions): add forced-decisions APIs to OptimizelyUserContext (#324)
## Summary Add a set of new APIs for forced-decisions to OptimizelyUserContext: - SetForcedDecision - GetForcedDecision - RemoveForcedDecision - RemoveAllForcedDecisions ## Test plan - unit tests for the new APIs - FSC tests with new test cases ## FSC Result - https://app.travis-ci.com/github/optimizely/fullstack-sdk-compatibility-suite/builds/239804154
1 parent 948015d commit 5cbe8e7

20 files changed

+990
-66
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jobs:
3636
- <<: *test
3737
stage: 'Unit test'
3838
env: GIMME_GO_VERSION=1.10.x
39+
dist: focal
3940
before_script:
4041
# GO module was not introduced earlier. need symlink to search in GOPATH
4142
- mkdir -p $GOPATH/src/github.com && pushd $GOPATH/src/github.com && ln -s $HOME/build/optimizely optimizely && popd

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.12
55
require (
66
github.com/google/uuid v1.1.1
77
github.com/hashicorp/go-multierror v1.1.0
8-
github.com/json-iterator/go v1.1.7
8+
github.com/json-iterator/go v1.1.12
99
github.com/pkg/errors v0.8.1
1010
github.com/pkg/profile v1.3.0
1111
github.com/stretchr/testify v1.4.0

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
99
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
1010
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
1111
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
12-
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
13-
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
12+
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
13+
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
1414
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
1515
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
16-
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
17-
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
16+
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
17+
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
1818
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
1919
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
2020
github.com/pkg/profile v1.3.0 h1:OQIvuDgm00gWVWGTf4m4mCt6W1/0YqU7Ntg0mySWgaI=

pkg/client/client.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2019-2020, Optimizely, Inc. and contributors *
2+
* Copyright 2019-2021, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -28,6 +28,7 @@ import (
2828
"github.com/optimizely/go-sdk/pkg/config"
2929
"github.com/optimizely/go-sdk/pkg/decide"
3030
"github.com/optimizely/go-sdk/pkg/decision"
31+
pkgReasons "github.com/optimizely/go-sdk/pkg/decision/reasons"
3132
"github.com/optimizely/go-sdk/pkg/entities"
3233
"github.com/optimizely/go-sdk/pkg/event"
3334
"github.com/optimizely/go-sdk/pkg/logging"
@@ -52,7 +53,7 @@ type OptimizelyClient struct {
5253
// CreateUserContext creates a context of the user for which decision APIs will be called.
5354
// A user context will be created successfully even when the SDK is not fully configured yet.
5455
func (o *OptimizelyClient) CreateUserContext(userID string, attributes map[string]interface{}) OptimizelyUserContext {
55-
return newOptimizelyUserContext(o, userID, attributes)
56+
return newOptimizelyUserContext(o, userID, attributes, nil)
5657
}
5758

5859
func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string, options *decide.Options) OptimizelyDecision {
@@ -73,7 +74,9 @@ func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string,
7374
}
7475
}()
7576

76-
decisionContext := decision.FeatureDecisionContext{}
77+
decisionContext := decision.FeatureDecisionContext{
78+
ForcedDecisionService: userContext.forcedDecisionService,
79+
}
7780
projectConfig, err := o.getProjectConfig()
7881
if err != nil {
7982
return NewErrorDecision(key, userContext, decide.GetDecideError(decide.SDKNotReady))
@@ -95,9 +98,30 @@ func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string,
9598
allOptions := o.getAllOptions(options)
9699
decisionReasons := decide.NewDecisionReasons(&allOptions)
97100
decisionContext.Variable = entities.Variable{}
101+
var featureDecision decision.FeatureDecision
102+
var reasons decide.DecisionReasons
98103

99-
featureDecision, reasons, err := o.DecisionService.GetFeatureDecision(decisionContext, usrContext, &allOptions)
100-
decisionReasons.Append(reasons)
104+
// To avoid cyclo-complexity warning
105+
findRegularDecision := func() {
106+
// regular decision
107+
featureDecision, reasons, err = o.DecisionService.GetFeatureDecision(decisionContext, usrContext, &allOptions)
108+
decisionReasons.Append(reasons)
109+
}
110+
111+
// check forced-decisions first
112+
// Passing empty rule-key because checking mapping with flagKey only
113+
if userContext.forcedDecisionService != nil {
114+
var variation *entities.Variation
115+
variation, reasons, err = userContext.forcedDecisionService.FindValidatedForcedDecision(projectConfig, key, "", &allOptions)
116+
decisionReasons.Append(reasons)
117+
if err != nil {
118+
findRegularDecision()
119+
} else {
120+
featureDecision = decision.FeatureDecision{Decision: decision.Decision{Reason: pkgReasons.ForcedDecisionFound}, Variation: variation, Source: decision.FeatureTest}
121+
}
122+
} else {
123+
findRegularDecision()
124+
}
101125

102126
if err != nil {
103127
o.logger.Warning(fmt.Sprintf(`Received error while making a decision for feature "%s": %s`, key, err))

pkg/client/optimizely_user_context.go

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2020, Optimizely, Inc. and contributors *
2+
* Copyright 2020-2021, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -21,6 +21,7 @@ import (
2121
"sync"
2222

2323
"github.com/optimizely/go-sdk/pkg/decide"
24+
"github.com/optimizely/go-sdk/pkg/decision"
2425
"github.com/optimizely/go-sdk/pkg/entities"
2526
)
2627

@@ -29,23 +30,24 @@ type OptimizelyUserContext struct {
2930
UserID string `json:"userId"`
3031
Attributes map[string]interface{} `json:"attributes"`
3132

32-
optimizely *OptimizelyClient
33-
mutex *sync.RWMutex
33+
optimizely *OptimizelyClient
34+
forcedDecisionService *decision.ForcedDecisionService
35+
mutex *sync.RWMutex
3436
}
3537

3638
// returns an instance of the optimizely user context.
37-
func newOptimizelyUserContext(optimizely *OptimizelyClient, userID string, attributes map[string]interface{}) OptimizelyUserContext {
39+
func newOptimizelyUserContext(optimizely *OptimizelyClient, userID string, attributes map[string]interface{}, forcedDecisionService *decision.ForcedDecisionService) OptimizelyUserContext {
3840
// store a copy of the provided attributes so it isn't affected by changes made afterwards.
3941
if attributes == nil {
4042
attributes = map[string]interface{}{}
4143
}
4244
attributesCopy := copyUserAttributes(attributes)
43-
4445
return OptimizelyUserContext{
45-
UserID: userID,
46-
Attributes: attributesCopy,
47-
optimizely: optimizely,
48-
mutex: new(sync.RWMutex),
46+
UserID: userID,
47+
Attributes: attributesCopy,
48+
optimizely: optimizely,
49+
forcedDecisionService: forcedDecisionService,
50+
mutex: new(sync.RWMutex),
4951
}
5052
}
5153

@@ -66,6 +68,13 @@ func (o OptimizelyUserContext) GetUserAttributes() map[string]interface{} {
6668
return copyUserAttributes(o.Attributes)
6769
}
6870

71+
func (o OptimizelyUserContext) getForcedDecisionService() *decision.ForcedDecisionService {
72+
if o.forcedDecisionService != nil {
73+
return o.forcedDecisionService.CreateCopy()
74+
}
75+
return nil
76+
}
77+
6978
// SetAttribute sets an attribute for a given key.
7079
func (o *OptimizelyUserContext) SetAttribute(key string, value interface{}) {
7180
o.mutex.Lock()
@@ -80,21 +89,21 @@ func (o *OptimizelyUserContext) SetAttribute(key string, value interface{}) {
8089
// all data required to deliver the flag or experiment.
8190
func (o *OptimizelyUserContext) Decide(key string, options []decide.OptimizelyDecideOptions) OptimizelyDecision {
8291
// use a copy of the user context so that any changes to the original context are not reflected inside the decision
83-
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes())
92+
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService())
8493
return o.optimizely.decide(userContextCopy, key, convertDecideOptions(options))
8594
}
8695

8796
// DecideAll returns a key-map of decision results for all active flag keys with options.
8897
func (o *OptimizelyUserContext) DecideAll(options []decide.OptimizelyDecideOptions) map[string]OptimizelyDecision {
8998
// use a copy of the user context so that any changes to the original context are not reflected inside the decision
90-
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes())
99+
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService())
91100
return o.optimizely.decideAll(userContextCopy, convertDecideOptions(options))
92101
}
93102

94103
// DecideForKeys returns a key-map of decision results for multiple flag keys and options.
95104
func (o *OptimizelyUserContext) DecideForKeys(keys []string, options []decide.OptimizelyDecideOptions) map[string]OptimizelyDecision {
96105
// use a copy of the user context so that any changes to the original context are not reflected inside the decision
97-
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes())
106+
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService())
98107
return o.optimizely.decideForKeys(userContextCopy, keys, convertDecideOptions(options))
99108
}
100109

@@ -108,6 +117,55 @@ func (o *OptimizelyUserContext) TrackEvent(eventKey string, eventTags map[string
108117
return o.optimizely.Track(eventKey, userContext, eventTags)
109118
}
110119

120+
// SetForcedDecision sets the forced decision (variation key) for a given flag and an optional rule.
121+
// returns true if the forced decision has been set successfully.
122+
func (o *OptimizelyUserContext) SetForcedDecision(flagKey, ruleKey, variationKey string) bool {
123+
if _, err := o.optimizely.getProjectConfig(); err != nil {
124+
o.optimizely.logger.Error("Optimizely instance is not valid, failing setForcedDecision call.", err)
125+
return false
126+
}
127+
if o.forcedDecisionService == nil {
128+
o.forcedDecisionService = decision.NewForcedDecisionService(o.GetUserID())
129+
}
130+
return o.forcedDecisionService.SetForcedDecision(flagKey, ruleKey, variationKey)
131+
}
132+
133+
// GetForcedDecision returns the forced decision for a given flag and an optional rule
134+
func (o *OptimizelyUserContext) GetForcedDecision(flagKey, ruleKey string) string {
135+
if _, err := o.optimizely.getProjectConfig(); err != nil {
136+
o.optimizely.logger.Error("Optimizely instance is not valid, failing getForcedDecision call.", err)
137+
return ""
138+
}
139+
if o.forcedDecisionService == nil {
140+
return ""
141+
}
142+
return o.forcedDecisionService.GetForcedDecision(flagKey, ruleKey)
143+
}
144+
145+
// RemoveForcedDecision removes the forced decision for a given flag and an optional rule.
146+
func (o *OptimizelyUserContext) RemoveForcedDecision(flagKey, ruleKey string) bool {
147+
if _, err := o.optimizely.getProjectConfig(); err != nil {
148+
o.optimizely.logger.Error("Optimizely instance is not valid, failing removeForcedDecision call.", err)
149+
return false
150+
}
151+
if o.forcedDecisionService == nil {
152+
return false
153+
}
154+
return o.forcedDecisionService.RemoveForcedDecision(flagKey, ruleKey)
155+
}
156+
157+
// RemoveAllForcedDecisions removes all forced decisions bound to this user context.
158+
func (o *OptimizelyUserContext) RemoveAllForcedDecisions() bool {
159+
if _, err := o.optimizely.getProjectConfig(); err != nil {
160+
o.optimizely.logger.Error("Optimizely instance is not valid, failing removeForcedDecision call.", err)
161+
return false
162+
}
163+
if o.forcedDecisionService == nil {
164+
return true
165+
}
166+
return o.forcedDecisionService.RemoveAllForcedDecisions()
167+
}
168+
111169
func copyUserAttributes(attributes map[string]interface{}) (attributesCopy map[string]interface{}) {
112170
if attributes != nil {
113171
attributesCopy = make(map[string]interface{})

0 commit comments

Comments
 (0)