Skip to content

Commit 82644c6

Browse files
authored
refact(forced-decision): Support for decision context added. (#326)
* updated with changes made in swift-sdk. * fixes. * suggested changes made. * Replacing `Variation` with `VariationKey` in forced-decision struct * fixing unit tests for empty flagkey.
1 parent 5cbe8e7 commit 82644c6

9 files changed

+193
-139
lines changed

pkg/client/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string,
112112
// Passing empty rule-key because checking mapping with flagKey only
113113
if userContext.forcedDecisionService != nil {
114114
var variation *entities.Variation
115-
variation, reasons, err = userContext.forcedDecisionService.FindValidatedForcedDecision(projectConfig, key, "", &allOptions)
115+
variation, reasons, err = userContext.forcedDecisionService.FindValidatedForcedDecision(projectConfig, decision.OptimizelyDecisionContext{FlagKey: key, RuleKey: ""}, &allOptions)
116116
decisionReasons.Append(reasons)
117117
if err != nil {
118118
findRegularDecision()

pkg/client/optimizely_user_context.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818
package client
1919

2020
import (
21+
"errors"
2122
"sync"
2223

2324
"github.com/optimizely/go-sdk/pkg/decide"
24-
"github.com/optimizely/go-sdk/pkg/decision"
25+
pkgDecision "github.com/optimizely/go-sdk/pkg/decision"
2526
"github.com/optimizely/go-sdk/pkg/entities"
2627
)
2728

@@ -31,12 +32,12 @@ type OptimizelyUserContext struct {
3132
Attributes map[string]interface{} `json:"attributes"`
3233

3334
optimizely *OptimizelyClient
34-
forcedDecisionService *decision.ForcedDecisionService
35+
forcedDecisionService *pkgDecision.ForcedDecisionService
3536
mutex *sync.RWMutex
3637
}
3738

3839
// returns an instance of the optimizely user context.
39-
func newOptimizelyUserContext(optimizely *OptimizelyClient, userID string, attributes map[string]interface{}, forcedDecisionService *decision.ForcedDecisionService) OptimizelyUserContext {
40+
func newOptimizelyUserContext(optimizely *OptimizelyClient, userID string, attributes map[string]interface{}, forcedDecisionService *pkgDecision.ForcedDecisionService) OptimizelyUserContext {
4041
// store a copy of the provided attributes so it isn't affected by changes made afterwards.
4142
if attributes == nil {
4243
attributes = map[string]interface{}{}
@@ -68,7 +69,7 @@ func (o OptimizelyUserContext) GetUserAttributes() map[string]interface{} {
6869
return copyUserAttributes(o.Attributes)
6970
}
7071

71-
func (o OptimizelyUserContext) getForcedDecisionService() *decision.ForcedDecisionService {
72+
func (o OptimizelyUserContext) getForcedDecisionService() *pkgDecision.ForcedDecisionService {
7273
if o.forcedDecisionService != nil {
7374
return o.forcedDecisionService.CreateCopy()
7475
}
@@ -117,41 +118,42 @@ func (o *OptimizelyUserContext) TrackEvent(eventKey string, eventTags map[string
117118
return o.optimizely.Track(eventKey, userContext, eventTags)
118119
}
119120

120-
// SetForcedDecision sets the forced decision (variation key) for a given flag and an optional rule.
121+
// SetForcedDecision sets the forced decision (variation key) for a given decision context (flag key and optional rule key).
121122
// returns true if the forced decision has been set successfully.
122-
func (o *OptimizelyUserContext) SetForcedDecision(flagKey, ruleKey, variationKey string) bool {
123+
func (o *OptimizelyUserContext) SetForcedDecision(context pkgDecision.OptimizelyDecisionContext, decision pkgDecision.OptimizelyForcedDecision) bool {
123124
if _, err := o.optimizely.getProjectConfig(); err != nil {
124125
o.optimizely.logger.Error("Optimizely instance is not valid, failing setForcedDecision call.", err)
125126
return false
126127
}
127128
if o.forcedDecisionService == nil {
128-
o.forcedDecisionService = decision.NewForcedDecisionService(o.GetUserID())
129+
o.forcedDecisionService = pkgDecision.NewForcedDecisionService(o.GetUserID())
129130
}
130-
return o.forcedDecisionService.SetForcedDecision(flagKey, ruleKey, variationKey)
131+
return o.forcedDecisionService.SetForcedDecision(context, decision)
131132
}
132133

133134
// GetForcedDecision returns the forced decision for a given flag and an optional rule
134-
func (o *OptimizelyUserContext) GetForcedDecision(flagKey, ruleKey string) string {
135+
func (o *OptimizelyUserContext) GetForcedDecision(context pkgDecision.OptimizelyDecisionContext) (pkgDecision.OptimizelyForcedDecision, error) {
136+
forcedDecision := pkgDecision.OptimizelyForcedDecision{}
135137
if _, err := o.optimizely.getProjectConfig(); err != nil {
136138
o.optimizely.logger.Error("Optimizely instance is not valid, failing getForcedDecision call.", err)
137-
return ""
139+
return forcedDecision, err
138140
}
139141
if o.forcedDecisionService == nil {
140-
return ""
142+
return forcedDecision, errors.New("decision not found")
141143
}
142-
return o.forcedDecisionService.GetForcedDecision(flagKey, ruleKey)
144+
return o.forcedDecisionService.GetForcedDecision(context)
143145
}
144146

145147
// RemoveForcedDecision removes the forced decision for a given flag and an optional rule.
146-
func (o *OptimizelyUserContext) RemoveForcedDecision(flagKey, ruleKey string) bool {
148+
func (o *OptimizelyUserContext) RemoveForcedDecision(context pkgDecision.OptimizelyDecisionContext) bool {
147149
if _, err := o.optimizely.getProjectConfig(); err != nil {
148150
o.optimizely.logger.Error("Optimizely instance is not valid, failing removeForcedDecision call.", err)
149151
return false
150152
}
151153
if o.forcedDecisionService == nil {
152154
return false
153155
}
154-
return o.forcedDecisionService.RemoveForcedDecision(flagKey, ruleKey)
156+
return o.forcedDecisionService.RemoveForcedDecision(context)
155157
}
156158

157159
// RemoveAllForcedDecisions removes all forced decisions bound to this user context.

pkg/client/optimizely_user_context_test.go

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ func (s *OptimizelyUserContextTestSuite) TestDecideFeatureTestWithForcedDecision
215215
s.NoError(err)
216216

217217
user := s.OptimizelyClient.CreateUserContext(s.userID, nil)
218-
user.SetForcedDecision(flagKey, ruleKey, variationKey)
218+
user.SetForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKey, RuleKey: ruleKey}, decision.OptimizelyForcedDecision{VariationKey: variationKey})
219219
decision := user.Decide(flagKey, []decide.OptimizelyDecideOptions{decide.IncludeReasons})
220220
s.OptimizelyClient.DecisionService.RemoveOnDecision(notificationID)
221221

@@ -275,7 +275,7 @@ func (s *OptimizelyUserContextTestSuite) TestDecideFeatureTestWithForcedDecision
275275
s.Nil(err)
276276

277277
user := s.OptimizelyClient.CreateUserContext(s.userID, nil)
278-
user.SetForcedDecision(flagKey, ruleKey, variationKey)
278+
user.SetForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKey, RuleKey: ruleKey}, decision.OptimizelyForcedDecision{VariationKey: variationKey})
279279
decision := user.Decide(flagKey, []decide.OptimizelyDecideOptions{decide.IncludeReasons})
280280

281281
s.Equal(variationKey, decision.VariationKey)
@@ -357,7 +357,7 @@ func (s *OptimizelyUserContextTestSuite) TestDecideRolloutWithForcedDecision() {
357357
s.Nil(err)
358358

359359
user := s.OptimizelyClient.CreateUserContext(s.userID, nil)
360-
user.SetForcedDecision(flagKey, ruleKey, variationKey)
360+
user.SetForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKey, RuleKey: ruleKey}, decision.OptimizelyForcedDecision{VariationKey: variationKey})
361361
decision := user.Decide(flagKey, []decide.OptimizelyDecideOptions{decide.IncludeReasons})
362362

363363
s.Equal(variationKey, decision.VariationKey)
@@ -1063,11 +1063,13 @@ func (s *OptimizelyUserContextTestSuite) TestForcedDecisionWithNilConfig() {
10631063
user := s.OptimizelyClient.CreateUserContext(s.userID, nil)
10641064
s.Nil(user.forcedDecisionService)
10651065

1066-
s.False(user.SetForcedDecision(flagKeyA, ruleKey, variationKeyA))
1066+
s.False(user.SetForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKeyA, RuleKey: ruleKey}, decision.OptimizelyForcedDecision{VariationKey: variationKeyA}))
10671067
s.Nil(user.forcedDecisionService)
10681068

1069-
s.Equal("", user.GetForcedDecision(flagKeyA, ruleKey))
1070-
s.False(user.RemoveForcedDecision(flagKeyA, ruleKey))
1069+
forcedDecision, err := user.GetForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKeyA, RuleKey: ruleKey})
1070+
s.Equal("", forcedDecision.VariationKey)
1071+
s.Error(err)
1072+
s.False(user.RemoveForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKeyA, RuleKey: ruleKey}))
10711073
s.False(user.RemoveAllForcedDecisions())
10721074
}
10731075

@@ -1081,25 +1083,39 @@ func (s *OptimizelyUserContextTestSuite) TestForcedDecision() {
10811083
// checking with nil forcedDecisionService
10821084
user := s.OptimizelyClient.CreateUserContext(s.userID, nil)
10831085
s.Nil(user.forcedDecisionService)
1084-
s.Equal("", user.GetForcedDecision(flagKeyA, ruleKey))
1085-
s.False(user.RemoveForcedDecision(flagKeyA, ruleKey))
1086+
forcedDecision, err := user.GetForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKeyA, RuleKey: ruleKey})
1087+
s.Equal("", forcedDecision.VariationKey)
1088+
s.Error(err)
1089+
s.False(user.RemoveForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKeyA, RuleKey: ruleKey}))
10861090
s.True(user.RemoveAllForcedDecisions())
10871091

