@@ -30,25 +30,61 @@ import (
3030type RolloutService struct {
3131 audienceTreeEvaluator evaluator.TreeEvaluator
3232 experimentBucketerService ExperimentService
33- logger logging.OptimizelyLogProducer
33+ logger logging.OptimizelyLogProducer
3434}
3535
3636// NewRolloutService returns a new instance of the Rollout service
3737func NewRolloutService (sdkKey string ) * RolloutService {
3838 return & RolloutService {
39- logger :logging .GetLogger (sdkKey , "RolloutService" ),
39+ logger : logging .GetLogger (sdkKey , "RolloutService" ),
4040 audienceTreeEvaluator : evaluator .NewMixedTreeEvaluator (),
4141 experimentBucketerService : NewExperimentBucketerService (logging .GetLogger (sdkKey , "ExperimentBucketerService" )),
4242 }
4343}
4444
4545// GetDecision returns a decision for the given feature and user context
4646func (r RolloutService ) GetDecision (decisionContext FeatureDecisionContext , userContext entities.UserContext ) (FeatureDecision , error ) {
47+
4748 featureDecision := FeatureDecision {
4849 Source : Rollout ,
4950 }
5051 feature := decisionContext .Feature
5152 rollout := feature .Rollout
53+
54+ evaluateConditionTree := func (experiment * entities.Experiment ) bool {
55+ condTreeParams := entities .NewTreeParameters (& userContext , decisionContext .ProjectConfig .GetAudienceMap ())
56+ evalResult , _ := r .audienceTreeEvaluator .Evaluate (experiment .AudienceConditionTree , condTreeParams )
57+ if ! evalResult {
58+ featureDecision .Reason = reasons .FailedRolloutTargeting
59+ r .logger .Debug (fmt .Sprintf (`User "%s" failed targeting for feature rollout with key "%s".` , userContext .ID , feature .Key ))
60+ }
61+ return evalResult
62+ }
63+
64+ getFeatureDecision := func (experiment * entities.Experiment , decision * ExperimentDecision ) (FeatureDecision , error ) {
65+ // translate the experiment reason into a more rollouts-appropriate reason
66+ switch decision .Reason {
67+ case reasons .NotBucketedIntoVariation :
68+ featureDecision .Decision = Decision {Reason : reasons .FailedRolloutBucketing }
69+ case reasons .BucketedIntoVariation :
70+ featureDecision .Decision = Decision {Reason : reasons .BucketedIntoRollout }
71+ default :
72+ featureDecision .Decision = decision .Decision
73+ }
74+
75+ featureDecision .Experiment = * experiment
76+ featureDecision .Variation = decision .Variation
77+ r .logger .Debug (fmt .Sprintf (`Decision made for user "%s" for feature rollout with key "%s": %s.` , userContext .ID , feature .Key , featureDecision .Reason ))
78+ return featureDecision , nil
79+ }
80+
81+ getExperimentDecisionContext := func (experiment * entities.Experiment ) ExperimentDecisionContext {
82+ return ExperimentDecisionContext {
83+ Experiment : experiment ,
84+ ProjectConfig : decisionContext .ProjectConfig ,
85+ }
86+ }
87+
5288 if rollout .ID == "" {
5389 featureDecision .Reason = reasons .NoRolloutForFeature
5490 return featureDecision , nil
@@ -60,38 +96,30 @@ func (r RolloutService) GetDecision(decisionContext FeatureDecisionContext, user
6096 return featureDecision , nil
6197 }
6298
63- // For now, Rollouts is just a single experiment layer
64- experiment := rollout .Experiments [0 ]
65- experimentDecisionContext := ExperimentDecisionContext {
66- Experiment : & experiment ,
67- ProjectConfig : decisionContext .ProjectConfig ,
68- }
69-
70- // if user fails rollout targeting rule we return out of it
71- if experiment .AudienceConditionTree != nil {
72- condTreeParams := entities .NewTreeParameters (& userContext , decisionContext .ProjectConfig .GetAudienceMap ())
73- evalResult , _ := r .audienceTreeEvaluator .Evaluate (experiment .AudienceConditionTree , condTreeParams )
74- if ! evalResult {
75- featureDecision .Reason = reasons .FailedRolloutTargeting
76- r .logger .Debug (fmt .Sprintf (`User "%s" failed targeting for feature rollout with key "%s".` , userContext .ID , feature .Key ))
77- return featureDecision , nil
99+ for index := 0 ; index < numberOfExperiments - 1 ; index ++ {
100+ experiment := & rollout .Experiments [index ]
101+ experimentDecisionContext := getExperimentDecisionContext (experiment )
102+ // Move to next evaluation if condition tree is available and evaluation fails
103+ if experiment .AudienceConditionTree != nil && ! evaluateConditionTree (experiment ) {
104+ // Evaluate this user for the next rule
105+ continue
106+ }
107+ decision , _ := r .experimentBucketerService .GetDecision (experimentDecisionContext , userContext )
108+ if decision .Variation == nil {
109+ // Evaluate fall back rule / last rule now
110+ break
78111 }
112+ return getFeatureDecision (experiment , & decision )
79113 }
80114
81- decision , _ := r .experimentBucketerService .GetDecision (experimentDecisionContext , userContext )
82- // translate the experiment reason into a more rollouts-appropriate reason
83- switch decision .Reason {
84- case reasons .NotBucketedIntoVariation :
85- featureDecision .Decision = Decision {Reason : reasons .FailedRolloutBucketing }
86- case reasons .BucketedIntoVariation :
87- featureDecision .Decision = Decision {Reason : reasons .BucketedIntoRollout }
88- default :
89- featureDecision .Decision = decision .Decision
115+ // fall back rule / last rule
116+ experiment := & rollout .Experiments [numberOfExperiments - 1 ]
117+ experimentDecisionContext := getExperimentDecisionContext (experiment )
118+ // Move to bucketing if conditionTree is unavailable or evaluation passes
119+ if experiment .AudienceConditionTree == nil || evaluateConditionTree (experiment ) {
120+ decision , _ := r .experimentBucketerService .GetDecision (experimentDecisionContext , userContext )
121+ return getFeatureDecision (experiment , & decision )
90122 }
91123
92- featureDecision .Experiment = experiment
93- featureDecision .Variation = decision .Variation
94- r .logger .Debug (fmt .Sprintf (`Decision made for user "%s" for feature rollout with key "%s": %s.` , userContext .ID , feature .Key , featureDecision .Reason ))
95-
96124 return featureDecision , nil
97125}
0 commit comments