Skip to content

Commit 86062a1

Browse files
authored
feat: Experiment override service (#164)
Summary: Add ExperimentOverrideService, a decision service that can pull variations from an ExperimentOverrideStore, which is an interface that returns variations for experiment key/user ID pairs. Included is an implementation of ExperimentOverrideStore based on a map. Test plan: New unit tests JIRA Issues: https://optimizely.atlassian.net/browse/OASIS-5419
1 parent 586b82d commit 86062a1

File tree

3 files changed

+236
-0
lines changed

3 files changed

+236
-0
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
// Package decision //
18+
package decision
19+
20+
import (
21+
"errors"
22+
"fmt"
23+
24+
"github.com/optimizely/go-sdk/pkg/decision/reasons"
25+
"github.com/optimizely/go-sdk/pkg/entities"
26+
"github.com/optimizely/go-sdk/pkg/logging"
27+
)
28+
29+
var eosLogger = logging.GetLogger("ExperimentOverrideService")
30+
31+
// ExperimentOverrideKey represents the user ID and experiment associated with an override variation
32+
type ExperimentOverrideKey struct {
33+
ExperimentKey, UserID string
34+
}
35+
36+
// ExperimentOverrideStore provides read access to overrides
37+
type ExperimentOverrideStore interface {
38+
// Returns a variation associated with overrideKey
39+
GetVariation(overrideKey ExperimentOverrideKey) (string, bool)
40+
}
41+
42+
// MapOverridesStore is a map-based implementation of OverrideStore
43+
type MapOverridesStore struct {
44+
overridesMap map[ExperimentOverrideKey]string
45+
}
46+
47+
// GetVariation returns the override associated with the given key in the map
48+
func (m *MapOverridesStore) GetVariation(overrideKey ExperimentOverrideKey) (string, bool) {
49+
variationKey, ok := m.overridesMap[overrideKey]
50+
return variationKey, ok
51+
}
52+
53+
// ExperimentOverrideService makes a decision using an ExperimentOverridesStore
54+
// Implements the ExperimentService interface
55+
type ExperimentOverrideService struct {
56+
Overrides ExperimentOverrideStore
57+
}
58+
59+
// NewExperimentOverrideService returns a pointer to an initialized ExperimentOverrideService
60+
func NewExperimentOverrideService(overrides ExperimentOverrideStore) *ExperimentOverrideService {
61+
return &ExperimentOverrideService{
62+
Overrides: overrides,
63+
}
64+
}
65+
66+
// GetDecision returns a decision with a variation when the store returns a variation assignment for the given user and experiment
67+
func (s ExperimentOverrideService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext) (ExperimentDecision, error) {
68+
decision := ExperimentDecision{}
69+
70+
if decisionContext.Experiment == nil {
71+
return decision, errors.New("decisionContext Experiment is nil")
72+
}
73+
74+
variationKey, ok := s.Overrides.GetVariation(ExperimentOverrideKey{ExperimentKey: decisionContext.Experiment.Key, UserID: userContext.ID})
75+
if !ok {
76+
decision.Reason = reasons.NoOverrideVariationAssignment
77+
return decision, nil
78+
}
79+
80+
// TODO(Matt): Implement and use a way to access variations by key
81+
for _, variation := range decisionContext.Experiment.Variations {
82+
variation := variation
83+
if variation.Key == variationKey {
84+
decision.Variation = &variation
85+
decision.Reason = reasons.OverrideVariationAssignmentFound
86+
eosLogger.Debug(fmt.Sprintf("Override variation %v found for user %v", variationKey, userContext.ID))
87+
return decision, nil
88+
}
89+
}
90+
91+
decision.Reason = reasons.InvalidOverrideVariationAssignment
92+
return decision, nil
93+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
// Package decision //
18+
package decision
19+
20+
import (
21+
"testing"
22+
23+
"github.com/optimizely/go-sdk/pkg/decision/reasons"
24+
"github.com/optimizely/go-sdk/pkg/entities"
25+
"github.com/stretchr/testify/suite"
26+
)
27+
28+
type ExperimentOverrideServiceTestSuite struct {
29+
suite.Suite
30+
mockConfig *mockProjectConfig
31+
overrides map[ExperimentOverrideKey]string
32+
overrideService *ExperimentOverrideService
33+
}
34+
35+
func (s *ExperimentOverrideServiceTestSuite) SetupTest() {
36+
s.mockConfig = new(mockProjectConfig)
37+
s.overrides = make(map[ExperimentOverrideKey]string)
38+
s.overrideService = NewExperimentOverrideService(&MapOverridesStore{
39+
overridesMap: s.overrides,
40+
})
41+
}
42+
43+
func (s *ExperimentOverrideServiceTestSuite) TestOverridesIncludeVariation() {
44+
testDecisionContext := ExperimentDecisionContext{
45+
Experiment: &testExp1111,
46+
ProjectConfig: s.mockConfig,
47+
}
48+
testUserContext := entities.UserContext{
49+
ID: "test_user_1",
50+
}
51+
s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_1"}] = testExp1111Var2222.Key
52+
decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext)
53+
s.NoError(err)
54+
s.NotNil(decision.Variation)
55+
s.Exactly(testExp1111Var2222.Key, decision.Variation.Key)
56+
s.Exactly(reasons.OverrideVariationAssignmentFound, decision.Reason)
57+
}
58+
59+
func (s *ExperimentOverrideServiceTestSuite) TestNilDecisionContextExperiment() {
60+
testDecisionContext := ExperimentDecisionContext{
61+
ProjectConfig: s.mockConfig,
62+
}
63+
testUserContext := entities.UserContext{
64+
ID: "test_user_1",
65+
}
66+
decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext)
67+
s.Error(err)
68+
s.Nil(decision.Variation)
69+
}
70+
71+
func (s *ExperimentOverrideServiceTestSuite) TestNoOverrideForExperiment() {
72+
testDecisionContext := ExperimentDecisionContext{
73+
Experiment: &testExp1111,
74+
ProjectConfig: s.mockConfig,
75+
}
76+
testUserContext := entities.UserContext{
77+
ID: "test_user_1",
78+
}
79+
// The decision context refers to testExp1111, but this override is for another experiment
80+
s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1113.Key, UserID: "test_user_1"}] = testExp1113Var2224.Key
81+
decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext)
82+
s.NoError(err)
83+
s.Nil(decision.Variation)
84+
s.Exactly(reasons.NoOverrideVariationAssignment, decision.Reason)
85+
}
86+
87+
func (s *ExperimentOverrideServiceTestSuite) TestNoOverrideForUser() {
88+
testDecisionContext := ExperimentDecisionContext{
89+
Experiment: &testExp1111,
90+
ProjectConfig: s.mockConfig,
91+
}
92+
testUserContext := entities.UserContext{
93+
ID: "test_user_1",
94+
}
95+
// The user context refers to "test_user_1", but this override is for another user
96+
s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_2"}] = testExp1111Var2222.Key
97+
decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext)
98+
s.NoError(err)
99+
s.Nil(decision.Variation)
100+
s.Exactly(reasons.NoOverrideVariationAssignment, decision.Reason)
101+
}
102+
103+
func (s *ExperimentOverrideServiceTestSuite) TestNoOverrideForUserOrExperiment() {
104+
testDecisionContext := ExperimentDecisionContext{
105+
Experiment: &testExp1111,
106+
ProjectConfig: s.mockConfig,
107+
}
108+
testUserContext := entities.UserContext{
109+
ID: "test_user_1",
110+
}
111+
// This override is for both a different user and a different experiment than the ones in the contexts above
112+
s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1113.Key, UserID: "test_user_3"}] = testExp1111Var2222.Key
113+
decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext)
114+
s.NoError(err)
115+
s.Nil(decision.Variation)
116+
s.Exactly(reasons.NoOverrideVariationAssignment, decision.Reason)
117+
}
118+
119+
func (s *ExperimentOverrideServiceTestSuite) TestInvalidVariationInOverride() {
120+
testDecisionContext := ExperimentDecisionContext{
121+
Experiment: &testExp1111,
122+
ProjectConfig: s.mockConfig,
123+
}
124+
testUserContext := entities.UserContext{
125+
ID: "test_user_1",
126+
}
127+
// This override variation key does not exist in the experiment
128+
s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_1"}] = "invalid_variation_key"
129+
decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext)
130+
s.NoError(err)
131+
s.Nil(decision.Variation)
132+
s.Exactly(reasons.InvalidOverrideVariationAssignment, decision.Reason)
133+
}
134+
135+
func TestExperimentOverridesTestSuite(t *testing.T) {
136+
suite.Run(t, new(ExperimentOverrideServiceTestSuite))
137+
}

pkg/decision/reasons/reason.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,10 @@ const (
4343
InvalidWhitelistVariationAssignment Reason = "Invalid whitelist variation assignment"
4444
// WhitelistVariationAssignmentFound - a valid variation assignment was found for the given user and experiment
4545
WhitelistVariationAssignmentFound Reason = "Whitelist variation assignment found"
46+
// NoOverrideVariationAssignment - No override variation was found for the given user and experiment
47+
NoOverrideVariationAssignment Reason = "No override variation assignment"
48+
// InvalidOverrideVariationAssignment - An override variation was found for the given user and experiment, but no variation with that key exists in the given experiment
49+
InvalidOverrideVariationAssignment Reason = "Invalid override variation assignment"
50+
// OverrideVariationAssignmentFound - A valid override variation was found for the given user and experiment
51+
OverrideVariationAssignmentFound Reason = "Override variation assignment found"
4652
)

0 commit comments

Comments
 (0)