10881092
// checking if forcedDecisionService was created using SetForcedDecision
1089-
s.True(user.SetForcedDecision(flagKeyA, ruleKey, variationKeyA))
1093+
s.True(user.SetForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKeyA, RuleKey: ruleKey}, decision.OptimizelyForcedDecision{VariationKey: variationKeyA}))
10901094
s.NotNil(user.forcedDecisionService)
10911095

1092-
s.True(user.SetForcedDecision(flagKeyB, ruleKey, variationKeyB))
1093-
s.Equal(variationKeyA, user.GetForcedDecision(flagKeyA, ruleKey))
1094-
s.Equal(variationKeyB, user.GetForcedDecision(flagKeyB, ruleKey))
1095-
1096-
s.True(user.RemoveForcedDecision(flagKeyA, ruleKey))
1097-
s.Equal("", user.GetForcedDecision(flagKeyA, ruleKey))
1098-
s.Equal(variationKeyB, user.GetForcedDecision(flagKeyB, ruleKey))
1096+
s.True(user.SetForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKeyB, RuleKey: ruleKey}, decision.OptimizelyForcedDecision{VariationKey: variationKeyB}))
1097+
forcedDecision, err = user.GetForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKeyA, RuleKey: ruleKey})
1098+
s.Equal(variationKeyA, forcedDecision.VariationKey)
1099+
s.NoError(err)
1100+
forcedDecision, err = user.GetForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKeyB, RuleKey: ruleKey})
1101+
s.Equal(variationKeyB, forcedDecision.VariationKey)
1102+
s.NoError(err)
1103+
1104+
s.True(user.RemoveForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKeyA, RuleKey: ruleKey}))
1105+
forcedDecision, err = user.GetForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKeyA, RuleKey: ruleKey})
1106+
s.Equal("", forcedDecision.VariationKey)
1107+
s.NoError(err)
1108+
forcedDecision, err = user.GetForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKeyB, RuleKey: ruleKey})
1109+
s.Equal(variationKeyB, forcedDecision.VariationKey)
1110+
s.NoError(err)
10991111

