-
Notifications
You must be signed in to change notification settings - Fork 19
feat: Experiment override service #164
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
Changes from 17 commits
fa460ea
5d78a4d
ecd5bf8
10dddbd
32f2822
94a28e7
9573948
870e509
1ea197a
9a45d65
d4e8470
d540136
69856c9
dc0a3a1
c2123b2
cfbbc14
223b79b
1445e83
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 |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| /**************************************************************************** | ||
| * Copyright 2019, 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. * | ||
| * You may obtain a copy of the License at * | ||
| * * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 * | ||
| * * | ||
| * Unless required by applicable law or agreed to in writing, software * | ||
| * distributed under the License is distributed on an "AS IS" BASIS, * | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * | ||
| * See the License for the specific language governing permissions and * | ||
| * limitations under the License. * | ||
| ***************************************************************************/ | ||
|
|
||
| // Package decision // | ||
| package decision | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
|
|
||
| "github.com/optimizely/go-sdk/pkg/decision/reasons" | ||
| "github.com/optimizely/go-sdk/pkg/entities" | ||
| "github.com/optimizely/go-sdk/pkg/logging" | ||
| ) | ||
|
|
||
| var eosLogger = logging.GetLogger("ExperimentOverrideService") | ||
|
|
||
| // ExperimentOverrideKey represents the user ID and experiment associated with an override variation | ||
| type ExperimentOverrideKey struct { | ||
| ExperimentKey, UserID string | ||
| } | ||
|
|
||
| // ExperimentOverrideStore provides read access to overrides | ||
| type ExperimentOverrideStore interface { | ||
| // Returns a variation associated with overrideKey | ||
| GetVariation(overrideKey ExperimentOverrideKey) (string, bool) | ||
| } | ||
|
|
||
| // MapOverridesStore is a map-based implementation of OverrideStore | ||
| type MapOverridesStore struct { | ||
| overridesMap map[ExperimentOverrideKey]string | ||
| } | ||
|
|
||
| // GetVariation returns the override associated with the given key in the map | ||
| func (m *MapOverridesStore) GetVariation(overrideKey ExperimentOverrideKey) (string, bool) { | ||
| variationKey, ok := m.overridesMap[overrideKey] | ||
| return variationKey, ok | ||
| } | ||
|
|
||
| // ExperimentOverrideService makes a decision using an ExperimentOverridesStore | ||
| // Implements the ExperimentService interface | ||
| type ExperimentOverrideService struct { | ||
| Overrides ExperimentOverrideStore | ||
| } | ||
|
|
||
| // NewExperimentOverrideService returns a pointer to an initialized ExperimentOverrideService | ||
| func NewExperimentOverrideService(overrides ExperimentOverrideStore) *ExperimentOverrideService { | ||
| return &ExperimentOverrideService{ | ||
| Overrides: overrides, | ||
| } | ||
| } | ||
|
|
||
| // GetDecision returns a decision with a variation when the store returns a variation assignment for the given user and experiment | ||
| func (s ExperimentOverrideService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext) (ExperimentDecision, error) { | ||
| decision := ExperimentDecision{} | ||
|
|
||
| if decisionContext.Experiment == nil { | ||
| return decision, errors.New("decisionContext Experiment is nil") | ||
| } | ||
|
|
||
| variationKey, ok := s.Overrides.GetVariation(ExperimentOverrideKey{ExperimentKey: decisionContext.Experiment.Key, UserID: userContext.ID}) | ||
| if !ok { | ||
| decision.Reason = reasons.NoOverrideVariationAssignment | ||
| return decision, nil | ||
| } | ||
|
|
||
| // TODO(Matt): Add a VariationsByKey map to the Experiment struct, and use it to look up Variation by key | ||
|
||
| for _, variation := range decisionContext.Experiment.Variations { | ||
| variation := variation | ||
| if variation.Key == variationKey { | ||
| decision.Variation = &variation | ||
| decision.Reason = reasons.OverrideVariationAssignmentFound | ||
| eosLogger.Info(fmt.Sprintf("Override variation %v found for user %v", variationKey, userContext.ID)) | ||
|
||
| return decision, nil | ||
| } | ||
| } | ||
|
|
||
| decision.Reason = reasons.InvalidOverrideVariationAssignment | ||
| return decision, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| /**************************************************************************** | ||
| * Copyright 2019, 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. * | ||
| * You may obtain a copy of the License at * | ||
| * * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 * | ||
| * * | ||
| * Unless required by applicable law or agreed to in writing, software * | ||
| * distributed under the License is distributed on an "AS IS" BASIS, * | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * | ||
| * See the License for the specific language governing permissions and * | ||
| * limitations under the License. * | ||
| ***************************************************************************/ | ||
|
|
||
| // Package decision // | ||
| package decision | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/optimizely/go-sdk/pkg/decision/reasons" | ||
| "github.com/optimizely/go-sdk/pkg/entities" | ||
| "github.com/stretchr/testify/suite" | ||
| ) | ||
|
|
||
| type ExperimentOverrideServiceTestSuite struct { | ||
| suite.Suite | ||
| mockConfig *mockProjectConfig | ||
| overrides map[ExperimentOverrideKey]string | ||
| overrideService *ExperimentOverrideService | ||
| } | ||
|
|
||
| func (s *ExperimentOverrideServiceTestSuite) SetupTest() { | ||
| s.mockConfig = new(mockProjectConfig) | ||
| s.overrides = make(map[ExperimentOverrideKey]string) | ||
| s.overrideService = NewExperimentOverrideService(&MapOverridesStore{ | ||
| overridesMap: s.overrides, | ||
| }) | ||
| } | ||
|
|
||
| func (s *ExperimentOverrideServiceTestSuite) TestOverridesIncludeVariation() { | ||
| testDecisionContext := ExperimentDecisionContext{ | ||
| Experiment: &testExp1111, | ||
| ProjectConfig: s.mockConfig, | ||
| } | ||
| testUserContext := entities.UserContext{ | ||
| ID: "test_user_1", | ||
| } | ||
| s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_1"}] = testExp1111Var2222.Key | ||
| decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) | ||
| s.NoError(err) | ||
| s.NotNil(decision.Variation) | ||
| s.Exactly(testExp1111Var2222.Key, decision.Variation.Key) | ||
| s.Exactly(reasons.OverrideVariationAssignmentFound, decision.Reason) | ||
| } | ||
|
|
||
| func (s *ExperimentOverrideServiceTestSuite) TestNilDecisionContextExperiment() { | ||
| testDecisionContext := ExperimentDecisionContext{ | ||
| ProjectConfig: s.mockConfig, | ||
| } | ||
| testUserContext := entities.UserContext{ | ||
| ID: "test_user_1", | ||
| } | ||
| decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) | ||
| s.Error(err) | ||
| s.Nil(decision.Variation) | ||
| } | ||
|
|
||
| func (s *ExperimentOverrideServiceTestSuite) TestNoOverrideForExperiment() { | ||
| testDecisionContext := ExperimentDecisionContext{ | ||
| Experiment: &testExp1111, | ||
| ProjectConfig: s.mockConfig, | ||
| } | ||
| testUserContext := entities.UserContext{ | ||
| ID: "test_user_1", | ||
| } | ||
| // The decision context refers to testExp1111, but this override is for another experiment | ||
| s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1113.Key, UserID: "test_user_1"}] = testExp1113Var2224.Key | ||
| decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) | ||
| s.NoError(err) | ||
| s.Nil(decision.Variation) | ||
| s.Exactly(reasons.NoOverrideVariationAssignment, decision.Reason) | ||
| } | ||
|
|
||
| func (s *ExperimentOverrideServiceTestSuite) TestNoOverrideForUser() { | ||
| testDecisionContext := ExperimentDecisionContext{ | ||
| Experiment: &testExp1111, | ||
| ProjectConfig: s.mockConfig, | ||
| } | ||
| testUserContext := entities.UserContext{ | ||
| ID: "test_user_1", | ||
| } | ||
| // The user context refers to "test_user_1", but this override is for another user | ||
| s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_2"}] = testExp1111Var2222.Key | ||
| decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) | ||
| s.NoError(err) | ||
| s.Nil(decision.Variation) | ||
| s.Exactly(reasons.NoOverrideVariationAssignment, decision.Reason) | ||
| } | ||
|
|
||
| func (s *ExperimentOverrideServiceTestSuite) TestNoOverrideForUserOrExperiment() { | ||
| testDecisionContext := ExperimentDecisionContext{ | ||
| Experiment: &testExp1111, | ||
| ProjectConfig: s.mockConfig, | ||
| } | ||
| testUserContext := entities.UserContext{ | ||
| ID: "test_user_1", | ||
| } | ||
| // This override is for both a different user and a different experiment than the ones in the contexts above | ||
| s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1113.Key, UserID: "test_user_3"}] = testExp1111Var2222.Key | ||
| decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) | ||
| s.NoError(err) | ||
| s.Nil(decision.Variation) | ||
| s.Exactly(reasons.NoOverrideVariationAssignment, decision.Reason) | ||
| } | ||
|
|
||
| func (s *ExperimentOverrideServiceTestSuite) TestInvalidVariationInOverride() { | ||
| testDecisionContext := ExperimentDecisionContext{ | ||
| Experiment: &testExp1111, | ||
| ProjectConfig: s.mockConfig, | ||
| } | ||
| testUserContext := entities.UserContext{ | ||
| ID: "test_user_1", | ||
| } | ||
| // This override variation key does not exist in the experiment | ||
| s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_1"}] = "invalid_variation_key" | ||
| decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) | ||
| s.NoError(err) | ||
| s.Nil(decision.Variation) | ||
| s.Exactly(reasons.InvalidOverrideVariationAssignment, decision.Reason) | ||
| } | ||
|
|
||
| func TestExperimentOverridesTestSuite(t *testing.T) { | ||
| suite.Run(t, new(ExperimentOverrideServiceTestSuite)) | ||
| } |
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.
Probably a keyToID map and then you can just look up by ID