Skip to content

Commit 238332f

Browse files
authored
feat(decide): add decide, decideAll and decideForKeys API. (#299)
1 parent 273fcd9 commit 238332f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1459
-184
lines changed

pkg/client/client.go

Lines changed: 185 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type OptimizelyClient struct {
4646
notificationCenter notification.Center
4747
execGroup *utils.ExecGroup
4848
logger logging.OptimizelyLogProducer
49-
defaultDecideOptions decide.OptimizelyDecideOptions
49+
defaultDecideOptions *decide.Options
5050
}
5151

5252
// CreateUserContext creates a context of the user for which decision APIs will be called.
@@ -55,6 +55,156 @@ func (o *OptimizelyClient) CreateUserContext(userID string, attributes map[strin
5555
return newOptimizelyUserContext(o, userID, attributes)
5656
}
5757

58+
func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string, options *decide.Options) OptimizelyDecision {
59+
var err error
60+
defer func() {
61+
if r := recover(); r != nil {
62+
switch t := r.(type) {
63+
case error:
64+
err = t
65+
case string:
66+
err = errors.New(t)
67+
default:
68+
err = errors.New("unexpected error")
69+
}
70+
errorMessage := fmt.Sprintf("decide call, optimizely SDK is panicking with the error:")
71+
o.logger.Error(errorMessage, err)
72+
o.logger.Debug(string(debug.Stack()))
73+
}
74+
}()
75+
76+
decisionContext := decision.FeatureDecisionContext{}
77+
projectConfig, err := o.getProjectConfig()
78+
if err != nil {
79+
return NewErrorDecision(key, userContext, decide.GetDecideError(decide.SDKNotReady))
80+
}
81+
decisionContext.ProjectConfig = projectConfig
82+
83+
feature, err := projectConfig.GetFeatureByKey(key)
84+
if err != nil {
85+
return NewErrorDecision(key, userContext, decide.GetDecideError(decide.FlagKeyInvalid, key))
86+
}
87+
decisionContext.Feature = &feature
88+
89+
usrContext := entities.UserContext{
90+
ID: userContext.GetUserID(),
91+
Attributes: userContext.GetUserAttributes(),
92+
}
93+
var variationKey string
94+
var eventSent, flagEnabled bool
95+
allOptions := o.getAllOptions(options)
96+
decisionReasons := decide.NewDecisionReasons(&allOptions)
97+
decisionContext.Variable = entities.Variable{}
98+
99+
featureDecision, err := o.DecisionService.GetFeatureDecision(decisionContext, usrContext, &allOptions, decisionReasons)
100+
if err != nil {
101+
o.logger.Warning(fmt.Sprintf(`Received error while making a decision for feature "%s": %s`, key, err))
102+
}
103+
104+
if featureDecision.Variation != nil {
105+
variationKey = featureDecision.Variation.Key
106+
flagEnabled = featureDecision.Variation.FeatureEnabled
107+
}
108+
109+
if !allOptions.DisableDecisionEvent {
110+
if ue, ok := event.CreateImpressionUserEvent(decisionContext.ProjectConfig, featureDecision.Experiment,
111+
featureDecision.Variation, usrContext, key, featureDecision.Experiment.Key, featureDecision.Source, flagEnabled); ok {
112+
o.EventProcessor.ProcessEvent(ue)
113+
eventSent = true
114+
}
115+
}
116+
117+
variableMap := map[string]interface{}{}
118+
if !allOptions.ExcludeVariables {
119+
variableMap = o.getDecisionVariableMap(feature, featureDecision.Variation, flagEnabled, decisionReasons)
120+
}
121+
optimizelyJSON := optimizelyjson.NewOptimizelyJSONfromMap(variableMap)
122+
reasonsToReport := decisionReasons.ToReport()
123+
ruleKey := featureDecision.Experiment.Key
124+
125+
if o.notificationCenter != nil {
126+
decisionNotification := decision.FlagNotification(key, variationKey, ruleKey, flagEnabled, eventSent, usrContext, variableMap, reasonsToReport)
127+
o.logger.Info(fmt.Sprintf(`Feature "%s" is enabled for user "%s"? %v`, key, usrContext.ID, flagEnabled))
128+
if e := o.notificationCenter.Send(notification.Decision, *decisionNotification); e != nil {
129+
o.logger.Warning("Problem with sending notification")
130+
}
131+
}
132+
133+
return NewOptimizelyDecision(variationKey, ruleKey, key, flagEnabled, optimizelyJSON, userContext, reasonsToReport)
134+
}
135+
136+
func (o *OptimizelyClient) decideForKeys(userContext OptimizelyUserContext, keys []string, options *decide.Options) map[string]OptimizelyDecision {
137+
var err error
138+
defer func() {
139+
if r := recover(); r != nil {
140+
switch t := r.(type) {
141+
case error:
142+
err = t
143+
case string:
144+
err = errors.New(t)
145+
default:
146+
err = errors.New("unexpected error")
147+
}
148+
errorMessage := fmt.Sprintf("decideForKeys call, optimizely SDK is panicking with the error:")
149+
o.logger.Error(errorMessage, err)
150+
o.logger.Debug(string(debug.Stack()))
151+
}
152+
}()
153+
154+
decisionMap := map[string]OptimizelyDecision{}
155+
if _, err = o.getProjectConfig(); err != nil {
156+
o.logger.Error("Optimizely instance is not valid, failing decideForKeys call.", err)
157+
return decisionMap
158+
}
159+
160+
if len(keys) == 0 {
161+
return decisionMap
162+
}
163+
164+
enabledFlagsOnly := o.getAllOptions(options).EnabledFlagsOnly
165+
for _, key := range keys {
166+
optimizelyDecision := o.decide(userContext, key, options)
167+
if !enabledFlagsOnly || optimizelyDecision.GetEnabled() {
168+
decisionMap[key] = optimizelyDecision
169+
}
170+
}
171+
172+
return decisionMap
173+
}
174+
175+
func (o *OptimizelyClient) decideAll(userContext OptimizelyUserContext, options *decide.Options) map[string]OptimizelyDecision {
176+
177+
var err error
178+
defer func() {
179+
if r := recover(); r != nil {
180+
switch t := r.(type) {
181+
case error:
182+
err = t
183+
case string:
184+
err = errors.New(t)
185+
default:
186+
err = errors.New("unexpected error")
187+
}
188+
errorMessage := fmt.Sprintf("decideAll call, optimizely SDK is panicking with the error:")
189+
o.logger.Error(errorMessage, err)
190+
o.logger.Debug(string(debug.Stack()))
191+
}
192+
}()
193+
194+
projectConfig, err := o.getProjectConfig()
195+
if err != nil {
196+
o.logger.Error("Optimizely instance is not valid, failing decideAll call.", err)
197+
return map[string]OptimizelyDecision{}
198+
}
199+
200+
allFlagKeys := []string{}
201+
for _, flag := range projectConfig.GetFeatureList() {
202+
allFlagKeys = append(allFlagKeys, flag.Key)
203+
}
204+
205+
return o.decideForKeys(userContext, allFlagKeys, options)
206+
}
207+
58208
// Activate returns the key of the variation the user is bucketed into and queues up an impression event to be sent to
59209
// the Optimizely log endpoint for results processing.
60210
func (o *OptimizelyClient) Activate(experimentKey string, userContext entities.UserContext) (result string, err error) {
@@ -657,7 +807,7 @@ func (o *OptimizelyClient) getFeatureDecision(featureKey, variableKey string, us
657807
}
658808

659809
decisionContext.Variable = variable
660-
options := decide.OptimizelyDecideOptions{}
810+
options := &decide.Options{}
661811
featureDecision, err = o.DecisionService.GetFeatureDecision(decisionContext, userContext, options, decide.NewDecisionReasons(options))
662812
if err != nil {
663813
o.logger.Warning(fmt.Sprintf(`Received error while making a decision for feature "%s": %s`, featureKey, err))
@@ -688,7 +838,7 @@ func (o *OptimizelyClient) getExperimentDecision(experimentKey string, userConte
688838
ProjectConfig: projectConfig,
689839
}
690840

691-
options := decide.OptimizelyDecideOptions{}
841+
options := &decide.Options{}
692842
experimentDecision, err = o.DecisionService.GetExperimentDecision(decisionContext, userContext, options, decide.NewDecisionReasons(options))
693843
if err != nil {
694844
o.logger.Warning(fmt.Sprintf(`Received error while making a decision for experiment "%s": %s`, experimentKey, err))
@@ -776,18 +926,49 @@ func (o *OptimizelyClient) getProjectConfig() (projectConfig config.ProjectConfi
776926
return projectConfig, nil
777927
}
778928

929+
func (o *OptimizelyClient) getAllOptions(options *decide.Options) decide.Options {
930+
return decide.Options{
931+
DisableDecisionEvent: o.defaultDecideOptions.DisableDecisionEvent || options.DisableDecisionEvent,
932+
EnabledFlagsOnly: o.defaultDecideOptions.EnabledFlagsOnly || options.EnabledFlagsOnly,
933+
ExcludeVariables: o.defaultDecideOptions.ExcludeVariables || options.ExcludeVariables,
934+
IgnoreUserProfileService: o.defaultDecideOptions.IgnoreUserProfileService || options.IgnoreUserProfileService,
935+
IncludeReasons: o.defaultDecideOptions.IncludeReasons || options.IncludeReasons,
936+
}
937+
}
938+
779939
// GetOptimizelyConfig returns OptimizelyConfig object
780940
func (o *OptimizelyClient) GetOptimizelyConfig() (optimizelyConfig *config.OptimizelyConfig) {
781941

782942
return o.ConfigManager.GetOptimizelyConfig()
783-
784943
}
785944

786945
// Close closes the Optimizely instance and stops any ongoing tasks from its children components.
787946
func (o *OptimizelyClient) Close() {
788947
o.execGroup.TerminateAndWait()
789948
}
790949

950+
func (o *OptimizelyClient) getDecisionVariableMap(feature entities.Feature, variation *entities.Variation, featureEnabled bool, decisionReasons decide.DecisionReasons) map[string]interface{} {
951+
valuesMap := map[string]interface{}{}
952+
953+
for _, v := range feature.VariableMap {
954+
val := v.DefaultValue
955+
956+
if featureEnabled {
957+
if variable, ok := variation.Variables[v.ID]; ok {
958+
val = variable.Value
959+
}
960+
}
961+
962+
typedValue, typedError := o.getTypedValue(val, v.Type)
963+
if typedError != nil {
964+
decisionReasons.AddError(decide.GetDecideMessage(decide.VariableValueInvalid, v.Key))
965+
}
966+
valuesMap[v.Key] = typedValue
967+
}
968+
969+
return valuesMap
970+
}
971+
791972
func isNil(v interface{}) bool {
792973
return v == nil || (reflect.ValueOf(v).Kind() == reflect.Ptr && reflect.ValueOf(v).IsNil())
793974
}

0 commit comments

Comments
 (0)