11001112
s.True(user.RemoveAllForcedDecisions())
1101-
s.Equal("", user.GetForcedDecision(flagKeyA, ruleKey))
1102-
s.Equal("", user.GetForcedDecision(flagKeyB, ruleKey))
1113+
forcedDecision, err = user.GetForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKeyA, RuleKey: ruleKey})
1114+
s.Equal("", forcedDecision.VariationKey)
1115+
s.Error(err)
1116+
forcedDecision, err = user.GetForcedDecision(decision.OptimizelyDecisionContext{FlagKey: flagKeyB, RuleKey: ruleKey})
1117+
s.Equal("", forcedDecision.VariationKey)
1118+
s.Error(err)
11031119
}
11041120

11051121
func TestOptimizelyUserContextTestSuite(t *testing.T) {

pkg/decision/feature_experiment_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func (f FeatureExperimentService) GetDecision(decisionContext FeatureDecisionCon
4848

4949
// Checking for forced decision
5050
if decisionContext.ForcedDecisionService != nil {
51-
forcedDecision, _reasons, err := decisionContext.ForcedDecisionService.FindValidatedForcedDecision(decisionContext.ProjectConfig, feature.Key, featureExperiment.Key, options)
51+
forcedDecision, _reasons, err := decisionContext.ForcedDecisionService.FindValidatedForcedDecision(decisionContext.ProjectConfig, OptimizelyDecisionContext{FlagKey: feature.Key, RuleKey: featureExperiment.Key}, options)
5252
reasons.Append(_reasons)
5353
if err == nil {
5454
featureDecision := FeatureDecision{

pkg/decision/feature_experiment_service_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func (s *FeatureExperimentServiceTestSuite) TestGetDecisionWithForcedDecision()
9191
},
9292
}
9393
s.mockConfig.On("GetFlagVariationsMap").Return(flagVariationsMap)
94-
s.testFeatureDecisionContext.ForcedDecisionService.SetForcedDecision(s.testFeatureDecisionContext.Feature.Key, testExp1113Key, expectedVariation.Key)
94+
s.testFeatureDecisionContext.ForcedDecisionService.SetForcedDecision(OptimizelyDecisionContext{FlagKey: s.testFeatureDecisionContext.Feature.Key, RuleKey: testExp1113Key}, OptimizelyForcedDecision{VariationKey: expectedVariation.Key})
9595

9696
testExperimentDecisionContext := ExperimentDecisionContext{
9797
Experiment: &testExp1113,
@@ -118,7 +118,7 @@ func (s *FeatureExperimentServiceTestSuite) TestGetDecisionWithForcedDecision()
118118
s.mockExperimentService.AssertNotCalled(s.T(), "GetDecision", testExperimentDecisionContext, testUserContext, options)
119119

120120
// invalid forced decision
121-
s.testFeatureDecisionContext.ForcedDecisionService.SetForcedDecision(s.testFeatureDecisionContext.Feature.Key, testExp1113Key, "invalid")
121+
s.testFeatureDecisionContext.ForcedDecisionService.SetForcedDecision(OptimizelyDecisionContext{FlagKey: s.testFeatureDecisionContext.Feature.Key, RuleKey: testExp1113Key}, OptimizelyForcedDecision{VariationKey: "invalid"})
122122

123123
expectedVariation = testExp1113.Variations["2223"]
124124
returnExperimentDecision := ExperimentDecision{

pkg/decision/forced_decision_service.go

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,62 +26,69 @@ import (
2626
"github.com/optimizely/go-sdk/pkg/entities"
2727
)
2828

29-
type forcedDecision struct {
30-
flagKey string
31-
ruleKey string
29+
// OptimizelyDecisionContext defines Decision Context
30+
type OptimizelyDecisionContext struct {
31+
FlagKey string
32+
RuleKey string
33+
}
34+
35+
// OptimizelyForcedDecision defines Forced Decision
36+
type OptimizelyForcedDecision struct {
37+
VariationKey string
3238
}
3339

3440
// ForcedDecisionService defines user contexts that the SDK will use to make decisions for.
3541
type ForcedDecisionService struct {
3642
UserID string
37-
forcedDecisions map[forcedDecision]string
43+
forcedDecisions map[OptimizelyDecisionContext]OptimizelyForcedDecision
3844
mutex *sync.RWMutex
3945
}
4046

4147
// NewForcedDecisionService returns an instance of the optimizely user context.
4248
func NewForcedDecisionService(userID string) *ForcedDecisionService {
4349
return &ForcedDecisionService{
4450
UserID: userID,
45-
forcedDecisions: map[forcedDecision]string{},
51+
forcedDecisions: map[OptimizelyDecisionContext]OptimizelyForcedDecision{},
4652
mutex: new(sync.RWMutex),
4753
}
4854
}
4955

5056
// SetForcedDecision sets the forced decision (variation key) for a given flag and an optional rule.
5157
// if rule key is empty, forced decision will be mapped against the flagKey.
5258
// returns true if the forced decision has been set successfully.
53-
func (f *ForcedDecisionService) SetForcedDecision(flagKey, ruleKey, variationKey string) bool {
54-
if flagKey == "" {
55-
return false
56-
}
59+
func (f *ForcedDecisionService) SetForcedDecision(context OptimizelyDecisionContext, decision OptimizelyForcedDecision) bool {
5760
f.mutex.Lock()
5861
defer f.mutex.Unlock()
59-
f.forcedDecisions[forcedDecision{flagKey: flagKey, ruleKey: ruleKey}] = variationKey
62+
f.forcedDecisions[context] = decision
6063
return true
6164
}
6265

6366
// GetForcedDecision returns the forced decision for a given flag and an optional rule
6467
// if rule key is empty, forced decision will be returned for the flagKey.
65-
func (f *ForcedDecisionService) GetForcedDecision(flagKey, ruleKey string) string {
68+
func (f *ForcedDecisionService) GetForcedDecision(context OptimizelyDecisionContext) (OptimizelyForcedDecision, error) {
6669
f.mutex.RLock()
6770
defer f.mutex.RUnlock()
71+
decision := OptimizelyForcedDecision{}
72+
err := errors.New("decision not found")
6873
if len(f.forcedDecisions) == 0 {
69-
return ""
74+
return decision, err
7075
}
71-
if variationKey, ok := f.forcedDecisions[forcedDecision{flagKey: flagKey, ruleKey: ruleKey}]; ok {
72-
return variationKey
76+
if forcedDecision, ok := f.forcedDecisions[context]; ok {
77+
return forcedDecision, nil
7378
}
74-
return ""
79+
return decision, err
7580
}
7681

7782
// RemoveForcedDecision removes the forced decision for a given flag and an optional rule.
7883
// if rule key is empty, forced decision will be removed for the flagKey.
79-
func (f *ForcedDecisionService) RemoveForcedDecision(flagKey, ruleKey string) bool {
84+
func (f *ForcedDecisionService) RemoveForcedDecision(context OptimizelyDecisionContext) bool {
8085
f.mutex.Lock()
8186
defer f.mutex.Unlock()
82-
decision := forcedDecision{flagKey: flagKey, ruleKey: ruleKey}
83-
if f.forcedDecisions[decision] != "" {
84-
f.forcedDecisions[decision] = ""
87+
if forcedDecision, ok := f.forcedDecisions[context]; ok && forcedDecision.VariationKey != "" {
88+
// modify the copy
89+
forcedDecision.VariationKey = ""
90+
// reassign map entry
91+
f.forcedDecisions[context] = forcedDecision
8592
return true
8693
}
8794
return false
@@ -91,29 +98,29 @@ func (f *ForcedDecisionService) RemoveForcedDecision(flagKey, ruleKey string) bo
9198
func (f *ForcedDecisionService) RemoveAllForcedDecisions() bool {
9299
f.mutex.Lock()
93100
defer f.mutex.Unlock()
94-
f.forcedDecisions = map[forcedDecision]string{}
101+
f.forcedDecisions = map[OptimizelyDecisionContext]OptimizelyForcedDecision{}
95102
return true
96103
}
97104

98105
// FindValidatedForcedDecision returns validated forced decision.
99-
func (f *ForcedDecisionService) FindValidatedForcedDecision(projectConfig config.ProjectConfig, flagKey, ruleKey string, options *decide.Options) (variation *entities.Variation, reasons decide.DecisionReasons, err error) {
106+
func (f *ForcedDecisionService) FindValidatedForcedDecision(projectConfig config.ProjectConfig, context OptimizelyDecisionContext, options *decide.Options) (variation *entities.Variation, reasons decide.DecisionReasons, err error) {
100107
decisionReasons := decide.NewDecisionReasons(options)
101-
variationKey := f.GetForcedDecision(flagKey, ruleKey)
102-
if variationKey == "" {
103-
return nil, decisionReasons, errors.New("decision not found")
108+
forcedDecision, err := f.GetForcedDecision(context)
109+
if err != nil {
110+
return nil, decisionReasons, err
104111
}
105112

106-
_variation, err := f.getFlagVariationByKey(projectConfig, flagKey, variationKey)
107-
target := "flag (" + flagKey + ")"
108-
if ruleKey != "" {
109-
target += ", rule (" + ruleKey + ")"
113+
_variation, err := f.getFlagVariationByKey(projectConfig, context.FlagKey, forcedDecision.VariationKey)
114+
target := "flag (" + context.FlagKey + ")"
115+
if context.RuleKey != "" {
116+
target += ", rule (" + context.RuleKey + ")"
110117
}
111118

112119
if err != nil {
113120
decisionReasons.AddInfo("Invalid variation is mapped to %s and user (%s) in the forced decision map.", target, f.UserID)
114121
return nil, decisionReasons, err
115122
}
116-
decisionReasons.AddInfo("Variation (%s) is mapped to %s and user (%s) in the forced decision map.", variationKey, target, f.UserID)
123+
decisionReasons.AddInfo("Variation (%s) is mapped to %s and user (%s) in the forced decision map.", forcedDecision.VariationKey, target, f.UserID)
117124
return _variation, decisionReasons, nil
118125
}
119126

@@ -132,7 +139,7 @@ func (f *ForcedDecisionService) getFlagVariationByKey(projectConfig config.Proje
132139
func (f *ForcedDecisionService) CreateCopy() *ForcedDecisionService {
133140
f.mutex.RLock()
134141
defer f.mutex.RUnlock()
135-
forceDecisions := map[forcedDecision]string{}
142+
forceDecisions := map[OptimizelyDecisionContext]OptimizelyForcedDecision{}
136143
for k, v := range f.forcedDecisions {
137144
forceDecisions[k] = v
138145
}

0 commit comments

Comments
 (0)