Skip to content

Commit b1b3e98

Browse files
author
Michael Ng
authored
fix(decision): Logs produced by the various decision services. (#180)
1 parent de9c888 commit b1b3e98

10 files changed

+114
-70
lines changed

pkg/client/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ func (o *OptimizelyClient) getExperimentDecision(experimentKey string, userConte
414414
result := experimentDecision.Variation.Key
415415
logger.Info(fmt.Sprintf(`User "%s" is bucketed into variation "%s" of experiment "%s".`, userContext.ID, result, experimentKey))
416416
} else {
417-
logger.Info(fmt.Sprintf(`User "%s" is not bucketed into any variation for experiment "%s".`, userContext.ID, experimentKey))
417+
logger.Info(fmt.Sprintf(`User "%s" is not bucketed into any variation for experiment "%s": %s.`, userContext.ID, experimentKey, experimentDecision.Reason))
418418
}
419419

420420
return decisionContext, experimentDecision, err

pkg/decision/composite_experiment_service.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
package decision
1919

2020
import (
21-
"errors"
2221
"fmt"
2322

2423
"github.com/optimizely/go-sdk/pkg/entities"
@@ -84,13 +83,11 @@ func NewCompositeExperimentService(options ...CESOptionFunc) *CompositeExperimen
8483
}
8584

8685
// GetDecision returns a decision for the given experiment and user context
87-
func (s CompositeExperimentService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext) (ExperimentDecision, error) {
88-
89-
experimentDecision := ExperimentDecision{}
86+
func (s CompositeExperimentService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext) (decision ExperimentDecision, err error) {
9087

9188
// Run through the various decision services until we get a decision
9289
for _, experimentService := range s.experimentServices {
93-
decision, err := experimentService.GetDecision(decisionContext, userContext)
90+
decision, err = experimentService.GetDecision(decisionContext, userContext)
9491
if err != nil {
9592
ceLogger.Debug(fmt.Sprintf("%v", err))
9693
}
@@ -99,5 +96,5 @@ func (s CompositeExperimentService) GetDecision(decisionContext ExperimentDecisi
9996
}
10097
}
10198

102-
return experimentDecision, errors.New("no decision was made")
99+
return decision, err
103100
}

pkg/decision/composite_experiment_service_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func (s *CompositeExperimentTestSuite) TestGetDecisionNoDecisionsMade() {
110110
}
111111
decision, err := compositeExperimentService.GetDecision(s.testDecisionContext, testUserContext)
112112

113-
s.Error(err)
113+
s.NoError(err)
114114
s.Equal(expectedExperimentDecision2, decision)
115115
s.mockExperimentService.AssertExpectations(s.T())
116116
s.mockExperimentService2.AssertExpectations(s.T())

pkg/decision/composite_feature_service.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ func (f CompositeFeatureService) GetDecision(decisionContext FeatureDecisionCont
5050
if err != nil {
5151
cfLogger.Debug(fmt.Sprintf("%v", err))
5252
}
53+
5354
if featureDecision.Variation != nil && err == nil {
5455
return featureDecision, err
5556
}

pkg/decision/experiment_bucketer_service.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ func (s ExperimentBucketerService) GetDecision(decisionContext ExperimentDecisio
7171
bLogger.Debug(fmt.Sprintf(`Error computing bucketing ID for experiment "%s": "%s"`, experiment.Key, err.Error()))
7272
}
7373

74-
bLogger.Debug(fmt.Sprintf(`Using bucketing ID: "%s"`, bucketingID))
74+
if bucketingID != userContext.ID {
75+
bLogger.Debug(fmt.Sprintf(`Using bucketing ID: "%s" for user "%s"`, bucketingID, userContext.ID))
76+
}
7577
// @TODO: handle error from bucketer
7678
variation, reason, _ := s.bucketer.Bucket(bucketingID, *experiment, group)
7779
experimentDecision.Reason = reason

pkg/decision/feature_experiment_service.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ import (
2121
"fmt"
2222

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

27+
var fesLogger = logging.GetLogger("FeatureExperimentService")
28+
2629
// FeatureExperimentService helps evaluate feature test associated with the feature
2730
type FeatureExperimentService struct {
2831
compositeExperimentService ExperimentService
@@ -47,6 +50,13 @@ func (f FeatureExperimentService) GetDecision(decisionContext FeatureDecisionCon
4750
}
4851

4952
experimentDecision, err := f.compositeExperimentService.GetDecision(experimentDecisionContext, userContext)
53+
fesLogger.Debug(fmt.Sprintf(
54+
`Decision made for feature test with key "%s" for user "%s" with the following reason: "%s".`,
55+
feature.Key,
56+
userContext.ID,
57+
experimentDecision.Reason,
58+
))
59+
5060
// Variation not nil means we got a decision and should return it
5161
if experimentDecision.Variation != nil {
5262
featureDecision := FeatureDecision{
@@ -56,12 +66,6 @@ func (f FeatureExperimentService) GetDecision(decisionContext FeatureDecisionCon
5666
Source: FeatureTest,
5767
}
5868

59-
cfLogger.Debug(fmt.Sprintf(
60-
`Decision made for feature test with key "%s" for user "%s" with the following reason: "%s".`,
61-
feature.Key,
62-
userContext.ID,
63-
featureDecision.Reason,
64-
))
6569
return featureDecision, err
6670
}
6771
}

pkg/decision/persisting_experiment_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424
"github.com/optimizely/go-sdk/pkg/logging"
2525
)
2626

27-
var pesLogger = logging.GetLogger("pkg/decision/persisting_experiment_service")
27+
var pesLogger = logging.GetLogger("PersistingExperimentService")
2828

2929
// PersistingExperimentService attempts to retrieve a saved decision from the user profile service
3030
// for the user before having the ExperimentBucketerService compute it.

pkg/decision/reasons/reason.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ const (
2525
BucketedVariationNotFound Reason = "Bucketed variation not found"
2626
// BucketedIntoVariation - the user is bucketed into a variation for the given experiment
2727
BucketedIntoVariation Reason = "Bucketed into variation"
28+
// BucketedIntoFeatureTest - the user is bucketed into a variation for the given feature test
29+
BucketedIntoFeatureTest Reason = "Bucketed into feature test"
30+
// BucketedIntoRollout - the user is bucketed into a variation for the given feature rollout
31+
BucketedIntoRollout Reason = "Bucketed into feature rollout"
32+
// FailedRolloutBucketing - the user is not bucketed into the feature rollout
33+
FailedRolloutBucketing Reason = "Not bucketed into rollout"
2834
// FailedRolloutTargeting - the user does not meet the rollout targeting rules
2935
FailedRolloutTargeting Reason = "Does not meet rollout targeting rule"
3036
// FailedAudienceTargeting - the user failed the audience targeting conditions

pkg/decision/rollout_service.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@
1818
package decision
1919

2020
import (
21+
"fmt"
22+
2123
"github.com/optimizely/go-sdk/pkg/decision/evaluator"
2224
"github.com/optimizely/go-sdk/pkg/decision/reasons"
25+
"github.com/optimizely/go-sdk/pkg/logging"
2326

2427
"github.com/optimizely/go-sdk/pkg/entities"
2528
)
2629

30+
var rsLogger = logging.GetLogger("RolloutService")
31+
2732
// RolloutService makes a feature decision for a given feature rollout
2833
type RolloutService struct {
2934
audienceTreeEvaluator evaluator.TreeEvaluator
@@ -69,13 +74,25 @@ func (r RolloutService) GetDecision(decisionContext FeatureDecisionContext, user
6974
evalResult := r.audienceTreeEvaluator.Evaluate(experiment.AudienceConditionTree, condTreeParams)
7075
if !evalResult {
7176
featureDecision.Reason = reasons.FailedRolloutTargeting
77+
rsLogger.Debug(fmt.Sprintf(`User "%s" failed targeting for feature rollout with key "%s".`, userContext.ID, feature.Key))
7278
return featureDecision, nil
7379
}
7480
}
7581

7682
decision, _ := r.experimentBucketerService.GetDecision(experimentDecisionContext, userContext)
77-
featureDecision.Decision = decision.Decision
83+
// translate the experiment reason into a more rollouts-appropriate reason
84+
switch decision.Reason {
85+
case reasons.NotBucketedIntoVariation:
86+
featureDecision.Decision = Decision{Reason: reasons.FailedRolloutBucketing}
87+
case reasons.BucketedIntoVariation:
88+
featureDecision.Decision = Decision{Reason: reasons.BucketedIntoRollout}
89+
default:
90+
featureDecision.Decision = decision.Decision
91+
}
92+
7893
featureDecision.Experiment = experiment
7994
featureDecision.Variation = decision.Variation
95+
rsLogger.Debug(fmt.Sprintf(`Decision made for user "%s" for feature rollout with key "%s": %s.`, userContext.ID, feature.Key, featureDecision.Reason))
96+
8097
return featureDecision, nil
8198
}

pkg/decision/rollout_service_test.go

Lines changed: 70 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,104 +24,121 @@ import (
2424
"github.com/optimizely/go-sdk/pkg/decision/reasons"
2525

2626
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/suite"
2728

2829
"github.com/optimizely/go-sdk/pkg/entities"
2930
)
3031

31-
func TestRolloutServiceGetDecision(t *testing.T) {
32-
testUserContext := entities.UserContext{
33-
ID: "test_user",
32+
type RolloutServiceTestSuite struct {
33+
suite.Suite
34+
mockConfig *mockProjectConfig
35+
mockAudienceTreeEvaluator *MockAudienceTreeEvaluator
36+
mockExperimentService *MockExperimentDecisionService
37+
testExperimentDecisionContext ExperimentDecisionContext
38+
testFeatureDecisionContext FeatureDecisionContext
39+
testConditionTreeParams *entities.TreeParameters
40+
testUserContext entities.UserContext
41+
}
42+
43+
func (s *RolloutServiceTestSuite) SetupTest() {
44+
s.mockConfig = new(mockProjectConfig)
45+
s.mockAudienceTreeEvaluator = new(MockAudienceTreeEvaluator)
46+
s.mockExperimentService = new(MockExperimentDecisionService)
47+
s.testExperimentDecisionContext = ExperimentDecisionContext{
48+
Experiment: &testExp1112,
49+
ProjectConfig: s.mockConfig,
3450
}
35-
mockProjectConfig := new(mockProjectConfig)
36-
testFeatureDecisionContext := FeatureDecisionContext{
51+
s.testFeatureDecisionContext = FeatureDecisionContext{
3752
Feature: &testFeatRollout3334,
38-
ProjectConfig: mockProjectConfig,
53+
ProjectConfig: s.mockConfig,
3954
}
55+
4056
testAudienceMap := map[string]entities.Audience{
4157
"5555": testAudience5555,
4258
}
43-
mockProjectConfig.On("GetAudienceMap").Return(testAudienceMap)
44-
testCondTreeParams := entities.NewTreeParameters(&testUserContext, testAudienceMap)
59+
s.testUserContext = entities.UserContext{
60+
ID: "test_user",
61+
}
62+
s.testConditionTreeParams = entities.NewTreeParameters(&s.testUserContext, testAudienceMap)
63+
s.mockConfig.On("GetAudienceMap").Return(testAudienceMap)
64+
}
4565

66+
func (s *RolloutServiceTestSuite) TestGetDecisionHappyPath() {
4667
// Test experiment passes targeting and bucketing
4768
testExperimentBucketerDecision := ExperimentDecision{
4869
Variation: &testExp1112Var2222,
70+
Decision: Decision{Reason: reasons.BucketedIntoVariation},
4971
}
50-
testExperimentBucketerDecisionContext := ExperimentDecisionContext{
51-
Experiment: &testExp1112,
52-
ProjectConfig: mockProjectConfig,
53-
}
72+
s.mockAudienceTreeEvaluator.On("Evaluate", testExp1112.AudienceConditionTree, s.testConditionTreeParams).Return(true)
73+
s.mockExperimentService.On("GetDecision", s.testExperimentDecisionContext, s.testUserContext).Return(testExperimentBucketerDecision, nil)
5474

55-
testAudienceConditionTree := testExp1112.AudienceConditionTree
56-
mockAudienceTreeEvaluator := new(MockAudienceTreeEvaluator)
57-
mockAudienceTreeEvaluator.On("Evaluate", testAudienceConditionTree, testCondTreeParams).Return(true)
58-
mockExperimentBucketerService := new(MockExperimentDecisionService)
59-
mockExperimentBucketerService.On("GetDecision", testExperimentBucketerDecisionContext, testUserContext).Return(testExperimentBucketerDecision, nil)
6075
testRolloutService := RolloutService{
61-
audienceTreeEvaluator: mockAudienceTreeEvaluator,
62-
experimentBucketerService: mockExperimentBucketerService,
76+
audienceTreeEvaluator: s.mockAudienceTreeEvaluator,
77+
experimentBucketerService: s.mockExperimentService,
6378
}
6479
expectedFeatureDecision := FeatureDecision{
6580
Experiment: testExp1112,
6681
Variation: &testExp1112Var2222,
6782
Source: Rollout,
83+
Decision: Decision{Reason: reasons.BucketedIntoRollout},
6884
}
69-
decision, _ := testRolloutService.GetDecision(testFeatureDecisionContext, testUserContext)
70-
assert.Equal(t, expectedFeatureDecision, decision)
71-
mockAudienceTreeEvaluator.AssertExpectations(t)
72-
mockExperimentBucketerService.AssertExpectations(t)
85+
decision, _ := testRolloutService.GetDecision(s.testFeatureDecisionContext, s.testUserContext)
86+
s.Equal(expectedFeatureDecision, decision)
87+
s.mockAudienceTreeEvaluator.AssertExpectations(s.T())
88+
s.mockExperimentService.AssertExpectations(s.T())
89+
}
7390

91+
func (s *RolloutServiceTestSuite) TestGetDecisionFailsBucketing() {
7492
// Test experiment passes targeting but not bucketing
75-
testExperimentBucketerDecision = ExperimentDecision{
93+
testExperimentBucketerDecision := ExperimentDecision{
7694
Decision: Decision{
7795
Reason: reasons.NotBucketedIntoVariation,
7896
},
7997
}
80-
testExperimentBucketerDecisionContext = ExperimentDecisionContext{
81-
Experiment: &testExp1112,
82-
ProjectConfig: mockProjectConfig,
83-
}
8498

85-
mockAudienceTreeEvaluator = new(MockAudienceTreeEvaluator)
86-
mockAudienceTreeEvaluator.On("Evaluate", testAudienceConditionTree, testCondTreeParams).Return(true)
87-
mockExperimentBucketerService = new(MockExperimentDecisionService)
88-
mockExperimentBucketerService.On("GetDecision", testExperimentBucketerDecisionContext, testUserContext).Return(testExperimentBucketerDecision, nil)
89-
testRolloutService = RolloutService{
90-
audienceTreeEvaluator: mockAudienceTreeEvaluator,
91-
experimentBucketerService: mockExperimentBucketerService,
99+
s.mockAudienceTreeEvaluator.On("Evaluate", testExp1112.AudienceConditionTree, s.testConditionTreeParams).Return(true)
100+
s.mockExperimentService.On("GetDecision", s.testExperimentDecisionContext, s.testUserContext).Return(testExperimentBucketerDecision, nil)
101+
testRolloutService := RolloutService{
102+
audienceTreeEvaluator: s.mockAudienceTreeEvaluator,
103+
experimentBucketerService: s.mockExperimentService,
92104
}
93-
expectedFeatureDecision = FeatureDecision{
105+
expectedFeatureDecision := FeatureDecision{
94106
Decision: Decision{
95-
Reason: reasons.NotBucketedIntoVariation,
107+
Reason: reasons.FailedRolloutBucketing,
96108
},
97109
Experiment: testExp1112,
98110
Source: Rollout,
99111
}
100-
decision, _ = testRolloutService.GetDecision(testFeatureDecisionContext, testUserContext)
101-
assert.Equal(t, expectedFeatureDecision, decision)
102-
mockAudienceTreeEvaluator.AssertExpectations(t)
103-
mockExperimentBucketerService.AssertExpectations(t)
104-
105-
// Test experiment fails targeting
106-
mockAudienceTreeEvaluator = new(MockAudienceTreeEvaluator)
107-
mockAudienceTreeEvaluator.On("Evaluate", testAudienceConditionTree, testCondTreeParams).Return(false)
108-
testRolloutService = RolloutService{
109-
audienceTreeEvaluator: mockAudienceTreeEvaluator,
110-
experimentBucketerService: mockExperimentBucketerService,
112+
decision, _ := testRolloutService.GetDecision(s.testFeatureDecisionContext, s.testUserContext)
113+
s.Equal(expectedFeatureDecision, decision)
114+
s.mockAudienceTreeEvaluator.AssertExpectations(s.T())
115+
s.mockExperimentService.AssertExpectations(s.T())
116+
}
117+
118+
func (s *RolloutServiceTestSuite) TestGetDecisionFailsTargeting() {
119+
s.mockAudienceTreeEvaluator.On("Evaluate", testExp1112.AudienceConditionTree, s.testConditionTreeParams).Return(false)
120+
testRolloutService := RolloutService{
121+
audienceTreeEvaluator: s.mockAudienceTreeEvaluator,
122+
experimentBucketerService: s.mockExperimentService,
111123
}
112-
expectedFeatureDecision = FeatureDecision{
124+
expectedFeatureDecision := FeatureDecision{
113125
Decision: Decision{
114126
Reason: reasons.FailedRolloutTargeting,
115127
},
128+
Source: Rollout,
116129
}
117-
decision, _ = testRolloutService.GetDecision(testFeatureDecisionContext, testUserContext)
118-
assert.Nil(t, decision.Variation)
119-
mockAudienceTreeEvaluator.AssertExpectations(t)
120-
mockExperimentBucketerService.AssertNotCalled(t, "GetDecision")
130+
decision, _ := testRolloutService.GetDecision(s.testFeatureDecisionContext, s.testUserContext)
131+
s.Equal(expectedFeatureDecision, decision)
132+
s.mockAudienceTreeEvaluator.AssertExpectations(s.T())
133+
s.mockExperimentService.AssertExpectations(s.T())
121134
}
122135

123136
func TestNewRolloutService(t *testing.T) {
124137
rolloutService := NewRolloutService()
125138
assert.IsType(t, &evaluator.MixedTreeEvaluator{}, rolloutService.audienceTreeEvaluator)
126139
assert.IsType(t, &ExperimentBucketerService{}, rolloutService.experimentBucketerService)
127140
}
141+
142+
func TestRolloutServiceTestSuite(t *testing.T) {
143+
suite.Run(t, new(RolloutServiceTestSuite))
144+
}

0 commit comments

Comments
 (0)