-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(ForcedDecisions): add forced-decisions APIs to OptimizelyUserContext #324
Changes from 17 commits
8588797
3c6362e
69f56e9
dbfba69
5271838
32600af
856909c
3f0d19c
3b036df
638783a
feb7647
7cd2554
4bad798
db8fc1d
a22ee99
5f5d52b
c3fac23
4d033d3
808356e
6ccaf0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
/**************************************************************************** | ||
* Copyright 2019-2020, Optimizely, Inc. and contributors * | ||
* Copyright 2019-2021, 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. * | ||
|
@@ -28,6 +28,7 @@ import ( | |
"github.com/optimizely/go-sdk/pkg/config" | ||
"github.com/optimizely/go-sdk/pkg/decide" | ||
"github.com/optimizely/go-sdk/pkg/decision" | ||
pkgReasons "github.com/optimizely/go-sdk/pkg/decision/reasons" | ||
"github.com/optimizely/go-sdk/pkg/entities" | ||
"github.com/optimizely/go-sdk/pkg/event" | ||
"github.com/optimizely/go-sdk/pkg/logging" | ||
|
@@ -52,7 +53,7 @@ type OptimizelyClient struct { | |
// CreateUserContext creates a context of the user for which decision APIs will be called. | ||
// A user context will be created successfully even when the SDK is not fully configured yet. | ||
func (o *OptimizelyClient) CreateUserContext(userID string, attributes map[string]interface{}) OptimizelyUserContext { | ||
return newOptimizelyUserContext(o, userID, attributes) | ||
return newOptimizelyUserContext(o, userID, attributes, nil) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why passing nil? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we don't have any instance of forcedDecisionService to pass since a new |
||
} | ||
|
||
func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string, options *decide.Options) OptimizelyDecision { | ||
|
@@ -73,7 +74,9 @@ func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string, | |
} | ||
}() | ||
|
||
decisionContext := decision.FeatureDecisionContext{} | ||
decisionContext := decision.FeatureDecisionContext{ | ||
ForcedDecisionService: userContext.forcedDecisionService, | ||
} | ||
projectConfig, err := o.getProjectConfig() | ||
if err != nil { | ||
return NewErrorDecision(key, userContext, decide.GetDecideError(decide.SDKNotReady)) | ||
|
@@ -95,9 +98,30 @@ func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string, | |
allOptions := o.getAllOptions(options) | ||
decisionReasons := decide.NewDecisionReasons(&allOptions) | ||
decisionContext.Variable = entities.Variable{} | ||
var featureDecision decision.FeatureDecision | ||
var reasons decide.DecisionReasons | ||
|
||
featureDecision, reasons, err := o.DecisionService.GetFeatureDecision(decisionContext, usrContext, &allOptions) | ||
decisionReasons.Append(reasons) | ||
// To avoid cyclo-complexity warning | ||
findRegularDecision := func() { | ||
// regular decision | ||
featureDecision, reasons, err = o.DecisionService.GetFeatureDecision(decisionContext, usrContext, &allOptions) | ||
decisionReasons.Append(reasons) | ||
} | ||
|
||
// check forced-decisions first | ||
// Passing empty rule-key because checking mapping with flagKey only | ||
if userContext.forcedDecisionService != nil { | ||
var variation *entities.Variation | ||
variation, reasons, err = userContext.forcedDecisionService.FindValidatedForcedDecision(projectConfig, key, "", &allOptions) | ||
decisionReasons.Append(reasons) | ||
if err != nil { | ||
findRegularDecision() | ||
} else { | ||
featureDecision = decision.FeatureDecision{Decision: decision.Decision{Reason: pkgReasons.ForcedDecisionFound}, Variation: variation, Source: decision.FeatureTest} | ||
} | ||
} else { | ||
findRegularDecision() | ||
} | ||
|
||
if err != nil { | ||
o.logger.Warning(fmt.Sprintf(`Received error while making a decision for feature "%s": %s`, key, err)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
/**************************************************************************** | ||
* Copyright 2020, Optimizely, Inc. and contributors * | ||
* Copyright 2020-2021, 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. * | ||
|
@@ -21,6 +21,7 @@ import ( | |
"sync" | ||
|
||
"github.com/optimizely/go-sdk/pkg/decide" | ||
"github.com/optimizely/go-sdk/pkg/decision" | ||
"github.com/optimizely/go-sdk/pkg/entities" | ||
) | ||
|
||
|
@@ -29,23 +30,24 @@ type OptimizelyUserContext struct { | |
UserID string `json:"userId"` | ||
Attributes map[string]interface{} `json:"attributes"` | ||
|
||
optimizely *OptimizelyClient | ||
mutex *sync.RWMutex | ||
optimizely *OptimizelyClient | ||
forcedDecisionService *decision.ForcedDecisionService | ||
mutex *sync.RWMutex | ||
} | ||
|
||
// returns an instance of the optimizely user context. | ||
func newOptimizelyUserContext(optimizely *OptimizelyClient, userID string, attributes map[string]interface{}) OptimizelyUserContext { | ||
func newOptimizelyUserContext(optimizely *OptimizelyClient, userID string, attributes map[string]interface{}, forcedDecisionService *decision.ForcedDecisionService) OptimizelyUserContext { | ||
// store a copy of the provided attributes so it isn't affected by changes made afterwards. | ||
if attributes == nil { | ||
attributes = map[string]interface{}{} | ||
} | ||
attributesCopy := copyUserAttributes(attributes) | ||
|
||
jaeopt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return OptimizelyUserContext{ | ||
UserID: userID, | ||
Attributes: attributesCopy, | ||
optimizely: optimizely, | ||
mutex: new(sync.RWMutex), | ||
UserID: userID, | ||
Attributes: attributesCopy, | ||
optimizely: optimizely, | ||
forcedDecisionService: forcedDecisionService, | ||
mutex: new(sync.RWMutex), | ||
} | ||
} | ||
|
||
|
@@ -66,6 +68,13 @@ func (o OptimizelyUserContext) GetUserAttributes() map[string]interface{} { | |
return copyUserAttributes(o.Attributes) | ||
} | ||
|
||
func (o OptimizelyUserContext) getForcedDecisionService() *decision.ForcedDecisionService { | ||
if o.forcedDecisionService != nil { | ||
return o.forcedDecisionService.CreateCopy() | ||
} | ||
return nil | ||
} | ||
|
||
// SetAttribute sets an attribute for a given key. | ||
func (o *OptimizelyUserContext) SetAttribute(key string, value interface{}) { | ||
o.mutex.Lock() | ||
|
@@ -80,21 +89,21 @@ func (o *OptimizelyUserContext) SetAttribute(key string, value interface{}) { | |
// all data required to deliver the flag or experiment. | ||
func (o *OptimizelyUserContext) Decide(key string, options []decide.OptimizelyDecideOptions) OptimizelyDecision { | ||
// use a copy of the user context so that any changes to the original context are not reflected inside the decision | ||
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes()) | ||
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we use "o.forcedDecisionService" here? We do not want to CreateCopy until SetForcedDecision is called. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My bad. It looks good as is :) |
||
return o.optimizely.decide(userContextCopy, key, convertDecideOptions(options)) | ||
} | ||
|
||
// DecideAll returns a key-map of decision results for all active flag keys with options. | ||
func (o *OptimizelyUserContext) DecideAll(options []decide.OptimizelyDecideOptions) map[string]OptimizelyDecision { | ||
// use a copy of the user context so that any changes to the original context are not reflected inside the decision | ||
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes()) | ||
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService()) | ||
return o.optimizely.decideAll(userContextCopy, convertDecideOptions(options)) | ||
} | ||
|
||
// DecideForKeys returns a key-map of decision results for multiple flag keys and options. | ||
func (o *OptimizelyUserContext) DecideForKeys(keys []string, options []decide.OptimizelyDecideOptions) map[string]OptimizelyDecision { | ||
// use a copy of the user context so that any changes to the original context are not reflected inside the decision | ||
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes()) | ||
userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService()) | ||
return o.optimizely.decideForKeys(userContextCopy, keys, convertDecideOptions(options)) | ||
} | ||
|
||
|
@@ -108,6 +117,55 @@ func (o *OptimizelyUserContext) TrackEvent(eventKey string, eventTags map[string | |
return o.optimizely.Track(eventKey, userContext, eventTags) | ||
} | ||
|
||
// SetForcedDecision sets the forced decision (variation key) for a given flag and an optional rule. | ||
// returns true if the forced decision has been set successfully. | ||
func (o *OptimizelyUserContext) SetForcedDecision(flagKey, ruleKey, variationKey string) bool { | ||
if _, err := o.optimizely.getProjectConfig(); err != nil { | ||
o.optimizely.logger.Error("Optimizely instance is not valid, failing setForcedDecision call.", err) | ||
return false | ||
} | ||
if o.forcedDecisionService == nil { | ||
o.forcedDecisionService = decision.NewForcedDecisionService(o.GetUserID()) | ||
} | ||
return o.forcedDecisionService.SetForcedDecision(flagKey, ruleKey, variationKey) | ||
} | ||
|
||
// GetForcedDecision returns the forced decision for a given flag and an optional rule | ||
func (o *OptimizelyUserContext) GetForcedDecision(flagKey, ruleKey string) string { | ||
if _, err := o.optimizely.getProjectConfig(); err != nil { | ||
o.optimizely.logger.Error("Optimizely instance is not valid, failing getForcedDecision call.", err) | ||
return "" | ||
} | ||
if o.forcedDecisionService == nil { | ||
return "" | ||
} | ||
return o.forcedDecisionService.GetForcedDecision(flagKey, ruleKey) | ||
} | ||
|
||
// RemoveForcedDecision removes the forced decision for a given flag and an optional rule. | ||
func (o *OptimizelyUserContext) RemoveForcedDecision(flagKey, ruleKey string) bool { | ||
if _, err := o.optimizely.getProjectConfig(); err != nil { | ||
o.optimizely.logger.Error("Optimizely instance is not valid, failing removeForcedDecision call.", err) | ||
return false | ||
} | ||
if o.forcedDecisionService == nil { | ||
return false | ||
} | ||
return o.forcedDecisionService.RemoveForcedDecision(flagKey, ruleKey) | ||
} | ||
|
||
// RemoveAllForcedDecisions removes all forced decisions bound to this user context. | ||
func (o *OptimizelyUserContext) RemoveAllForcedDecisions() bool { | ||
if _, err := o.optimizely.getProjectConfig(); err != nil { | ||
o.optimizely.logger.Error("Optimizely instance is not valid, failing removeForcedDecision call.", err) | ||
return false | ||
} | ||
if o.forcedDecisionService == nil { | ||
return true | ||
} | ||
return o.forcedDecisionService.RemoveAllForcedDecisions() | ||
} | ||
|
||
func copyUserAttributes(attributes map[string]interface{}) (attributesCopy map[string]interface{}) { | ||
if attributes != nil { | ||
attributesCopy = make(map[string]interface{}) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any reason for this named package?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is because there are several properties named
reasons
throughout the code.