diff --git a/pkg/client/client.go b/pkg/client/client.go index 188d9accb..a25a65c9e 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -657,7 +657,8 @@ func (o *OptimizelyClient) getFeatureDecision(featureKey, variableKey string, us } decisionContext.Variable = variable - featureDecision, err = o.DecisionService.GetFeatureDecision(decisionContext, userContext) + options := decide.OptimizelyDecideOptions{} + featureDecision, err = o.DecisionService.GetFeatureDecision(decisionContext, userContext, options, decide.NewDecisionReasons(options)) if err != nil { o.logger.Warning(fmt.Sprintf(`Received error while making a decision for feature "%s": %s`, featureKey, err)) return decisionContext, featureDecision, nil @@ -687,7 +688,8 @@ func (o *OptimizelyClient) getExperimentDecision(experimentKey string, userConte ProjectConfig: projectConfig, } - experimentDecision, err = o.DecisionService.GetExperimentDecision(decisionContext, userContext) + options := decide.OptimizelyDecideOptions{} + experimentDecision, err = o.DecisionService.GetExperimentDecision(decisionContext, userContext, options, decide.NewDecisionReasons(options)) if err != nil { o.logger.Warning(fmt.Sprintf(`Received error while making a decision for experiment "%s": %s`, experimentKey, err)) return decisionContext, experimentDecision, nil diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index fd402045f..cc0c019f6 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -25,6 +25,7 @@ import ( "testing" "github.com/optimizely/go-sdk/pkg/config" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/event" @@ -301,7 +302,7 @@ func TestGetFeatureVariableBool(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) client := OptimizelyClient{ ConfigManager: mockConfigManager, @@ -379,7 +380,7 @@ func TestGetFeatureVariableBoolWithNotification(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) notificationCenter := notification.NewNotificationCenter() client := OptimizelyClient{ @@ -486,7 +487,7 @@ func TestGetFeatureVariableDouble(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) client := OptimizelyClient{ ConfigManager: mockConfigManager, @@ -564,7 +565,7 @@ func TestGetFeatureVariableDoubleWithNotification(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) notificationCenter := notification.NewNotificationCenter() client := OptimizelyClient{ @@ -671,7 +672,7 @@ func TestGetFeatureVariableInteger(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) client := OptimizelyClient{ ConfigManager: mockConfigManager, @@ -749,7 +750,7 @@ func TestGetFeatureVariableIntegerWithNotification(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) notificationCenter := notification.NewNotificationCenter() client := OptimizelyClient{ @@ -854,7 +855,7 @@ func TestGetFeatureVariableSting(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) client := OptimizelyClient{ ConfigManager: mockConfigManager, @@ -930,7 +931,7 @@ func TestGetFeatureVariableStringWithNotification(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) notificationCenter := notification.NewNotificationCenter() client := OptimizelyClient{ @@ -1037,7 +1038,7 @@ func TestGetFeatureVariableJSON(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) client := OptimizelyClient{ ConfigManager: mockConfigManager, @@ -1121,7 +1122,7 @@ func TestGetFeatureVariableJSONWithNotification(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) notificationCenter := notification.NewNotificationCenter() client := OptimizelyClient{ @@ -1270,7 +1271,7 @@ func TestGetFeatureDecisionValid(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) client := OptimizelyClient{ ConfigManager: mockConfigManager, @@ -1316,7 +1317,7 @@ func TestGetFeatureDecisionErrProjectConfig(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) client := OptimizelyClient{ ConfigManager: mockConfigManager, @@ -1360,7 +1361,7 @@ func TestGetFeatureDecisionPanicProjectConfig(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) client := OptimizelyClient{ ConfigManager: &PanickingConfigManager{}, @@ -1441,7 +1442,7 @@ func TestGetFeatureDecisionErrFeatureDecision(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, errors.New("error feature")) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, errors.New("error feature")) client := OptimizelyClient{ ConfigManager: mockConfigManager, @@ -1496,7 +1497,7 @@ func TestGetAllFeatureVariablesWithDecision(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) client := OptimizelyClient{ ConfigManager: mockConfigManager, @@ -1553,7 +1554,7 @@ func TestGetAllFeatureVariablesWithDecisionWithNotification(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) notificationCenter := notification.NewNotificationCenter() client := OptimizelyClient{ @@ -1615,7 +1616,7 @@ func TestGetAllFeatureVariablesWithDecisionWithError(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, errors.New("")) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, errors.New("")) client := OptimizelyClient{ ConfigManager: mockConfigManager, @@ -1695,7 +1696,7 @@ func TestGetDetailedFeatureDecisionUnsafeWithNotification(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) notificationCenter := notification.NewNotificationCenter() client := OptimizelyClient{ @@ -1766,7 +1767,7 @@ func TestGetDetailedFeatureDecisionUnsafeWithTrackingDisabled(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) client := OptimizelyClient{ ConfigManager: mockConfigManager, @@ -1826,7 +1827,7 @@ func TestGetDetailedFeatureDecisionUnsafeWithError(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, errors.New("")) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, errors.New("")) client := OptimizelyClient{ ConfigManager: mockConfigManager, @@ -1866,7 +1867,7 @@ func TestGetDetailedFeatureDecisionUnsafeWithFeatureTestAndTrackingEnabled(t *te Source: decision.FeatureTest, } - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) client := OptimizelyClient{ ConfigManager: mockConfigManager, @@ -1927,7 +1928,7 @@ func TestGetAllFeatureVariables(t *testing.T) { expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation) mockDecisionService := new(MockDecisionService) - mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) client := OptimizelyClient{ ConfigManager: mockConfigManager, @@ -1935,10 +1936,10 @@ func TestGetAllFeatureVariables(t *testing.T) { logger: logging.GetLogger("", ""), } - optlyJson, err := client.GetAllFeatureVariables(testFeatureKey, testUserContext) + optlyJSON, err := client.GetAllFeatureVariables(testFeatureKey, testUserContext) assert.NoError(t, err) - assert.NotNil(t, optlyJson) - variationMap := optlyJson.ToMap() + assert.NotNil(t, optlyJSON) + variationMap := optlyJSON.ToMap() assert.NoError(t, err) for _, v := range variables { @@ -2043,7 +2044,7 @@ func (s *ClientTestSuiteAB) TestActivate() { expectedExperimentDecision := decision.ExperimentDecision{ Variation: &expectedVariation, } - s.mockDecisionService.On("GetExperimentDecision", testDecisionContext, testUserContext).Return(expectedExperimentDecision, nil) + s.mockDecisionService.On("GetExperimentDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedExperimentDecision, nil) s.mockEventProcessor.On("ProcessEvent", mock.AnythingOfType("event.UserEvent")) testClient := OptimizelyClient{ @@ -2112,7 +2113,7 @@ func (s *ClientTestSuiteAB) TestGetVariation() { expectedExperimentDecision := decision.ExperimentDecision{ Variation: &expectedVariation, } - s.mockDecisionService.On("GetExperimentDecision", testDecisionContext, testUserContext).Return(expectedExperimentDecision, nil) + s.mockDecisionService.On("GetExperimentDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedExperimentDecision, nil) testClient := OptimizelyClient{ ConfigManager: s.mockConfigManager, @@ -2142,7 +2143,7 @@ func (s *ClientTestSuiteAB) TestGetVariationWithDecisionError() { expectedExperimentDecision := decision.ExperimentDecision{ Variation: &expectedVariation, } - s.mockDecisionService.On("GetExperimentDecision", testDecisionContext, testUserContext).Return(expectedExperimentDecision, errors.New("")) + s.mockDecisionService.On("GetExperimentDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedExperimentDecision, errors.New("")) testClient := OptimizelyClient{ ConfigManager: s.mockConfigManager, @@ -2210,7 +2211,7 @@ func (s *ClientTestSuiteFM) TestIsFeatureEnabled() { Source: decision.FeatureTest, } - s.mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + s.mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) client := OptimizelyClient{ ConfigManager: s.mockConfigManager, @@ -2247,7 +2248,7 @@ func (s *ClientTestSuiteFM) TestIsFeatureEnabledWithNotification() { Source: decision.FeatureTest, } - s.mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + s.mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, nil) notificationCenter := notification.NewNotificationCenter() client := OptimizelyClient{ @@ -2301,7 +2302,7 @@ func (s *ClientTestSuiteFM) TestIsFeatureEnabledWithDecisionError() { Source: decision.FeatureTest, } - s.mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, errors.New("")) + s.mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecision, errors.New("")) s.mockEventProcessor.On("ProcessEvent", mock.AnythingOfType("event.UserEvent")) client := OptimizelyClient{ @@ -2410,8 +2411,8 @@ func (s *ClientTestSuiteFM) TestGetEnabledFeatures() { Variation: &testVariationDisabled, } - s.mockDecisionService.On("GetFeatureDecision", testDecisionContextEnabled, testUserContext).Return(expectedFeatureDecisionEnabled, nil) - s.mockDecisionService.On("GetFeatureDecision", testDecisionContextDisabled, testUserContext).Return(expectedFeatureDecisionDisabled, nil) + s.mockDecisionService.On("GetFeatureDecision", testDecisionContextEnabled, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecisionEnabled, nil) + s.mockDecisionService.On("GetFeatureDecision", testDecisionContextDisabled, testUserContext, decide.OptimizelyDecideOptions{}, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})).Return(expectedFeatureDecisionDisabled, nil) client := OptimizelyClient{ ConfigManager: s.mockConfigManager, diff --git a/pkg/client/fixtures_test.go b/pkg/client/fixtures_test.go index c3f7ae84d..1c24d63f1 100644 --- a/pkg/client/fixtures_test.go +++ b/pkg/client/fixtures_test.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -21,6 +21,7 @@ import ( "fmt" "github.com/optimizely/go-sdk/pkg/config" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/event" @@ -116,13 +117,13 @@ type MockDecisionService struct { mock.Mock } -func (m *MockDecisionService) GetFeatureDecision(decisionContext decision.FeatureDecisionContext, userContext entities.UserContext) (decision.FeatureDecision, error) { - args := m.Called(decisionContext, userContext) +func (m *MockDecisionService) GetFeatureDecision(decisionContext decision.FeatureDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (decision.FeatureDecision, error) { + args := m.Called(decisionContext, userContext, options, reasons) return args.Get(0).(decision.FeatureDecision), args.Error(1) } -func (m *MockDecisionService) GetExperimentDecision(decisionContext decision.ExperimentDecisionContext, userContext entities.UserContext) (decision.ExperimentDecision, error) { - args := m.Called(decisionContext, userContext) +func (m *MockDecisionService) GetExperimentDecision(decisionContext decision.ExperimentDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (decision.ExperimentDecision, error) { + args := m.Called(decisionContext, userContext, options, reasons) return args.Get(0).(decision.ExperimentDecision), args.Error(1) } @@ -159,11 +160,11 @@ func (m *PanickingConfigManager) GetConfig() (config.ProjectConfig, error) { type PanickingDecisionService struct { } -func (m *PanickingDecisionService) GetFeatureDecision(decisionContext decision.FeatureDecisionContext, userContext entities.UserContext) (decision.FeatureDecision, error) { +func (m *PanickingDecisionService) GetFeatureDecision(decisionContext decision.FeatureDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (decision.FeatureDecision, error) { panic("I'm panicking") } -func (m *PanickingDecisionService) GetExperimentDecision(decisionContext decision.ExperimentDecisionContext, userContext entities.UserContext) (decision.ExperimentDecision, error) { +func (m *PanickingDecisionService) GetExperimentDecision(decisionContext decision.ExperimentDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (decision.ExperimentDecision, error) { panic("I'm panicking") } diff --git a/pkg/decide/decide_errors.go b/pkg/decide/decide_errors.go index bb2a0c85e..b1fd2ceb9 100644 --- a/pkg/decide/decide_errors.go +++ b/pkg/decide/decide_errors.go @@ -24,6 +24,10 @@ type decideError string const ( // SDKNotReady when sdk is not ready SDKNotReady decideError = "Optimizely SDK not configured properly yet" + // FlagKeyInvalid when invalid flag key is provided + FlagKeyInvalid decideError = `No flag was found for key "%s".` + // VariableValueInvalid when invalid variable value is provided + VariableValueInvalid decideError = `Variable value for key "%s" is invalid or wrong type.` ) // GetError returns error for decide error type diff --git a/pkg/decide/decision_reasons.go b/pkg/decide/decision_reasons.go new file mode 100644 index 000000000..9c74c459b --- /dev/null +++ b/pkg/decide/decision_reasons.go @@ -0,0 +1,25 @@ +/**************************************************************************** + * Copyright 2020, 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 decide // +package decide + +// DecisionReasons defines the reasons for which the decision was made. +type DecisionReasons interface { + AddError(format string, arguments ...interface{}) + AddInfo(format string, arguments ...interface{}) string + ToReport() []string +} diff --git a/pkg/decide/decision_reasons_test.go b/pkg/decide/decision_reasons_test.go new file mode 100644 index 000000000..7f740a073 --- /dev/null +++ b/pkg/decide/decision_reasons_test.go @@ -0,0 +1,77 @@ +/**************************************************************************** + * Copyright 2020, 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 decide + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewDecisionReasonsWithEmptyOptions(t *testing.T) { + reasons := NewDecisionReasons(OptimizelyDecideOptions{}) + assert.Equal(t, 0, len(reasons.ToReport())) +} + +func TestAddErrorWorksWithEveryOption(t *testing.T) { + options := OptimizelyDecideOptions{ + DisableDecisionEvent: true, + EnabledFlagsOnly: true, + IgnoreUserProfileService: true, + ExcludeVariables: true, + IncludeReasons: true, + } + reasons := NewDecisionReasons(options) + reasons.AddError("error message") + + reportedReasons := reasons.ToReport() + assert.Equal(t, 1, len(reportedReasons)) + assert.Equal(t, "error message", reportedReasons[0]) +} + +func TestInfoLogsAreOnlyReportedWhenIncludeReasonsOptionIsSet(t *testing.T) { + options := OptimizelyDecideOptions{ + DisableDecisionEvent: true, + EnabledFlagsOnly: true, + IgnoreUserProfileService: true, + ExcludeVariables: true, + } + reasons := NewDecisionReasons(options) + reasons.AddError("error message") + reasons.AddError("error message: code %d", 121) + reasons.AddInfo("info message") + reasons.AddInfo("info message: %s", "unexpected string") + + reportedReasons := reasons.ToReport() + assert.Equal(t, 2, len(reportedReasons)) + assert.Equal(t, "error message", reportedReasons[0]) + assert.Equal(t, "error message: code 121", reportedReasons[1]) + + options.IncludeReasons = true + reasons = NewDecisionReasons(options) + reasons.AddError("error message") + reasons.AddError("error message: code %d", 121) + reasons.AddInfo("info message") + reasons.AddInfo("info message: %s", "unexpected string") + + reportedReasons = reasons.ToReport() + assert.Equal(t, 4, len(reportedReasons)) + assert.Equal(t, "error message", reportedReasons[0]) + assert.Equal(t, "error message: code 121", reportedReasons[1]) + assert.Equal(t, "info message", reportedReasons[2]) + assert.Equal(t, "info message: unexpected string", reportedReasons[3]) +} diff --git a/pkg/decide/default_decision_reasons.go b/pkg/decide/default_decision_reasons.go new file mode 100644 index 000000000..aba8d6d50 --- /dev/null +++ b/pkg/decide/default_decision_reasons.go @@ -0,0 +1,61 @@ +/**************************************************************************** + * Copyright 2020, 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 decide // +package decide + +import ( + "fmt" +) + +// DefaultDecisionReasons provides the default implementation of DecisionReasons. +type DefaultDecisionReasons struct { + errors, logs []string + includeReasons bool +} + +// NewDecisionReasons returns a new instance of DecisionReasons. +func NewDecisionReasons(options OptimizelyDecideOptions) *DefaultDecisionReasons { + return &DefaultDecisionReasons{ + errors: []string{}, + logs: []string{}, + includeReasons: options.IncludeReasons, + } +} + +// AddError appends given message to the error list. +func (o *DefaultDecisionReasons) AddError(format string, arguments ...interface{}) { + o.errors = append(o.errors, fmt.Sprintf(format, arguments...)) +} + +// AddInfo appends given info message to the info list after formatting. +func (o *DefaultDecisionReasons) AddInfo(format string, arguments ...interface{}) string { + message := fmt.Sprintf(format, arguments...) + if !o.includeReasons { + return message + } + o.logs = append(o.logs, message) + return message +} + +// ToReport returns reasons to be reported. +func (o *DefaultDecisionReasons) ToReport() []string { + reasons := o.errors + if !o.includeReasons { + return reasons + } + return append(reasons, o.logs...) +} diff --git a/pkg/decision/composite_experiment_service.go b/pkg/decision/composite_experiment_service.go index c8fd3de98..7a0c29ddd 100644 --- a/pkg/decision/composite_experiment_service.go +++ b/pkg/decision/composite_experiment_service.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -19,6 +19,8 @@ package decision import ( "fmt" + + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) @@ -54,7 +56,7 @@ func NewCompositeExperimentService(sdkKey string, options ...CESOptionFunc) *Com // 1. Overrides (if supplied) // 2. Whitelist // 3. Bucketing (with User profile integration if supplied) - compositeExperimentService := &CompositeExperimentService{logger:logging.GetLogger(sdkKey, "CompositeExperimentService")} + compositeExperimentService := &CompositeExperimentService{logger: logging.GetLogger(sdkKey, "CompositeExperimentService")} for _, opt := range options { opt(compositeExperimentService) } @@ -81,11 +83,10 @@ func NewCompositeExperimentService(sdkKey string, options ...CESOptionFunc) *Com } // GetDecision returns a decision for the given experiment and user context -func (s CompositeExperimentService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext) (decision ExperimentDecision, err error) { - +func (s CompositeExperimentService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (decision ExperimentDecision, err error) { // Run through the various decision services until we get a decision for _, experimentService := range s.experimentServices { - decision, err = experimentService.GetDecision(decisionContext, userContext) + decision, err = experimentService.GetDecision(decisionContext, userContext, options, reasons) if err != nil { s.logger.Debug(fmt.Sprintf("%v", err)) } diff --git a/pkg/decision/composite_experiment_service_test.go b/pkg/decision/composite_experiment_service_test.go index 86f497c6b..b6638a341 100644 --- a/pkg/decision/composite_experiment_service_test.go +++ b/pkg/decision/composite_experiment_service_test.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/suite" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) @@ -32,12 +33,16 @@ type CompositeExperimentTestSuite struct { mockExperimentService *MockExperimentDecisionService mockExperimentService2 *MockExperimentDecisionService testDecisionContext ExperimentDecisionContext + options decide.OptimizelyDecideOptions + reasons decide.DecisionReasons } func (s *CompositeExperimentTestSuite) SetupTest() { s.mockConfig = new(mockProjectConfig) s.mockExperimentService = new(MockExperimentDecisionService) s.mockExperimentService2 = new(MockExperimentDecisionService) + s.options = decide.OptimizelyDecideOptions{} + s.reasons = decide.NewDecisionReasons(s.options) // Setup test data s.testDecisionContext = ExperimentDecisionContext{ @@ -56,13 +61,13 @@ func (s *CompositeExperimentTestSuite) TestGetDecision() { expectedExperimentDecision := ExperimentDecision{ Variation: &expectedVariation, } - s.mockExperimentService.On("GetDecision", s.testDecisionContext, testUserContext).Return(expectedExperimentDecision, nil) + s.mockExperimentService.On("GetDecision", s.testDecisionContext, testUserContext, s.options, s.reasons).Return(expectedExperimentDecision, nil) compositeExperimentService := &CompositeExperimentService{ - experimentServices: []ExperimentService{s.mockExperimentService, s.mockExperimentService2,}, - logger:logging.GetLogger("sdkKey", "ExperimentService"), + experimentServices: []ExperimentService{s.mockExperimentService, s.mockExperimentService2}, + logger: logging.GetLogger("sdkKey", "ExperimentService"), } - decision, err := compositeExperimentService.GetDecision(s.testDecisionContext, testUserContext) + decision, err := compositeExperimentService.GetDecision(s.testDecisionContext, testUserContext, s.options, s.reasons) s.Equal(expectedExperimentDecision, decision) s.NoError(err) s.mockExperimentService.AssertExpectations(s.T()) @@ -78,18 +83,18 @@ func (s *CompositeExperimentTestSuite) TestGetDecisionFallthrough() { expectedVariation := testExp1111.Variations["2222"] expectedExperimentDecision := ExperimentDecision{} - s.mockExperimentService.On("GetDecision", s.testDecisionContext, testUserContext).Return(expectedExperimentDecision, nil) + s.mockExperimentService.On("GetDecision", s.testDecisionContext, testUserContext, s.options, s.reasons).Return(expectedExperimentDecision, nil) expectedExperimentDecision2 := ExperimentDecision{ Variation: &expectedVariation, } - s.mockExperimentService2.On("GetDecision", s.testDecisionContext, testUserContext).Return(expectedExperimentDecision2, nil) + s.mockExperimentService2.On("GetDecision", s.testDecisionContext, testUserContext, s.options, s.reasons).Return(expectedExperimentDecision2, nil) compositeExperimentService := &CompositeExperimentService{ experimentServices: []ExperimentService{s.mockExperimentService, s.mockExperimentService2}, - logger:logging.GetLogger("sdkKey", "CompositeExperimentService"), + logger: logging.GetLogger("sdkKey", "CompositeExperimentService"), } - decision, err := compositeExperimentService.GetDecision(s.testDecisionContext, testUserContext) + decision, err := compositeExperimentService.GetDecision(s.testDecisionContext, testUserContext, s.options, s.reasons) s.NoError(err) s.Equal(expectedExperimentDecision2, decision) @@ -103,16 +108,16 @@ func (s *CompositeExperimentTestSuite) TestGetDecisionNoDecisionsMade() { ID: "test_user_1", } expectedExperimentDecision := ExperimentDecision{} - s.mockExperimentService.On("GetDecision", s.testDecisionContext, testUserContext).Return(expectedExperimentDecision, nil) + s.mockExperimentService.On("GetDecision", s.testDecisionContext, testUserContext, s.options, s.reasons).Return(expectedExperimentDecision, nil) expectedExperimentDecision2 := ExperimentDecision{} - s.mockExperimentService2.On("GetDecision", s.testDecisionContext, testUserContext).Return(expectedExperimentDecision2, nil) + s.mockExperimentService2.On("GetDecision", s.testDecisionContext, testUserContext, s.options, s.reasons).Return(expectedExperimentDecision2, nil) compositeExperimentService := &CompositeExperimentService{ experimentServices: []ExperimentService{s.mockExperimentService, s.mockExperimentService2}, - logger:logging.GetLogger("sdkKey", "CompositeExperimentService"), + logger: logging.GetLogger("sdkKey", "CompositeExperimentService"), } - decision, err := compositeExperimentService.GetDecision(s.testDecisionContext, testUserContext) + decision, err := compositeExperimentService.GetDecision(s.testDecisionContext, testUserContext, s.options, s.reasons) s.NoError(err) s.Equal(expectedExperimentDecision2, decision) @@ -134,21 +139,21 @@ func (s *CompositeExperimentTestSuite) TestGetDecisionReturnsError() { shouldBeIgnoredDecision := ExperimentDecision{ Variation: &testExp1114Var2225, } - s.mockExperimentService.On("GetDecision", testDecisionContext, testUserContext).Return(shouldBeIgnoredDecision, errors.New("Error making decision")) + s.mockExperimentService.On("GetDecision", testDecisionContext, testUserContext, s.options, s.reasons).Return(shouldBeIgnoredDecision, errors.New("Error making decision")) expectedDecision := ExperimentDecision{ Variation: &testExp1114Var2226, } - s.mockExperimentService2.On("GetDecision", testDecisionContext, testUserContext).Return(expectedDecision, nil) + s.mockExperimentService2.On("GetDecision", testDecisionContext, testUserContext, s.options, s.reasons).Return(expectedDecision, nil) compositeExperimentService := &CompositeExperimentService{ experimentServices: []ExperimentService{ s.mockExperimentService, s.mockExperimentService2, }, - logger:logging.GetLogger("sdkKey", "CompositeExperimentService"), + logger: logging.GetLogger("sdkKey", "CompositeExperimentService"), } - decision, err := compositeExperimentService.GetDecision(testDecisionContext, testUserContext) + decision, err := compositeExperimentService.GetDecision(testDecisionContext, testUserContext, s.options, s.reasons) s.Equal(expectedDecision, decision) s.NoError(err) s.mockExperimentService.AssertExpectations(s.T()) diff --git a/pkg/decision/composite_feature_service.go b/pkg/decision/composite_feature_service.go index 671131147..d7e2018d1 100644 --- a/pkg/decision/composite_feature_service.go +++ b/pkg/decision/composite_feature_service.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -20,6 +20,7 @@ package decision import ( "fmt" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) @@ -27,13 +28,13 @@ import ( // CompositeFeatureService is the default out-of-the-box feature decision service type CompositeFeatureService struct { featureServices []FeatureService - logger logging.OptimizelyLogProducer + logger logging.OptimizelyLogProducer } // NewCompositeFeatureService returns a new instance of the CompositeFeatureService func NewCompositeFeatureService(sdkKey string, compositeExperimentService ExperimentService) *CompositeFeatureService { return &CompositeFeatureService{ - logger:logging.GetLogger(sdkKey, "CompositeFeatureService"), + logger: logging.GetLogger(sdkKey, "CompositeFeatureService"), featureServices: []FeatureService{ NewFeatureExperimentService(logging.GetLogger(sdkKey, "FeatureExperimentService"), compositeExperimentService), NewRolloutService(sdkKey), @@ -42,11 +43,11 @@ func NewCompositeFeatureService(sdkKey string, compositeExperimentService Experi } // GetDecision returns a decision for the given feature and user context -func (f CompositeFeatureService) GetDecision(decisionContext FeatureDecisionContext, userContext entities.UserContext) (FeatureDecision, error) { +func (f CompositeFeatureService) GetDecision(decisionContext FeatureDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (FeatureDecision, error) { var featureDecision = FeatureDecision{} var err error for _, featureDecisionService := range f.featureServices { - featureDecision, err = featureDecisionService.GetDecision(decisionContext, userContext) + featureDecision, err = featureDecisionService.GetDecision(decisionContext, userContext, options, reasons) if err != nil { f.logger.Debug(fmt.Sprintf("%v", err)) } diff --git a/pkg/decision/composite_feature_service_test.go b/pkg/decision/composite_feature_service_test.go index 08d915771..2c345f19c 100644 --- a/pkg/decision/composite_feature_service_test.go +++ b/pkg/decision/composite_feature_service_test.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -20,6 +20,7 @@ import ( "errors" "testing" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision/reasons" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" @@ -32,6 +33,8 @@ type CompositeFeatureServiceTestSuite struct { mockFeatureService *MockFeatureDecisionService mockFeatureService2 *MockFeatureDecisionService testFeatureDecisionContext FeatureDecisionContext + options decide.OptimizelyDecideOptions + reasons decide.DecisionReasons } func (s *CompositeFeatureServiceTestSuite) SetupTest() { @@ -39,6 +42,8 @@ func (s *CompositeFeatureServiceTestSuite) SetupTest() { s.mockFeatureService = new(MockFeatureDecisionService) s.mockFeatureService2 = new(MockFeatureDecisionService) + s.options = decide.OptimizelyDecideOptions{} + s.reasons = decide.NewDecisionReasons(s.options) // Setup test data s.testFeatureDecisionContext = FeatureDecisionContext{ @@ -59,16 +64,16 @@ func (s *CompositeFeatureServiceTestSuite) TestGetDecision() { Experiment: testExp1113, Variation: &testExp1113Var2223, } - s.mockFeatureService.On("GetDecision", s.testFeatureDecisionContext, testUserContext).Return(expectedDecision, nil) + s.mockFeatureService.On("GetDecision", s.testFeatureDecisionContext, testUserContext, s.options, s.reasons).Return(expectedDecision, nil) compositeFeatureService := &CompositeFeatureService{ featureServices: []FeatureService{ s.mockFeatureService, s.mockFeatureService2, }, - logger:logging.GetLogger("sdkKey", "CompositeFeatureService"), + logger: logging.GetLogger("sdkKey", "CompositeFeatureService"), } - decision, err := compositeFeatureService.GetDecision(s.testFeatureDecisionContext, testUserContext) + decision, err := compositeFeatureService.GetDecision(s.testFeatureDecisionContext, testUserContext, s.options, s.reasons) s.Equal(expectedDecision, decision) s.NoError(err) s.mockFeatureService.AssertExpectations(s.T()) @@ -82,21 +87,21 @@ func (s *CompositeFeatureServiceTestSuite) TestGetDecisionFallthrough() { } nilDecision := FeatureDecision{} - s.mockFeatureService.On("GetDecision", s.testFeatureDecisionContext, testUserContext).Return(nilDecision, nil) + s.mockFeatureService.On("GetDecision", s.testFeatureDecisionContext, testUserContext, s.options, s.reasons).Return(nilDecision, nil) expectedDecision := FeatureDecision{ Variation: &testExp1113Var2223, } - s.mockFeatureService2.On("GetDecision", s.testFeatureDecisionContext, testUserContext).Return(expectedDecision, nil) + s.mockFeatureService2.On("GetDecision", s.testFeatureDecisionContext, testUserContext, s.options, s.reasons).Return(expectedDecision, nil) compositeFeatureService := &CompositeFeatureService{ featureServices: []FeatureService{ s.mockFeatureService, s.mockFeatureService2, }, - logger:logging.GetLogger("sdkKey", "CompositeFeatureService"), + logger: logging.GetLogger("sdkKey", "CompositeFeatureService"), } - decision, err := compositeFeatureService.GetDecision(s.testFeatureDecisionContext, testUserContext) + decision, err := compositeFeatureService.GetDecision(s.testFeatureDecisionContext, testUserContext, s.options, s.reasons) s.Equal(expectedDecision, decision) s.NoError(err) s.mockFeatureService.AssertExpectations(s.T()) @@ -112,22 +117,21 @@ func (s *CompositeFeatureServiceTestSuite) TestGetDecisionReturnsError() { shouldBeIgnoredDecision := FeatureDecision{ Variation: &testExp1113Var2223, } - s.mockFeatureService.On("GetDecision", s.testFeatureDecisionContext, testUserContext).Return(shouldBeIgnoredDecision, errors.New("Error making decision")) + s.mockFeatureService.On("GetDecision", s.testFeatureDecisionContext, testUserContext, s.options, s.reasons).Return(shouldBeIgnoredDecision, errors.New("Error making decision")) expectedDecision := FeatureDecision{ Variation: &testExp1113Var2224, } - s.mockFeatureService2.On("GetDecision", s.testFeatureDecisionContext, testUserContext).Return(expectedDecision, nil) + s.mockFeatureService2.On("GetDecision", s.testFeatureDecisionContext, testUserContext, s.options, s.reasons).Return(expectedDecision, nil) compositeFeatureService := &CompositeFeatureService{ featureServices: []FeatureService{ s.mockFeatureService, s.mockFeatureService2, }, - logger:logging.GetLogger("sdkKey", "CompositeFeatureService"), - + logger: logging.GetLogger("sdkKey", "CompositeFeatureService"), } - decision, err := compositeFeatureService.GetDecision(s.testFeatureDecisionContext, testUserContext) + decision, err := compositeFeatureService.GetDecision(s.testFeatureDecisionContext, testUserContext, s.options, s.reasons) s.Equal(expectedDecision, decision) s.NoError(err) s.mockFeatureService.AssertExpectations(s.T()) @@ -143,18 +147,17 @@ func (s *CompositeFeatureServiceTestSuite) TestGetDecisionReturnsLastDecisionWit expectedDecision := FeatureDecision{ Variation: &testExp1113Var2223, } - s.mockFeatureService.On("GetDecision", s.testFeatureDecisionContext, testUserContext).Return(expectedDecision, errors.New("Error making decision")) - - s.mockFeatureService2.On("GetDecision", s.testFeatureDecisionContext, testUserContext).Return(expectedDecision, errors.New("test error")) + s.mockFeatureService.On("GetDecision", s.testFeatureDecisionContext, testUserContext, s.options, s.reasons).Return(expectedDecision, errors.New("Error making decision")) + s.mockFeatureService2.On("GetDecision", s.testFeatureDecisionContext, testUserContext, s.options, s.reasons).Return(expectedDecision, errors.New("test error")) compositeFeatureService := &CompositeFeatureService{ featureServices: []FeatureService{ s.mockFeatureService, s.mockFeatureService2, }, - logger:logging.GetLogger("sdkKey", "CompositeFeatureService"), + logger: logging.GetLogger("sdkKey", "CompositeFeatureService"), } - decision, err := compositeFeatureService.GetDecision(s.testFeatureDecisionContext, testUserContext) + decision, err := compositeFeatureService.GetDecision(s.testFeatureDecisionContext, testUserContext, s.options, s.reasons) s.Equal(expectedDecision, decision) s.Error(err) s.Equal(err.Error(), "test error") diff --git a/pkg/decision/composite_service.go b/pkg/decision/composite_service.go index e31d9d9fc..5cf99df6c 100644 --- a/pkg/decision/composite_service.go +++ b/pkg/decision/composite_service.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -19,6 +19,8 @@ package decision import ( "fmt" + + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" "github.com/optimizely/go-sdk/pkg/notification" @@ -63,15 +65,14 @@ func NewCompositeService(sdkKey string, options ...CSOptionFunc) *CompositeServi } // GetFeatureDecision returns a decision for the given feature key -func (s CompositeService) GetFeatureDecision(featureDecisionContext FeatureDecisionContext, userContext entities.UserContext) (FeatureDecision, error) { - featureDecision, err := s.compositeFeatureService.GetDecision(featureDecisionContext, userContext) - +func (s CompositeService) GetFeatureDecision(featureDecisionContext FeatureDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (FeatureDecision, error) { + featureDecision, err := s.compositeFeatureService.GetDecision(featureDecisionContext, userContext, options, reasons) return featureDecision, err } // GetExperimentDecision returns a decision for the given experiment key -func (s CompositeService) GetExperimentDecision(experimentDecisionContext ExperimentDecisionContext, userContext entities.UserContext) (experimentDecision ExperimentDecision, err error) { - if experimentDecision, err = s.compositeExperimentService.GetDecision(experimentDecisionContext, userContext); err != nil { +func (s CompositeService) GetExperimentDecision(experimentDecisionContext ExperimentDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (experimentDecision ExperimentDecision, err error) { + if experimentDecision, err = s.compositeExperimentService.GetDecision(experimentDecisionContext, userContext, options, reasons); err != nil { return experimentDecision, err } diff --git a/pkg/decision/composite_service_test.go b/pkg/decision/composite_service_test.go index fdcebbc74..97d0e218c 100644 --- a/pkg/decision/composite_service_test.go +++ b/pkg/decision/composite_service_test.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/suite" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/notification" ) @@ -28,6 +29,8 @@ import ( type CompositeServiceFeatureTestSuite struct { suite.Suite decisionContext FeatureDecisionContext + options decide.OptimizelyDecideOptions + reasons decide.DecisionReasons mockFeatureService *MockFeatureDecisionService testUserContext entities.UserContext } @@ -39,6 +42,8 @@ func (s *CompositeServiceFeatureTestSuite) SetupTest() { Feature: &testFeat3333, ProjectConfig: mockConfig, } + s.options = decide.OptimizelyDecideOptions{} + s.reasons = decide.NewDecisionReasons(s.options) s.mockFeatureService = new(MockFeatureDecisionService) s.testUserContext = entities.UserContext{ ID: "test_user", @@ -53,8 +58,8 @@ func (s *CompositeServiceFeatureTestSuite) TestGetFeatureDecision() { decisionService := &CompositeService{ compositeFeatureService: s.mockFeatureService, } - s.mockFeatureService.On("GetDecision", s.decisionContext, s.testUserContext).Return(expectedFeatureDecision, nil) - featureDecision, err := decisionService.GetFeatureDecision(s.decisionContext, s.testUserContext) + s.mockFeatureService.On("GetDecision", s.decisionContext, s.testUserContext, s.options, s.reasons).Return(expectedFeatureDecision, nil) + featureDecision, err := decisionService.GetFeatureDecision(s.decisionContext, s.testUserContext, s.options, s.reasons) // Test assertions s.Equal(expectedFeatureDecision, featureDecision) @@ -80,6 +85,8 @@ func (s *CompositeServiceFeatureTestSuite) TestNewCompositeServiceWithCustomOpti type CompositeServiceExperimentTestSuite struct { suite.Suite decisionContext ExperimentDecisionContext + options decide.OptimizelyDecideOptions + reasons decide.DecisionReasons mockExperimentService *MockExperimentDecisionService testUserContext entities.UserContext } @@ -90,6 +97,8 @@ func (s *CompositeServiceExperimentTestSuite) SetupTest() { Experiment: &testExp1111, ProjectConfig: mockConfig, } + s.options = decide.OptimizelyDecideOptions{} + s.reasons = decide.NewDecisionReasons(s.options) s.mockExperimentService = new(MockExperimentDecisionService) s.testUserContext = entities.UserContext{ ID: "test_user", @@ -103,8 +112,8 @@ func (s *CompositeServiceExperimentTestSuite) TestGetExperimentDecision() { decisionService := &CompositeService{ compositeExperimentService: s.mockExperimentService, } - s.mockExperimentService.On("GetDecision", s.decisionContext, s.testUserContext).Return(expectedExperimentDecision, nil) - experimentDecision, err := decisionService.GetExperimentDecision(s.decisionContext, s.testUserContext) + s.mockExperimentService.On("GetDecision", s.decisionContext, s.testUserContext, s.options, s.reasons).Return(expectedExperimentDecision, nil) + experimentDecision, err := decisionService.GetExperimentDecision(s.decisionContext, s.testUserContext, s.options, s.reasons) // Test assertions s.Equal(expectedExperimentDecision, experimentDecision) @@ -121,8 +130,8 @@ func (s *CompositeServiceExperimentTestSuite) TestDecisionListeners() { compositeExperimentService: s.mockExperimentService, notificationCenter: notificationCenter, } - s.mockExperimentService.On("GetDecision", s.decisionContext, s.testUserContext).Return(expectedExperimentDecision, nil) - decisionService.GetExperimentDecision(s.decisionContext, s.testUserContext) + s.mockExperimentService.On("GetDecision", s.decisionContext, s.testUserContext, s.options, s.reasons).Return(expectedExperimentDecision, nil) + decisionService.GetExperimentDecision(s.decisionContext, s.testUserContext, s.options, s.reasons) var numberOfCalls = 0 @@ -133,13 +142,13 @@ func (s *CompositeServiceExperimentTestSuite) TestDecisionListeners() { id, _ := decisionService.OnDecision(callback) s.NotEqual(0, id) - decisionService.GetExperimentDecision(s.decisionContext, s.testUserContext) + decisionService.GetExperimentDecision(s.decisionContext, s.testUserContext, s.options, s.reasons) s.Equal(numberOfCalls, 1) err := decisionService.RemoveOnDecision(id) s.NoError(err) - decisionService.GetExperimentDecision(s.decisionContext, s.testUserContext) + decisionService.GetExperimentDecision(s.decisionContext, s.testUserContext, s.options, s.reasons) s.Equal(numberOfCalls, 1) } diff --git a/pkg/decision/evaluator/condition.go b/pkg/decision/evaluator/condition.go index 39f5f18cb..d77da5b1d 100644 --- a/pkg/decision/evaluator/condition.go +++ b/pkg/decision/evaluator/condition.go @@ -18,8 +18,10 @@ package evaluator import ( + "errors" "fmt" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision/evaluator/matchers" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" @@ -27,7 +29,7 @@ import ( // ItemEvaluator evaluates a condition against the given user's attributes type ItemEvaluator interface { - Evaluate(interface{}, *entities.TreeParameters) (bool, error) + Evaluate(interface{}, *entities.TreeParameters, decide.DecisionReasons) (bool, error) } // CustomAttributeConditionEvaluator evaluates conditions with custom attributes @@ -41,13 +43,14 @@ func NewCustomAttributeConditionEvaluator(logger logging.OptimizelyLogProducer) } // Evaluate returns true if the given user's attributes match the condition -func (c CustomAttributeConditionEvaluator) Evaluate(condition entities.Condition, condTreeParams *entities.TreeParameters) (bool, error) { +func (c CustomAttributeConditionEvaluator) Evaluate(condition entities.Condition, condTreeParams *entities.TreeParameters, reasons decide.DecisionReasons) (bool, error) { // We should only be evaluating custom attributes if condition.Type != customAttributeType { - c.logger.Warning(fmt.Sprintf(logging.UnknownConditionType.String(), condition.StringRepresentation)) - return false, fmt.Errorf(`unable to evaluate condition of type "%s"`, condition.Type) + errorMessage := fmt.Sprintf(`unable to evaluate condition of type "%s"`, condition.Type) + reasons.AddError(errorMessage) + return false, errors.New(errorMessage) } matchType := condition.Match @@ -58,10 +61,12 @@ func (c CustomAttributeConditionEvaluator) Evaluate(condition entities.Condition matcher, ok := matchers.Get(matchType) if !ok { c.logger.Warning(fmt.Sprintf(logging.UnknownMatchType.String(), condition.StringRepresentation)) - return false, fmt.Errorf(`invalid Condition matcher "%s"`, condition.Match) + errorMessage := fmt.Sprintf(`invalid Condition matcher "%s"`, condition.Match) + reasons.AddError(errorMessage) + return false, errors.New(errorMessage) } - return matcher(condition, *condTreeParams.User, c.logger) + return matcher(condition, *condTreeParams.User, c.logger, reasons) } // AudienceConditionEvaluator evaluates conditions with audience condition @@ -75,19 +80,22 @@ func NewAudienceConditionEvaluator(logger logging.OptimizelyLogProducer) *Audien } // Evaluate returns true if the given user's attributes match the condition -func (c AudienceConditionEvaluator) Evaluate(audienceID string, condTreeParams *entities.TreeParameters) (bool, error) { - +func (c AudienceConditionEvaluator) Evaluate(audienceID string, condTreeParams *entities.TreeParameters, reasons decide.DecisionReasons) (bool, error) { if audience, ok := condTreeParams.AudienceMap[audienceID]; ok { c.logger.Debug(fmt.Sprintf(logging.AudienceEvaluationStarted.String(), audienceID)) condTree := audience.ConditionTree conditionTreeEvaluator := NewMixedTreeEvaluator(c.logger) - retValue, isValid := conditionTreeEvaluator.Evaluate(condTree, condTreeParams) + retValue, isValid := conditionTreeEvaluator.Evaluate(condTree, condTreeParams, reasons) if !isValid { - return false, fmt.Errorf(`an error occurred while evaluating nested tree for audience ID "%s"`, audienceID) + errorMessage := fmt.Sprintf(`an error occurred while evaluating nested tree for audience ID "%s"`, audienceID) + reasons.AddError(errorMessage) + return false, errors.New(errorMessage) } c.logger.Debug(fmt.Sprintf(logging.AudienceEvaluatedTo.String(), audienceID, retValue)) return retValue, nil } - return false, fmt.Errorf(`unable to evaluate nested tree for audience ID "%s"`, audienceID) + errorMessage := fmt.Sprintf(`unable to evaluate nested tree for audience ID "%s"`, audienceID) + reasons.AddError(errorMessage) + return false, errors.New(errorMessage) } diff --git a/pkg/decision/evaluator/condition_test.go b/pkg/decision/evaluator/condition_test.go index 0cce66ccf..63686db7f 100644 --- a/pkg/decision/evaluator/condition_test.go +++ b/pkg/decision/evaluator/condition_test.go @@ -20,6 +20,7 @@ import ( "fmt" "testing" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" "github.com/stretchr/testify/suite" @@ -29,11 +30,13 @@ type ConditionTestSuite struct { suite.Suite mockLogger *MockLogger conditionEvaluator *CustomAttributeConditionEvaluator + reasons decide.DecisionReasons } func (s *ConditionTestSuite) SetupTest() { s.mockLogger = new(MockLogger) s.conditionEvaluator = NewCustomAttributeConditionEvaluator(s.mockLogger) + s.reasons = decide.NewDecisionReasons(decide.OptimizelyDecideOptions{}) } func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluator() { @@ -52,7 +55,7 @@ func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluator() { } condTreeParams := entities.NewTreeParameters(&user, map[string]entities.Audience{}) - result, _ := s.conditionEvaluator.Evaluate(condition, condTreeParams) + result, _ := s.conditionEvaluator.Evaluate(condition, condTreeParams, s.reasons) s.Equal(result, true) // Test condition fails @@ -61,7 +64,7 @@ func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluator() { "string_foo": "not_foo", }, } - result, _ = s.conditionEvaluator.Evaluate(condition, condTreeParams) + result, _ = s.conditionEvaluator.Evaluate(condition, condTreeParams, s.reasons) s.Equal(result, false) } @@ -80,7 +83,7 @@ func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorWithoutMatchTy } condTreeParams := entities.NewTreeParameters(&user, map[string]entities.Audience{}) - result, _ := s.conditionEvaluator.Evaluate(condition, condTreeParams) + result, _ := s.conditionEvaluator.Evaluate(condition, condTreeParams, s.reasons) s.Equal(result, true) // Test condition fails @@ -89,7 +92,7 @@ func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorWithoutMatchTy "string_foo": "not_foo", }, } - result, _ = s.conditionEvaluator.Evaluate(condition, condTreeParams) + result, _ = s.conditionEvaluator.Evaluate(condition, condTreeParams, s.reasons) s.Equal(result, false) } @@ -110,7 +113,7 @@ func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorWithInvalidMat condTreeParams := entities.NewTreeParameters(&user, map[string]entities.Audience{}) s.mockLogger.On("Warning", fmt.Sprintf(logging.UnknownMatchType.String(), "")) - result, _ := s.conditionEvaluator.Evaluate(condition, condTreeParams) + result, _ := s.conditionEvaluator.Evaluate(condition, condTreeParams, s.reasons) s.Equal(result, false) s.mockLogger.AssertExpectations(s.T()) } @@ -131,12 +134,12 @@ func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorWithUnknownTyp condTreeParams := entities.NewTreeParameters(&user, map[string]entities.Audience{}) s.mockLogger.On("Warning", fmt.Sprintf(logging.UnknownConditionType.String(), "")) - result, _ := s.conditionEvaluator.Evaluate(condition, condTreeParams) + result, _ := s.conditionEvaluator.Evaluate(condition, condTreeParams, s.reasons) s.Equal(result, false) s.mockLogger.AssertExpectations(s.T()) } -func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorForGeSemver() { +func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorForGeSemver() { conditionEvaluator := CustomAttributeConditionEvaluator{} condition := entities.Condition{ Match: "semver_ge", @@ -153,11 +156,11 @@ func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorForGeSemver() } condTreeParams := entities.NewTreeParameters(&user, map[string]entities.Audience{}) - result, _ := conditionEvaluator.Evaluate(condition, condTreeParams) - s.Equal( result, true) + result, _ := conditionEvaluator.Evaluate(condition, condTreeParams, s.reasons) + s.Equal(result, true) } -func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorForGeSemverBeta() { +func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorForGeSemverBeta() { conditionEvaluator := CustomAttributeConditionEvaluator{} condition := entities.Condition{ Match: "semver_ge", @@ -174,11 +177,11 @@ func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorForGeSemverBe } condTreeParams := entities.NewTreeParameters(&user, map[string]entities.Audience{}) - result, _ := conditionEvaluator.Evaluate(condition, condTreeParams) + result, _ := conditionEvaluator.Evaluate(condition, condTreeParams, s.reasons) s.Equal(true, result) } -func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorForGeSemverInvalid() { +func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorForGeSemverInvalid() { conditionEvaluator := CustomAttributeConditionEvaluator{} condition := entities.Condition{ Match: "semver_ge", @@ -195,7 +198,7 @@ func (s *ConditionTestSuite) TestCustomAttributeConditionEvaluatorForGeSemverIn } condTreeParams := entities.NewTreeParameters(&user, map[string]entities.Audience{}) - _, err := conditionEvaluator.Evaluate(condition, condTreeParams) + _, err := conditionEvaluator.Evaluate(condition, condTreeParams, s.reasons) s.NotNil(err) } diff --git a/pkg/decision/evaluator/condition_tree.go b/pkg/decision/evaluator/condition_tree.go index 3ded0e716..37f8b8160 100644 --- a/pkg/decision/evaluator/condition_tree.go +++ b/pkg/decision/evaluator/condition_tree.go @@ -20,6 +20,7 @@ package evaluator import ( "fmt" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) @@ -37,7 +38,7 @@ const ( // TreeEvaluator evaluates a tree type TreeEvaluator interface { - Evaluate(*entities.TreeNode, *entities.TreeParameters) (evalResult, isValid bool) + Evaluate(*entities.TreeNode, *entities.TreeParameters, decide.DecisionReasons) (evalResult, isValid bool) } // MixedTreeEvaluator evaluates a tree of mixed node types (condition node or audience nodes) @@ -51,16 +52,16 @@ func NewMixedTreeEvaluator(logger logging.OptimizelyLogProducer) *MixedTreeEvalu } // Evaluate returns whether the userAttributes satisfy the given condition tree and the evaluation of the condition is valid or not (to handle null bubbling) -func (c MixedTreeEvaluator) Evaluate(node *entities.TreeNode, condTreeParams *entities.TreeParameters) (evalResult, isValid bool) { +func (c MixedTreeEvaluator) Evaluate(node *entities.TreeNode, condTreeParams *entities.TreeParameters, reasons decide.DecisionReasons) (evalResult, isValid bool) { operator := node.Operator if operator != "" { switch operator { case andOperator: - return c.evaluateAnd(node.Nodes, condTreeParams) + return c.evaluateAnd(node.Nodes, condTreeParams, reasons) case notOperator: - return c.evaluateNot(node.Nodes, condTreeParams) + return c.evaluateNot(node.Nodes, condTreeParams, reasons) default: // orOperator - return c.evaluateOr(node.Nodes, condTreeParams) + return c.evaluateOr(node.Nodes, condTreeParams, reasons) } } @@ -69,10 +70,10 @@ func (c MixedTreeEvaluator) Evaluate(node *entities.TreeNode, condTreeParams *en switch v := node.Item.(type) { case entities.Condition: evaluator := NewCustomAttributeConditionEvaluator(c.logger) - result, err = evaluator.Evaluate(node.Item.(entities.Condition), condTreeParams) + result, err = evaluator.Evaluate(node.Item.(entities.Condition), condTreeParams, reasons) case string: evaluator := NewAudienceConditionEvaluator(c.logger) - result, err = evaluator.Evaluate(node.Item.(string), condTreeParams) + result, err = evaluator.Evaluate(node.Item.(string), condTreeParams, reasons) default: fmt.Printf("I don't know about type %T!\n", v) return false, false @@ -85,10 +86,10 @@ func (c MixedTreeEvaluator) Evaluate(node *entities.TreeNode, condTreeParams *en return result, true } -func (c MixedTreeEvaluator) evaluateAnd(nodes []*entities.TreeNode, condTreeParams *entities.TreeParameters) (evalResult, isValid bool) { +func (c MixedTreeEvaluator) evaluateAnd(nodes []*entities.TreeNode, condTreeParams *entities.TreeParameters, reasons decide.DecisionReasons) (evalResult, isValid bool) { sawInvalid := false for _, node := range nodes { - result, isValid := c.Evaluate(node, condTreeParams) + result, isValid := c.Evaluate(node, condTreeParams, reasons) if !isValid { return false, isValid } else if !result { @@ -104,9 +105,9 @@ func (c MixedTreeEvaluator) evaluateAnd(nodes []*entities.TreeNode, condTreePara return true, true } -func (c MixedTreeEvaluator) evaluateNot(nodes []*entities.TreeNode, condTreeParams *entities.TreeParameters) (evalResult, isValid bool) { +func (c MixedTreeEvaluator) evaluateNot(nodes []*entities.TreeNode, condTreeParams *entities.TreeParameters, reasons decide.DecisionReasons) (evalResult, isValid bool) { if len(nodes) > 0 { - result, isValid := c.Evaluate(nodes[0], condTreeParams) + result, isValid := c.Evaluate(nodes[0], condTreeParams, reasons) if !isValid { return false, false } @@ -115,10 +116,10 @@ func (c MixedTreeEvaluator) evaluateNot(nodes []*entities.TreeNode, condTreePara return false, false } -func (c MixedTreeEvaluator) evaluateOr(nodes []*entities.TreeNode, condTreeParams *entities.TreeParameters) (evalResult, isValid bool) { +func (c MixedTreeEvaluator) evaluateOr(nodes []*entities.TreeNode, condTreeParams *entities.TreeParameters, reasons decide.DecisionReasons) (evalResult, isValid bool) { sawInvalid := false for _, node := range nodes { - result, isValid := c.Evaluate(node, condTreeParams) + result, isValid := c.Evaluate(node, condTreeParams, reasons) if !isValid { sawInvalid = true } else if result { diff --git a/pkg/decision/evaluator/condition_tree_test.go b/pkg/decision/evaluator/condition_tree_test.go index 40fdd20fb..072a76f12 100644 --- a/pkg/decision/evaluator/condition_tree_test.go +++ b/pkg/decision/evaluator/condition_tree_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" + "github.com/optimizely/go-sdk/pkg/decide" e "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) @@ -71,11 +72,13 @@ var int42Condition = e.Condition{ type ConditionTreeTestSuite struct { suite.Suite mockLogger *MockLogger + reasons decide.DecisionReasons conditionTreeEvaluator *MixedTreeEvaluator } func (s *ConditionTreeTestSuite) SetupTest() { s.mockLogger = new(MockLogger) + s.reasons = decide.NewDecisionReasons(decide.OptimizelyDecideOptions{}) s.conditionTreeEvaluator = NewMixedTreeEvaluator(s.mockLogger) } @@ -96,7 +99,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateSimpleCondition() { }, } condTreeParams := e.NewTreeParameters(&user, map[string]e.Audience{}) - result, _ := s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ := s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.True(result) // Test no match @@ -105,7 +108,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateSimpleCondition() { "string_foo": "not foo", }, } - result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.False(result) } @@ -130,7 +133,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateMultipleOrConditions() } condTreeParams := e.NewTreeParameters(&user, map[string]e.Audience{}) - result, _ := s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ := s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.True(result) // Test match bool @@ -140,7 +143,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateMultipleOrConditions() }, } s.mockLogger.On("Debug", fmt.Sprintf(logging.NullUserAttribute.String(), "", "string_foo")) - result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.True(result) @@ -151,7 +154,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateMultipleOrConditions() "bool_true": true, }, } - result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.True(result) // Test no match @@ -161,7 +164,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateMultipleOrConditions() "bool_true": false, }, } - result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.False(result) s.mockLogger.AssertExpectations(s.T()) } @@ -188,7 +191,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateMultipleAndConditions( condTreeParams := e.NewTreeParameters(&user, map[string]e.Audience{}) s.mockLogger.On("Debug", fmt.Sprintf(logging.NullUserAttribute.String(), "", "bool_true")) - result, _ := s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ := s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.False(result) // Test only bool match with NULL bubbling @@ -199,7 +202,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateMultipleAndConditions( } s.mockLogger.On("Debug", fmt.Sprintf(logging.NullUserAttribute.String(), "", "string_foo")) - result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.False(result) // Test match both @@ -209,7 +212,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateMultipleAndConditions( "bool_true": true, }, } - result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.True(result) // Test no match @@ -219,7 +222,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateMultipleAndConditions( "bool_true": false, }, } - result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.False(result) s.mockLogger.AssertExpectations(s.T()) } @@ -256,7 +259,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateNotCondition() { } condTreeParams := e.NewTreeParameters(&user, map[string]e.Audience{}) - result, _ := s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ := s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.True(result) // Test match bool @@ -266,7 +269,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateNotCondition() { }, } s.mockLogger.On("Debug", fmt.Sprintf(logging.NullUserAttribute.String(), "", "string_foo")) - result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.True(result) // Test match both @@ -276,7 +279,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateNotCondition() { "bool_true": false, }, } - result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.True(result) // Test no match @@ -286,7 +289,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateNotCondition() { "bool_true": true, }, } - result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.False(result) s.mockLogger.AssertExpectations(s.T()) } @@ -336,7 +339,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateMultipleMixedCondition } condTreeParams := e.NewTreeParameters(&user, map[string]e.Audience{}) - result, _ := s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ := s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.True(result) // Test only match the NOT condition @@ -347,7 +350,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateMultipleMixedCondition "int_42": 43, }, } - result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.True(result) // Test only match the int condition @@ -358,7 +361,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateMultipleMixedCondition "int_42": 42, }, } - result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.True(result) // Test no match @@ -369,7 +372,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateMultipleMixedCondition "int_42": 43, }, } - result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams) + result, _ = s.conditionTreeEvaluator.Evaluate(conditionTree, condTreeParams, s.reasons) s.False(result) } @@ -438,7 +441,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateAnAudienceTreeSingleAu s.mockLogger.On("Debug", fmt.Sprintf(logging.AudienceEvaluationStarted.String(), "11111")) s.mockLogger.On("Debug", fmt.Sprintf(logging.AudienceEvaluatedTo.String(), "11111", true)) - result, _ := s.conditionTreeEvaluator.Evaluate(audienceTree, treeParams) + result, _ := s.conditionTreeEvaluator.Evaluate(audienceTree, treeParams, s.reasons) s.True(result) s.mockLogger.AssertExpectations(s.T()) } @@ -468,7 +471,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateAnAudienceTreeMultiple } s.mockLogger.On("Debug", fmt.Sprintf(logging.AudienceEvaluationStarted.String(), "11111")) s.mockLogger.On("Debug", fmt.Sprintf(logging.AudienceEvaluatedTo.String(), "11111", true)) - result, _ := s.conditionTreeEvaluator.Evaluate(audienceTree, treeParams) + result, _ := s.conditionTreeEvaluator.Evaluate(audienceTree, treeParams, s.reasons) s.True(result) // Test only matches audience 11112 @@ -487,7 +490,7 @@ func (s *ConditionTreeTestSuite) TestConditionTreeEvaluateAnAudienceTreeMultiple s.mockLogger.On("Debug", fmt.Sprintf(logging.AudienceEvaluationStarted.String(), "11112")) s.mockLogger.On("Debug", fmt.Sprintf(logging.AudienceEvaluatedTo.String(), "11112", true)) - result, _ = s.conditionTreeEvaluator.Evaluate(audienceTree, treeParams) + result, _ = s.conditionTreeEvaluator.Evaluate(audienceTree, treeParams, s.reasons) s.True(result) s.mockLogger.AssertExpectations(s.T()) } diff --git a/pkg/decision/evaluator/matchers/exact.go b/pkg/decision/evaluator/matchers/exact.go index e6e883684..966e43ca6 100644 --- a/pkg/decision/evaluator/matchers/exact.go +++ b/pkg/decision/evaluator/matchers/exact.go @@ -20,13 +20,14 @@ package matchers import ( "fmt" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision/evaluator/matchers/utils" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) // ExactMatcher matches against the "exact" match type -func ExactMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer) (bool, error) { +func ExactMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer, reasons decide.DecisionReasons) (bool, error) { if !user.CheckAttributeExists(condition.Name) { logger.Debug(fmt.Sprintf(logging.NullUserAttribute.String(), condition.StringRepresentation, condition.Name)) return false, fmt.Errorf(`no attribute named "%s"`, condition.Name) diff --git a/pkg/decision/evaluator/matchers/exact_test.go b/pkg/decision/evaluator/matchers/exact_test.go index f19f33fd0..f2a24ef97 100644 --- a/pkg/decision/evaluator/matchers/exact_test.go +++ b/pkg/decision/evaluator/matchers/exact_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) @@ -50,11 +51,13 @@ func (m *MockLogger) Error(message string, err interface{}) { type ExactTestSuite struct { suite.Suite mockLogger *MockLogger + reasons decide.DecisionReasons matcher Matcher } func (s *ExactTestSuite) SetupTest() { s.mockLogger = new(MockLogger) + s.reasons = decide.NewDecisionReasons(decide.OptimizelyDecideOptions{}) s.matcher, _ = Get(ExactMatchType) } @@ -71,7 +74,7 @@ func (s *ExactTestSuite) TestExactMatcherString() { "string_foo": "foo", }, } - result, err := s.matcher(condition, user, s.mockLogger) + result, err := s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.True(result) @@ -82,7 +85,7 @@ func (s *ExactTestSuite) TestExactMatcherString() { }, } - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.False(result) @@ -93,7 +96,7 @@ func (s *ExactTestSuite) TestExactMatcherString() { }, } s.mockLogger.On("Debug", fmt.Sprintf(logging.NullUserAttribute.String(), "", "string_foo")) - _, err = s.matcher(condition, user, s.mockLogger) + _, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) // Test attribute of different type @@ -103,7 +106,7 @@ func (s *ExactTestSuite) TestExactMatcherString() { }, } s.mockLogger.On("Warning", fmt.Sprintf(logging.InvalidAttributeValueType.String(), "", 121, "string_foo")) - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) s.False(false) s.mockLogger.AssertExpectations(s.T()) @@ -122,7 +125,7 @@ func (s *ExactTestSuite) TestExactMatcherBool() { "bool_true": true, }, } - result, err := s.matcher(condition, user, s.mockLogger) + result, err := s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.True(result) @@ -133,7 +136,7 @@ func (s *ExactTestSuite) TestExactMatcherBool() { }, } - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.False(result) @@ -145,7 +148,7 @@ func (s *ExactTestSuite) TestExactMatcherBool() { } s.mockLogger.On("Debug", fmt.Sprintf(logging.NullUserAttribute.String(), "", "bool_true")) - _, err = s.matcher(condition, user, s.mockLogger) + _, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) // Test attribute of different type @@ -155,7 +158,7 @@ func (s *ExactTestSuite) TestExactMatcherBool() { }, } s.mockLogger.On("Warning", fmt.Sprintf(logging.InvalidAttributeValueType.String(), "", 121, "bool_true")) - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) s.False(result) s.mockLogger.AssertExpectations(s.T()) @@ -174,7 +177,7 @@ func (s *ExactTestSuite) TestExactMatcherInt() { "int_42": 42, }, } - result, err := s.matcher(condition, user, s.mockLogger) + result, err := s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.True(result) @@ -185,7 +188,7 @@ func (s *ExactTestSuite) TestExactMatcherInt() { }, } - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.True(result) @@ -196,7 +199,7 @@ func (s *ExactTestSuite) TestExactMatcherInt() { }, } - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.False(result) @@ -207,7 +210,7 @@ func (s *ExactTestSuite) TestExactMatcherInt() { }, } s.mockLogger.On("Debug", fmt.Sprintf(logging.NullUserAttribute.String(), "", "int_42")) - _, err = s.matcher(condition, user, s.mockLogger) + _, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) // Test attribute of different type @@ -217,7 +220,7 @@ func (s *ExactTestSuite) TestExactMatcherInt() { }, } s.mockLogger.On("Warning", fmt.Sprintf(logging.InvalidAttributeValueType.String(), "", "test", "int_42")) - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) s.False(result) s.mockLogger.AssertExpectations(s.T()) @@ -236,7 +239,7 @@ func (s *ExactTestSuite) TestExactMatcherFloat() { "float_4_2": 4.2, }, } - result, err := s.matcher(condition, user, s.mockLogger) + result, err := s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.True(result) @@ -247,7 +250,7 @@ func (s *ExactTestSuite) TestExactMatcherFloat() { }, } - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.False(result) @@ -258,7 +261,7 @@ func (s *ExactTestSuite) TestExactMatcherFloat() { }, } s.mockLogger.On("Debug", fmt.Sprintf(logging.NullUserAttribute.String(), "", "float_4_2")) - _, err = s.matcher(condition, user, s.mockLogger) + _, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) // Test attribute of different type @@ -268,7 +271,7 @@ func (s *ExactTestSuite) TestExactMatcherFloat() { }, } s.mockLogger.On("Warning", fmt.Sprintf(logging.InvalidAttributeValueType.String(), "", "test", "float_4_2")) - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) s.False(result) s.mockLogger.AssertExpectations(s.T()) @@ -288,7 +291,7 @@ func (s *ExactTestSuite) TestExactMatcherUnsupportedConditionValue() { }, } s.mockLogger.On("Warning", fmt.Sprintf(logging.UnsupportedConditionValue.String(), "")) - result, err := s.matcher(condition, user, s.mockLogger) + result, err := s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) s.False(result) s.mockLogger.AssertExpectations(s.T()) diff --git a/pkg/decision/evaluator/matchers/exists.go b/pkg/decision/evaluator/matchers/exists.go index 7c2a75d66..037045128 100644 --- a/pkg/decision/evaluator/matchers/exists.go +++ b/pkg/decision/evaluator/matchers/exists.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -18,11 +18,12 @@ package matchers import ( + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) // ExistsMatcher matches against the "exists" match type -func ExistsMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer) (bool, error) { +func ExistsMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer, reasons decide.DecisionReasons) (bool, error) { return user.CheckAttributeExists(condition.Name), nil } diff --git a/pkg/decision/evaluator/matchers/exists_test.go b/pkg/decision/evaluator/matchers/exists_test.go index 43e23d95d..22d9bb159 100644 --- a/pkg/decision/evaluator/matchers/exists_test.go +++ b/pkg/decision/evaluator/matchers/exists_test.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" ) @@ -38,7 +39,8 @@ func TestExistsMatcher(t *testing.T) { "string_foo": "any_value", }, } - result, err := existsMatcher(condition, user, nil) + + result, err := existsMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.True(t, result) @@ -49,7 +51,7 @@ func TestExistsMatcher(t *testing.T) { }, } - result, err = existsMatcher(condition, user, nil) + result, err = existsMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.False(t, result) @@ -57,7 +59,7 @@ func TestExistsMatcher(t *testing.T) { user = entities.UserContext{ Attributes: map[string]interface{}{}, } - result, err = ExistsMatcher(condition, user, nil) + result, err = ExistsMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.False(t, result) } diff --git a/pkg/decision/evaluator/matchers/ge.go b/pkg/decision/evaluator/matchers/ge.go index 6c18475f5..12644022b 100644 --- a/pkg/decision/evaluator/matchers/ge.go +++ b/pkg/decision/evaluator/matchers/ge.go @@ -19,6 +19,8 @@ package matchers import ( "fmt" + + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/logging" "github.com/optimizely/go-sdk/pkg/decision/evaluator/matchers/utils" @@ -26,7 +28,7 @@ import ( ) // GeMatcher matches against the "ge" match type -func GeMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer) (bool, error) { +func GeMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer, reasons decide.DecisionReasons) (bool, error) { if floatValue, ok := utils.ToFloat(condition.Value); ok { attributeValue, err := user.GetFloatAttribute(condition.Name) diff --git a/pkg/decision/evaluator/matchers/ge_test.go b/pkg/decision/evaluator/matchers/ge_test.go index a74909594..49ace5871 100644 --- a/pkg/decision/evaluator/matchers/ge_test.go +++ b/pkg/decision/evaluator/matchers/ge_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" ) @@ -39,7 +40,7 @@ func TestGeMatcherInt(t *testing.T) { "int_42": 43, }, } - result, err := geMatcher(condition, user, nil) + result, err := geMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.True(t, result) @@ -50,7 +51,7 @@ func TestGeMatcherInt(t *testing.T) { }, } - result, err = geMatcher(condition, user, nil) + result, err = geMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.True(t, result) @@ -61,7 +62,7 @@ func TestGeMatcherInt(t *testing.T) { }, } - result, err = geMatcher(condition, user, nil) + result, err = geMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.True(t, result) @@ -72,7 +73,7 @@ func TestGeMatcherInt(t *testing.T) { }, } - result, err = geMatcher(condition, user, nil) + result, err = geMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.False(t, result) @@ -83,7 +84,7 @@ func TestGeMatcherInt(t *testing.T) { }, } - _, err = geMatcher(condition, user, nil) + _, err = geMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.Error(t, err) // Test wrong int @@ -93,7 +94,7 @@ func TestGeMatcherInt(t *testing.T) { }, } - _, err = geMatcher(condition, user, nil) + _, err = geMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.Error(t, err) // Test bad condition @@ -108,7 +109,7 @@ func TestGeMatcherInt(t *testing.T) { }, } - _, err = geMatcher(condition, user, nil) + _, err = geMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.Error(t, err) } @@ -125,7 +126,7 @@ func TestGeMatcherFloat(t *testing.T) { "float_4_2": 5, }, } - result, err := geMatcher(condition, user, nil) + result, err := geMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.True(t, result) @@ -135,7 +136,7 @@ func TestGeMatcherFloat(t *testing.T) { "float_4_2": 4.29999, }, } - result, err = geMatcher(condition, user, nil) + result, err = geMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.True(t, result) @@ -146,7 +147,7 @@ func TestGeMatcherFloat(t *testing.T) { }, } - result, err = geMatcher(condition, user, nil) + result, err = geMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.True(t, result) @@ -157,6 +158,6 @@ func TestGeMatcherFloat(t *testing.T) { }, } - _, err = geMatcher(condition, user, nil) + _, err = geMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.Error(t, err) } diff --git a/pkg/decision/evaluator/matchers/gt.go b/pkg/decision/evaluator/matchers/gt.go index c54771f1e..3a9d35b07 100644 --- a/pkg/decision/evaluator/matchers/gt.go +++ b/pkg/decision/evaluator/matchers/gt.go @@ -20,21 +20,22 @@ package matchers import ( "fmt" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision/evaluator/matchers/utils" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) // GtMatcher matches against the "gt" match type -func GtMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer) (bool, error) { - res, err := compare(condition, user, logger) +func GtMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer, reasons decide.DecisionReasons) (bool, error) { + res, err := compare(condition, user, logger, reasons) if err != nil { return false, err } return res > 0, nil } -func compare(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer) (int, error) { +func compare(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer, _ decide.DecisionReasons) (int, error) { if !user.CheckAttributeExists(condition.Name) { logger.Debug(fmt.Sprintf(logging.NullUserAttribute.String(), condition.StringRepresentation, condition.Name)) return 0, fmt.Errorf(`no attribute named "%s"`, condition.Name) diff --git a/pkg/decision/evaluator/matchers/gt_test.go b/pkg/decision/evaluator/matchers/gt_test.go index 4db4d35ef..cf0750cc2 100644 --- a/pkg/decision/evaluator/matchers/gt_test.go +++ b/pkg/decision/evaluator/matchers/gt_test.go @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/suite" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) @@ -29,11 +30,13 @@ import ( type GtTestSuite struct { suite.Suite mockLogger *MockLogger + reasons decide.DecisionReasons matcher Matcher } func (s *GtTestSuite) SetupTest() { s.mockLogger = new(MockLogger) + s.reasons = decide.NewDecisionReasons(decide.OptimizelyDecideOptions{}) s.matcher, _ = Get(GtMatchType) } @@ -50,7 +53,7 @@ func (s *GtTestSuite) TestGtMatcherInt() { "int_42": 43, }, } - result, err := s.matcher(condition, user, s.mockLogger) + result, err := s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.True(result) @@ -61,7 +64,7 @@ func (s *GtTestSuite) TestGtMatcherInt() { }, } - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.True(result) @@ -72,7 +75,7 @@ func (s *GtTestSuite) TestGtMatcherInt() { }, } - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.False(result) @@ -83,7 +86,7 @@ func (s *GtTestSuite) TestGtMatcherInt() { }, } - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.False(result) @@ -94,7 +97,7 @@ func (s *GtTestSuite) TestGtMatcherInt() { }, } s.mockLogger.On("Debug", fmt.Sprintf(logging.NullUserAttribute.String(), "", "int_42")) - _, err = s.matcher(condition, user, s.mockLogger) + _, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) // Test attribute of different type @@ -104,7 +107,7 @@ func (s *GtTestSuite) TestGtMatcherInt() { }, } s.mockLogger.On("Warning", fmt.Sprintf(logging.InvalidAttributeValueType.String(), "", true, "int_42")) - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) s.False(result) s.mockLogger.AssertExpectations(s.T()) @@ -123,7 +126,7 @@ func (s *GtTestSuite) TestGtMatcherFloat() { "float_4_2": 5, }, } - result, err := s.matcher(condition, user, s.mockLogger) + result, err := s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.True(result) @@ -133,7 +136,7 @@ func (s *GtTestSuite) TestGtMatcherFloat() { "float_4_2": 4.29999, }, } - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.True(result) @@ -144,7 +147,7 @@ func (s *GtTestSuite) TestGtMatcherFloat() { }, } - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.False(result) @@ -156,7 +159,7 @@ func (s *GtTestSuite) TestGtMatcherFloat() { } s.mockLogger.On("Debug", fmt.Sprintf(logging.NullUserAttribute.String(), "", "float_4_2")) - _, err = s.matcher(condition, user, s.mockLogger) + _, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) // Test attribute of different type @@ -166,7 +169,7 @@ func (s *GtTestSuite) TestGtMatcherFloat() { }, } s.mockLogger.On("Warning", fmt.Sprintf(logging.InvalidAttributeValueType.String(), "", true, "float_4_2")) - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) s.False(result) s.mockLogger.AssertExpectations(s.T()) @@ -186,7 +189,7 @@ func (s *GtTestSuite) TestGtMatcherUnsupportedConditionValue() { }, } s.mockLogger.On("Warning", fmt.Sprintf(logging.UnsupportedConditionValue.String(), "")) - result, err := s.matcher(condition, user, s.mockLogger) + result, err := s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) s.False(result) s.mockLogger.AssertExpectations(s.T()) diff --git a/pkg/decision/evaluator/matchers/le.go b/pkg/decision/evaluator/matchers/le.go index 8b61f2531..d9757723d 100644 --- a/pkg/decision/evaluator/matchers/le.go +++ b/pkg/decision/evaluator/matchers/le.go @@ -19,6 +19,8 @@ package matchers import ( "fmt" + + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/logging" "github.com/optimizely/go-sdk/pkg/decision/evaluator/matchers/utils" @@ -26,7 +28,7 @@ import ( ) // LeMatcher matches against the "le" match type -func LeMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer) (bool, error) { +func LeMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer, reasons decide.DecisionReasons) (bool, error) { if floatValue, ok := utils.ToFloat(condition.Value); ok { attributeValue, err := user.GetFloatAttribute(condition.Name) diff --git a/pkg/decision/evaluator/matchers/le_test.go b/pkg/decision/evaluator/matchers/le_test.go index 8dcd5ce76..c22fc4269 100644 --- a/pkg/decision/evaluator/matchers/le_test.go +++ b/pkg/decision/evaluator/matchers/le_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" ) @@ -39,7 +40,7 @@ func TestLeMatcherInt(t *testing.T) { "int_42": 41, }, } - result, err := leMatcher(condition, user, nil) + result, err := leMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.True(t, result) @@ -49,7 +50,7 @@ func TestLeMatcherInt(t *testing.T) { "int_42": 42, }, } - result, err = leMatcher(condition, user, nil) + result, err = leMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.True(t, result) // Test match int to float @@ -59,7 +60,7 @@ func TestLeMatcherInt(t *testing.T) { }, } - result, err = leMatcher(condition, user, nil) + result, err = leMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.True(t, result) @@ -70,7 +71,7 @@ func TestLeMatcherInt(t *testing.T) { }, } - result, err = leMatcher(condition, user, nil) + result, err = leMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.False(t, result) @@ -81,7 +82,7 @@ func TestLeMatcherInt(t *testing.T) { }, } - result, err = leMatcher(condition, user, nil) + result, err = leMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.False(t, result) @@ -92,7 +93,7 @@ func TestLeMatcherInt(t *testing.T) { }, } - _, err = leMatcher(condition, user, nil) + _, err = leMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.Error(t, err) // Test bad condition @@ -107,7 +108,7 @@ func TestLeMatcherInt(t *testing.T) { }, } - _, err = LeMatcher(condition, user, nil) + _, err = LeMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.Error(t, err) } @@ -124,7 +125,7 @@ func TestLeMatcherFloat(t *testing.T) { "float_4_2": 4, }, } - result, err := leMatcher(condition, user, nil) + result, err := leMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.True(t, result) @@ -134,7 +135,7 @@ func TestLeMatcherFloat(t *testing.T) { "float_4_2": 4.19999, }, } - result, err = leMatcher(condition, user, nil) + result, err = leMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.True(t, result) @@ -144,7 +145,7 @@ func TestLeMatcherFloat(t *testing.T) { "float_4_2": 4.200000, }, } - result, err = leMatcher(condition, user, nil) + result, err = leMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.True(t, result) @@ -155,7 +156,7 @@ func TestLeMatcherFloat(t *testing.T) { }, } - result, err = leMatcher(condition, user, nil) + result, err = leMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err) assert.False(t, result) @@ -166,6 +167,6 @@ func TestLeMatcherFloat(t *testing.T) { }, } - _, err = leMatcher(condition, user, nil) + _, err = leMatcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.Error(t, err) } diff --git a/pkg/decision/evaluator/matchers/lt.go b/pkg/decision/evaluator/matchers/lt.go index 3d4445a47..fd04ae6f1 100644 --- a/pkg/decision/evaluator/matchers/lt.go +++ b/pkg/decision/evaluator/matchers/lt.go @@ -18,13 +18,14 @@ package matchers import ( + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) // LtMatcher matches against the "lt" match type -func LtMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer) (bool, error) { - res, err := compare(condition, user, logger) +func LtMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer, reasons decide.DecisionReasons) (bool, error) { + res, err := compare(condition, user, logger, reasons) if err != nil { return false, err } diff --git a/pkg/decision/evaluator/matchers/lt_test.go b/pkg/decision/evaluator/matchers/lt_test.go index e3a76d12a..0f2dd0ff6 100644 --- a/pkg/decision/evaluator/matchers/lt_test.go +++ b/pkg/decision/evaluator/matchers/lt_test.go @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/suite" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) @@ -29,11 +30,13 @@ import ( type LtTestSuite struct { suite.Suite mockLogger *MockLogger + reasons decide.DecisionReasons matcher Matcher } func (s *LtTestSuite) SetupTest() { s.mockLogger = new(MockLogger) + s.reasons = decide.NewDecisionReasons(decide.OptimizelyDecideOptions{}) s.matcher, _ = Get(LtMatchType) } @@ -51,7 +54,7 @@ func (s *LtTestSuite) TestLtMatcherInt() { }, } - result, err := s.matcher(condition, user, s.mockLogger) + result, err := s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.True(result) @@ -62,7 +65,7 @@ func (s *LtTestSuite) TestLtMatcherInt() { }, } - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.True(result) @@ -73,7 +76,7 @@ func (s *LtTestSuite) TestLtMatcherInt() { }, } - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.False(result) @@ -84,7 +87,7 @@ func (s *LtTestSuite) TestLtMatcherInt() { }, } - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.False(result) @@ -96,7 +99,7 @@ func (s *LtTestSuite) TestLtMatcherInt() { } s.mockLogger.On("Debug", fmt.Sprintf(logging.NullUserAttribute.String(), "", "int_42")) - _, err = s.matcher(condition, user, s.mockLogger) + _, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) // Test attribute of different type @@ -106,7 +109,7 @@ func (s *LtTestSuite) TestLtMatcherInt() { }, } s.mockLogger.On("Warning", fmt.Sprintf(logging.InvalidAttributeValueType.String(), "", true, "int_42")) - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) s.False(result) s.mockLogger.AssertExpectations(s.T()) @@ -126,7 +129,7 @@ func (s *LtTestSuite) TestLtMatcherFloat() { }, } - result, err := s.matcher(condition, user, s.mockLogger) + result, err := s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.True(result) @@ -137,7 +140,7 @@ func (s *LtTestSuite) TestLtMatcherFloat() { }, } - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.True(result) @@ -148,7 +151,7 @@ func (s *LtTestSuite) TestLtMatcherFloat() { }, } - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.False(result) @@ -160,7 +163,7 @@ func (s *LtTestSuite) TestLtMatcherFloat() { } s.mockLogger.On("Debug", fmt.Sprintf(logging.NullUserAttribute.String(), "", "float_4_2")) - _, err = s.matcher(condition, user, s.mockLogger) + _, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) // Test attribute of different type @@ -170,7 +173,7 @@ func (s *LtTestSuite) TestLtMatcherFloat() { }, } s.mockLogger.On("Warning", fmt.Sprintf(logging.InvalidAttributeValueType.String(), "", true, "float_4_2")) - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) s.False(result) s.mockLogger.AssertExpectations(s.T()) @@ -190,7 +193,7 @@ func (s *LtTestSuite) TestLtMatcherUnsupportedConditionValue() { }, } s.mockLogger.On("Warning", fmt.Sprintf(logging.UnsupportedConditionValue.String(), "")) - result, err := s.matcher(condition, user, s.mockLogger) + result, err := s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) s.False(result) s.mockLogger.AssertExpectations(s.T()) diff --git a/pkg/decision/evaluator/matchers/registry.go b/pkg/decision/evaluator/matchers/registry.go index db0014f58..579a0beea 100644 --- a/pkg/decision/evaluator/matchers/registry.go +++ b/pkg/decision/evaluator/matchers/registry.go @@ -20,12 +20,13 @@ package matchers import ( "sync" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) // Matcher type is used to evaluate audience conditional primitives -type Matcher func(entities.Condition, entities.UserContext, logging.OptimizelyLogProducer) (bool, error) +type Matcher func(entities.Condition, entities.UserContext, logging.OptimizelyLogProducer, decide.DecisionReasons) (bool, error) const ( // ExactMatchType name for the "exact" matcher diff --git a/pkg/decision/evaluator/matchers/registry_test.go b/pkg/decision/evaluator/matchers/registry_test.go index 6a109fa73..1887b2f4b 100644 --- a/pkg/decision/evaluator/matchers/registry_test.go +++ b/pkg/decision/evaluator/matchers/registry_test.go @@ -22,17 +22,18 @@ import ( "github.com/stretchr/testify/assert" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) func TestRegister(t *testing.T) { - expected := func(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer) (bool, error) { + expected := func(entities.Condition, entities.UserContext, logging.OptimizelyLogProducer, decide.DecisionReasons) (bool, error) { return false, nil } Register("test", expected) actual := assertMatcher(t, "test") - matches, err := actual(entities.Condition{}, entities.UserContext{}, nil) + matches, err := actual(entities.Condition{}, entities.UserContext{}, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.False(t, matches) assert.NoError(t, err) } diff --git a/pkg/decision/evaluator/matchers/semver.go b/pkg/decision/evaluator/matchers/semver.go index 57a1f375f..ca1855f02 100644 --- a/pkg/decision/evaluator/matchers/semver.go +++ b/pkg/decision/evaluator/matchers/semver.go @@ -23,6 +23,7 @@ import ( "strconv" "strings" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/logging" "github.com/optimizely/go-sdk/pkg/decision/reasons" @@ -215,7 +216,7 @@ func SemverEvaluator(cond entities.Condition, user entities.UserContext) (int, e } // SemverEqMatcher returns true if the user's semver attribute is equal to the semver condition value -func SemverEqMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer) (bool, error) { +func SemverEqMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer, _ decide.DecisionReasons) (bool, error) { comparison, err := SemverEvaluator(condition, user) if err != nil { return false, err @@ -224,7 +225,7 @@ func SemverEqMatcher(condition entities.Condition, user entities.UserContext, lo } // SemverGeMatcher returns true if the user's semver attribute is greater or equal to the semver condition value -func SemverGeMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer) (bool, error) { +func SemverGeMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer, _ decide.DecisionReasons) (bool, error) { comparison, err := SemverEvaluator(condition, user) if err != nil { return false, err @@ -233,7 +234,7 @@ func SemverGeMatcher(condition entities.Condition, user entities.UserContext, lo } // SemverGtMatcher returns true if the user's semver attribute is greater than the semver condition value -func SemverGtMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer) (bool, error) { +func SemverGtMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer, _ decide.DecisionReasons) (bool, error) { comparison, err := SemverEvaluator(condition, user) if err != nil { return false, err @@ -242,7 +243,7 @@ func SemverGtMatcher(condition entities.Condition, user entities.UserContext, lo } // SemverLeMatcher returns true if the user's semver attribute is less than or equal to the semver condition value -func SemverLeMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer) (bool, error) { +func SemverLeMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer, _ decide.DecisionReasons) (bool, error) { comparison, err := SemverEvaluator(condition, user) if err != nil { return false, err @@ -251,7 +252,7 @@ func SemverLeMatcher(condition entities.Condition, user entities.UserContext, lo } // SemverLtMatcher returns true if the user's semver attribute is less than the semver condition value -func SemverLtMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer) (bool, error) { +func SemverLtMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer, _ decide.DecisionReasons) (bool, error) { comparison, err := SemverEvaluator(condition, user) if err != nil { return false, err diff --git a/pkg/decision/evaluator/matchers/semver_test.go b/pkg/decision/evaluator/matchers/semver_test.go index 91a6174d5..a549d05f8 100644 --- a/pkg/decision/evaluator/matchers/semver_test.go +++ b/pkg/decision/evaluator/matchers/semver_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" ) @@ -69,7 +70,7 @@ func TestValidAttributes(t *testing.T) { matcher, ok := Get(scenario.matchType) assert.True(t, ok, messageAndArgs...) - actual, err := matcher(condition, user, nil) + actual, err := matcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err, messageAndArgs...) assert.Equal(t, scenario.expected, actual, messageAndArgs...) @@ -120,7 +121,7 @@ func TestValidAttributesReleaseToBeta(t *testing.T) { matcher, ok := Get(scenario.matchType) assert.True(t, ok, messageAndArgs...) - actual, err := matcher(condition, user, nil) + actual, err := matcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err, messageAndArgs...) assert.Equal(t, scenario.expected, actual, messageAndArgs...) @@ -171,7 +172,7 @@ func TestValidAttributesBetaToRelease(t *testing.T) { matcher, ok := Get(scenario.matchType) assert.True(t, ok, messageAndArgs...) - actual, err := matcher(condition, user, nil) + actual, err := matcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err, messageAndArgs...) assert.Equal(t, scenario.expected, actual, messageAndArgs...) @@ -219,7 +220,7 @@ func TestTargetBetaAndBetaComplex(t *testing.T) { matcher, ok := Get(scenario.matchType) assert.True(t, ok, messageAndArgs...) - actual, err := matcher(condition, user, nil) + actual, err := matcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err, messageAndArgs...) assert.Equal(t, scenario.expected, actual, messageAndArgs...) @@ -277,7 +278,7 @@ func TestDifferentAttributeAgainstBuildAndPrerelease(t *testing.T) { matcher, ok := Get(scenario.matchType) assert.True(t, ok, messageAndArgs...) - actual, err := matcher(condition, user, nil) + actual, err := matcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.NoError(t, err, messageAndArgs...) assert.Equal(t, scenario.expected, actual, messageAndArgs...) @@ -329,7 +330,7 @@ func TestInvalidAttributes(t *testing.T) { "version": attribute, }, } - _, err := matcher(condition, user, nil) + _, err := matcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.Error(t, err, "matchType: %s, value: %v", matchType, attribute) } } @@ -357,7 +358,7 @@ func TestInvalidConditions(t *testing.T) { "version": "12.2.3", }, } - _, err := matcher(condition, user, nil) + _, err := matcher(condition, user, nil, decide.NewDecisionReasons(decide.OptimizelyDecideOptions{})) assert.Error(t, err, "matchType: semver_eq, value: 12.2.3") } diff --git a/pkg/decision/evaluator/matchers/substring.go b/pkg/decision/evaluator/matchers/substring.go index ec99b5fcc..2f129807d 100644 --- a/pkg/decision/evaluator/matchers/substring.go +++ b/pkg/decision/evaluator/matchers/substring.go @@ -21,12 +21,13 @@ import ( "fmt" "strings" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) // SubstringMatcher matches against the "substring" match type -func SubstringMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer) (bool, error) { +func SubstringMatcher(condition entities.Condition, user entities.UserContext, logger logging.OptimizelyLogProducer, reasons decide.DecisionReasons) (bool, error) { if !user.CheckAttributeExists(condition.Name) { logger.Debug(fmt.Sprintf(logging.NullUserAttribute.String(), condition.StringRepresentation, condition.Name)) diff --git a/pkg/decision/evaluator/matchers/substring_test.go b/pkg/decision/evaluator/matchers/substring_test.go index d0afb7455..4ac7ca6b6 100644 --- a/pkg/decision/evaluator/matchers/substring_test.go +++ b/pkg/decision/evaluator/matchers/substring_test.go @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/suite" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) @@ -29,11 +30,13 @@ import ( type SubstringTestSuite struct { suite.Suite mockLogger *MockLogger + reasons decide.DecisionReasons matcher Matcher } func (s *SubstringTestSuite) SetupTest() { s.mockLogger = new(MockLogger) + s.reasons = decide.NewDecisionReasons(decide.OptimizelyDecideOptions{}) s.matcher, _ = Get(SubstringMatchType) } @@ -51,7 +54,7 @@ func (s *SubstringTestSuite) TestSubstringMatcher() { }, } - result, err := s.matcher(condition, user, s.mockLogger) + result, err := s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.True(result) @@ -62,7 +65,7 @@ func (s *SubstringTestSuite) TestSubstringMatcher() { }, } - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.NoError(err) s.False(result) @@ -74,7 +77,7 @@ func (s *SubstringTestSuite) TestSubstringMatcher() { } s.mockLogger.On("Debug", fmt.Sprintf(logging.NullUserAttribute.String(), "", "string_foo")) - _, err = s.matcher(condition, user, s.mockLogger) + _, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) // Test attribute of different type @@ -84,7 +87,7 @@ func (s *SubstringTestSuite) TestSubstringMatcher() { }, } s.mockLogger.On("Warning", fmt.Sprintf(logging.InvalidAttributeValueType.String(), "", true, "string_foo")) - result, err = s.matcher(condition, user, s.mockLogger) + result, err = s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) s.False(result) s.mockLogger.AssertExpectations(s.T()) @@ -104,7 +107,7 @@ func (s *SubstringTestSuite) TestSubstringMatcherUnsupportedConditionValue() { }, } s.mockLogger.On("Warning", fmt.Sprintf(logging.UnsupportedConditionValue.String(), "")) - result, err := s.matcher(condition, user, s.mockLogger) + result, err := s.matcher(condition, user, s.mockLogger, s.reasons) s.Error(err) s.False(result) s.mockLogger.AssertExpectations(s.T()) diff --git a/pkg/decision/experiment_bucketer_service.go b/pkg/decision/experiment_bucketer_service.go index a06d71514..0c210d53c 100644 --- a/pkg/decision/experiment_bucketer_service.go +++ b/pkg/decision/experiment_bucketer_service.go @@ -20,9 +20,10 @@ package decision import ( "fmt" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision/bucketer" "github.com/optimizely/go-sdk/pkg/decision/evaluator" - "github.com/optimizely/go-sdk/pkg/decision/reasons" + pkgReasons "github.com/optimizely/go-sdk/pkg/decision/reasons" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) @@ -45,7 +46,7 @@ func NewExperimentBucketerService(logger logging.OptimizelyLogProducer) *Experim } // GetDecision returns the decision with the variation the user is bucketed into -func (s ExperimentBucketerService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext) (ExperimentDecision, error) { +func (s ExperimentBucketerService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (ExperimentDecision, error) { experimentDecision := ExperimentDecision{} experiment := decisionContext.Experiment @@ -53,11 +54,13 @@ func (s ExperimentBucketerService) GetDecision(decisionContext ExperimentDecisio if experiment.AudienceConditionTree != nil { condTreeParams := entities.NewTreeParameters(&userContext, decisionContext.ProjectConfig.GetAudienceMap()) s.logger.Debug(fmt.Sprintf(logging.EvaluatingAudiencesForExperiment.String(), experiment.Key)) - evalResult, _ := s.audienceTreeEvaluator.Evaluate(experiment.AudienceConditionTree, condTreeParams) + evalResult, _ := s.audienceTreeEvaluator.Evaluate(experiment.AudienceConditionTree, condTreeParams, reasons) s.logger.Debug(fmt.Sprintf(logging.ExperimentAudiencesEvaluatedTo.String(), experiment.Key, evalResult)) if !evalResult { - s.logger.Debug(fmt.Sprintf(logging.UserNotInExperiment.String(), userContext.ID, experiment.Key)) - experimentDecision.Reason = reasons.FailedAudienceTargeting + logMessage := fmt.Sprintf(logging.UserNotInExperiment.String(), userContext.ID, experiment.Key) + s.logger.Debug(logMessage) + reasons.AddInfo(logMessage) + experimentDecision.Reason = pkgReasons.FailedAudienceTargeting return experimentDecision, nil } } else { diff --git a/pkg/decision/experiment_bucketer_service_test.go b/pkg/decision/experiment_bucketer_service_test.go index 88383ac80..6fabb72fd 100644 --- a/pkg/decision/experiment_bucketer_service_test.go +++ b/pkg/decision/experiment_bucketer_service_test.go @@ -20,6 +20,7 @@ import ( "fmt" "testing" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision/reasons" "github.com/optimizely/go-sdk/pkg/logging" @@ -62,12 +63,16 @@ type ExperimentBucketerTestSuite struct { mockBucketer *MockBucketer mockLogger *MockLogger mockConfig *mockProjectConfig + options decide.OptimizelyDecideOptions + reasons decide.DecisionReasons } func (s *ExperimentBucketerTestSuite) SetupTest() { s.mockBucketer = new(MockBucketer) s.mockLogger = new(MockLogger) s.mockConfig = new(mockProjectConfig) + s.options = decide.OptimizelyDecideOptions{} + s.reasons = decide.NewDecisionReasons(s.options) } func (s *ExperimentBucketerTestSuite) TestGetDecisionNoTargeting() { @@ -92,7 +97,7 @@ func (s *ExperimentBucketerTestSuite) TestGetDecisionNoTargeting() { bucketer: s.mockBucketer, logger: s.mockLogger, } - decision, err := experimentBucketerService.GetDecision(testDecisionContext, testUserContext) + decision, err := experimentBucketerService.GetDecision(testDecisionContext, testUserContext, s.options, s.reasons) s.Equal(expectedDecision, decision) s.NoError(err) s.mockLogger.AssertExpectations(s.T()) @@ -112,7 +117,7 @@ func (s *ExperimentBucketerTestSuite) TestGetDecisionWithTargetingPasses() { s.mockBucketer.On("Bucket", testUserContext.ID, testTargetedExp1116, entities.Group{}).Return(&testTargetedExp1116Var2228, reasons.BucketedIntoVariation, nil) mockAudienceTreeEvaluator := new(MockAudienceTreeEvaluator) - mockAudienceTreeEvaluator.On("Evaluate", mock.Anything, mock.Anything).Return(true, true) + mockAudienceTreeEvaluator.On("Evaluate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(true, true) experimentBucketerService := ExperimentBucketerService{ audienceTreeEvaluator: mockAudienceTreeEvaluator, logger: s.mockLogger, @@ -126,7 +131,7 @@ func (s *ExperimentBucketerTestSuite) TestGetDecisionWithTargetingPasses() { Experiment: &testTargetedExp1116, ProjectConfig: s.mockConfig, } - decision, err := experimentBucketerService.GetDecision(testDecisionContext, testUserContext) + decision, err := experimentBucketerService.GetDecision(testDecisionContext, testUserContext, s.options, s.reasons) s.Equal(expectedDecision, decision) s.NoError(err) s.mockLogger.AssertExpectations(s.T()) @@ -143,7 +148,7 @@ func (s *ExperimentBucketerTestSuite) TestGetDecisionWithTargetingFails() { }, } mockAudienceTreeEvaluator := new(MockAudienceTreeEvaluator) - mockAudienceTreeEvaluator.On("Evaluate", mock.Anything, mock.Anything).Return(false, true) + mockAudienceTreeEvaluator.On("Evaluate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(false, true) experimentBucketerService := ExperimentBucketerService{ audienceTreeEvaluator: mockAudienceTreeEvaluator, logger: s.mockLogger, @@ -158,7 +163,7 @@ func (s *ExperimentBucketerTestSuite) TestGetDecisionWithTargetingFails() { Experiment: &testTargetedExp1116, ProjectConfig: s.mockConfig, } - decision, err := experimentBucketerService.GetDecision(testDecisionContext, testUserContext) + decision, err := experimentBucketerService.GetDecision(testDecisionContext, testUserContext, s.options, s.reasons) s.Equal(expectedDecision, decision) s.NoError(err) s.mockBucketer.AssertNotCalled(s.T(), "Bucket") diff --git a/pkg/decision/experiment_override_service.go b/pkg/decision/experiment_override_service.go index b55c58bc4..fd7264166 100644 --- a/pkg/decision/experiment_override_service.go +++ b/pkg/decision/experiment_override_service.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -22,7 +22,8 @@ import ( "fmt" "sync" - "github.com/optimizely/go-sdk/pkg/decision/reasons" + "github.com/optimizely/go-sdk/pkg/decide" + pkgReasons "github.com/optimizely/go-sdk/pkg/decision/reasons" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) @@ -84,13 +85,13 @@ type ExperimentOverrideService struct { // NewExperimentOverrideService returns a pointer to an initialized ExperimentOverrideService func NewExperimentOverrideService(overrides ExperimentOverrideStore, logger logging.OptimizelyLogProducer) *ExperimentOverrideService { return &ExperimentOverrideService{ - logger: logger, + logger: logger, 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) { +func (s ExperimentOverrideService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (experimentDecision ExperimentDecision, err error) { decision := ExperimentDecision{} if decisionContext.Experiment == nil { @@ -99,19 +100,19 @@ func (s ExperimentOverrideService) GetDecision(decisionContext ExperimentDecisio variationKey, ok := s.Overrides.GetVariation(ExperimentOverrideKey{ExperimentKey: decisionContext.Experiment.Key, UserID: userContext.ID}) if !ok { - decision.Reason = reasons.NoOverrideVariationAssignment + decision.Reason = pkgReasons.NoOverrideVariationAssignment return decision, nil } if variationID, ok := decisionContext.Experiment.VariationKeyToIDMap[variationKey]; ok { if variation, ok := decisionContext.Experiment.Variations[variationID]; ok { decision.Variation = &variation - decision.Reason = reasons.OverrideVariationAssignmentFound + decision.Reason = pkgReasons.OverrideVariationAssignmentFound s.logger.Debug(fmt.Sprintf("Override variation %v found for user %v", variationKey, userContext.ID)) return decision, nil } } - decision.Reason = reasons.InvalidOverrideVariationAssignment + decision.Reason = pkgReasons.InvalidOverrideVariationAssignment return decision, nil } diff --git a/pkg/decision/experiment_override_service_test.go b/pkg/decision/experiment_override_service_test.go index f36fc2450..aa3226a2f 100644 --- a/pkg/decision/experiment_override_service_test.go +++ b/pkg/decision/experiment_override_service_test.go @@ -21,6 +21,7 @@ import ( "sync" "testing" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision/reasons" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" @@ -33,12 +34,16 @@ type ExperimentOverrideServiceTestSuite struct { mockConfig *mockProjectConfig overrides *MapExperimentOverridesStore overrideService *ExperimentOverrideService + options decide.OptimizelyDecideOptions + reasons decide.DecisionReasons } func (s *ExperimentOverrideServiceTestSuite) SetupTest() { s.mockConfig = new(mockProjectConfig) s.overrides = NewMapExperimentOverridesStore() s.overrideService = NewExperimentOverrideService(s.overrides, logging.GetLogger("", "")) + s.options = decide.OptimizelyDecideOptions{} + s.reasons = decide.NewDecisionReasons(s.options) } func (s *ExperimentOverrideServiceTestSuite) TestOverridesIncludeVariation() { @@ -50,7 +55,7 @@ func (s *ExperimentOverrideServiceTestSuite) TestOverridesIncludeVariation() { ID: "test_user_1", } s.overrides.SetVariation(ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_1"}, testExp1111Var2222.Key) - decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) + decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext, s.options, s.reasons) s.NoError(err) s.NotNil(decision.Variation) s.Exactly(testExp1111Var2222.Key, decision.Variation.Key) @@ -64,7 +69,7 @@ func (s *ExperimentOverrideServiceTestSuite) TestNilDecisionContextExperiment() testUserContext := entities.UserContext{ ID: "test_user_1", } - decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) + decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext, s.options, s.reasons) s.Error(err) s.Nil(decision.Variation) } @@ -79,7 +84,7 @@ func (s *ExperimentOverrideServiceTestSuite) TestNoOverrideForExperiment() { } // The decision context refers to testExp1111, but this override is for another experiment s.overrides.SetVariation(ExperimentOverrideKey{ExperimentKey: testExp1113.Key, UserID: "test_user_1"}, testExp1113Var2224.Key) - decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) + decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext, s.options, s.reasons) s.NoError(err) s.Nil(decision.Variation) s.Exactly(reasons.NoOverrideVariationAssignment, decision.Reason) @@ -95,7 +100,7 @@ func (s *ExperimentOverrideServiceTestSuite) TestNoOverrideForUser() { } // The user context refers to "test_user_1", but this override is for another user s.overrides.SetVariation(ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_2"}, testExp1111Var2222.Key) - decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) + decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext, s.options, s.reasons) s.NoError(err) s.Nil(decision.Variation) s.Exactly(reasons.NoOverrideVariationAssignment, decision.Reason) @@ -111,7 +116,7 @@ func (s *ExperimentOverrideServiceTestSuite) TestNoOverrideForUserOrExperiment() } // This override is for both a different user and a different experiment than the ones in the contexts above s.overrides.SetVariation(ExperimentOverrideKey{ExperimentKey: testExp1113.Key, UserID: "test_user_3"}, testExp1111Var2222.Key) - decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) + decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext, s.options, s.reasons) s.NoError(err) s.Nil(decision.Variation) s.Exactly(reasons.NoOverrideVariationAssignment, decision.Reason) @@ -127,7 +132,7 @@ func (s *ExperimentOverrideServiceTestSuite) TestInvalidVariationInOverride() { } // This override variation key does not exist in the experiment s.overrides.SetVariation(ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_1"}, "invalid_variation_key") - decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) + decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext, s.options, s.reasons) s.NoError(err) s.Nil(decision.Variation) s.Exactly(reasons.InvalidOverrideVariationAssignment, decision.Reason) @@ -146,7 +151,7 @@ func (s *ExperimentOverrideServiceTestSuite) TestMapExperimentOverridesStoreConc s.overrides.SetVariation(ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_1"}, testExp1111Var2222.Key) user1Decision, _ := s.overrideService.GetDecision(testDecisionContext, entities.UserContext{ ID: "test_user_1", - }) + }, s.options, s.reasons) s.NotNil(user1Decision.Variation) s.Exactly(testExp1111Var2222.Key, user1Decision.Variation.Key) wg.Done() @@ -156,7 +161,7 @@ func (s *ExperimentOverrideServiceTestSuite) TestMapExperimentOverridesStoreConc s.overrides.SetVariation(ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_2"}, testExp1111Var2222.Key) user2Decision, _ := s.overrideService.GetDecision(testDecisionContext, entities.UserContext{ ID: "test_user_2", - }) + }, s.options, s.reasons) s.NotNil(user2Decision.Variation) s.Exactly(testExp1111Var2222.Key, user2Decision.Variation.Key) wg.Done() @@ -166,7 +171,7 @@ func (s *ExperimentOverrideServiceTestSuite) TestMapExperimentOverridesStoreConc s.overrides.SetVariation(ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_3"}, testExp1111Var2222.Key) user3Decision, _ := s.overrideService.GetDecision(testDecisionContext, entities.UserContext{ ID: "test_user_3", - }) + }, s.options, s.reasons) s.NotNil(user3Decision.Variation) s.Exactly(testExp1111Var2222.Key, user3Decision.Variation.Key) wg.Done() @@ -174,13 +179,13 @@ func (s *ExperimentOverrideServiceTestSuite) TestMapExperimentOverridesStoreConc wg.Wait() user1Decision, _ := s.overrideService.GetDecision(testDecisionContext, entities.UserContext{ ID: "test_user_1", - }) + }, s.options, s.reasons) user2Decision, _ := s.overrideService.GetDecision(testDecisionContext, entities.UserContext{ ID: "test_user_2", - }) + }, s.options, s.reasons) user3Decision, _ := s.overrideService.GetDecision(testDecisionContext, entities.UserContext{ ID: "test_user_3", - }) + }, s.options, s.reasons) s.NotNil(user1Decision.Variation) s.Exactly(testExp1111Var2222.Key, user1Decision.Variation.Key) s.NotNil(user2Decision.Variation) @@ -200,7 +205,7 @@ func (s *ExperimentOverrideServiceTestSuite) TestRemovePreviouslySetVariation() overrideKey := ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_1"} s.overrides.SetVariation(overrideKey, testExp1111Var2222.Key) s.overrides.RemoveVariation(overrideKey) - decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) + decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext, s.options, s.reasons) s.NoError(err) s.Nil(decision.Variation) } diff --git a/pkg/decision/experiment_whitelist_service.go b/pkg/decision/experiment_whitelist_service.go index 2ca62820b..17ea99ec3 100644 --- a/pkg/decision/experiment_whitelist_service.go +++ b/pkg/decision/experiment_whitelist_service.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -20,7 +20,8 @@ package decision import ( "errors" - "github.com/optimizely/go-sdk/pkg/decision/reasons" + "github.com/optimizely/go-sdk/pkg/decide" + pkgReasons "github.com/optimizely/go-sdk/pkg/decision/reasons" "github.com/optimizely/go-sdk/pkg/entities" ) @@ -34,7 +35,7 @@ func NewExperimentWhitelistService() *ExperimentWhitelistService { } // GetDecision returns a decision with a variation when a variation assignment is found in the experiment whitelist for the given user and experiment -func (s ExperimentWhitelistService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext) (ExperimentDecision, error) { +func (s ExperimentWhitelistService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (ExperimentDecision, error) { decision := ExperimentDecision{} if decisionContext.Experiment == nil { @@ -43,18 +44,18 @@ func (s ExperimentWhitelistService) GetDecision(decisionContext ExperimentDecisi variationKey, ok := decisionContext.Experiment.Whitelist[userContext.ID] if !ok { - decision.Reason = reasons.NoWhitelistVariationAssignment + decision.Reason = pkgReasons.NoWhitelistVariationAssignment return decision, nil } if id, ok := decisionContext.Experiment.VariationKeyToIDMap[variationKey]; ok { if variation, ok := decisionContext.Experiment.Variations[id]; ok { - decision.Reason = reasons.WhitelistVariationAssignmentFound + decision.Reason = pkgReasons.WhitelistVariationAssignmentFound decision.Variation = &variation return decision, nil } } - decision.Reason = reasons.InvalidWhitelistVariationAssignment + decision.Reason = pkgReasons.InvalidWhitelistVariationAssignment return decision, nil } diff --git a/pkg/decision/experiment_whitelist_service_test.go b/pkg/decision/experiment_whitelist_service_test.go index 92d317f7d..523bcbec2 100644 --- a/pkg/decision/experiment_whitelist_service_test.go +++ b/pkg/decision/experiment_whitelist_service_test.go @@ -20,6 +20,7 @@ package decision import ( "testing" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision/reasons" "github.com/optimizely/go-sdk/pkg/entities" "github.com/stretchr/testify/suite" @@ -29,11 +30,15 @@ type ExperimentWhitelistServiceTestSuite struct { suite.Suite mockConfig *mockProjectConfig whitelistService *ExperimentWhitelistService + options decide.OptimizelyDecideOptions + reasons decide.DecisionReasons } func (s *ExperimentWhitelistServiceTestSuite) SetupTest() { s.mockConfig = new(mockProjectConfig) s.whitelistService = NewExperimentWhitelistService() + s.options = decide.OptimizelyDecideOptions{} + s.reasons = decide.NewDecisionReasons(s.options) } func (s *ExperimentWhitelistServiceTestSuite) TestWhitelistIncludesDecision() { @@ -46,7 +51,7 @@ func (s *ExperimentWhitelistServiceTestSuite) TestWhitelistIncludesDecision() { ID: "test_user_1", } - decision, err := s.whitelistService.GetDecision(testDecisionContext, testUserContext) + decision, err := s.whitelistService.GetDecision(testDecisionContext, testUserContext, s.options, s.reasons) s.NoError(err) s.NotNil(decision.Variation) @@ -63,7 +68,7 @@ func (s *ExperimentWhitelistServiceTestSuite) TestNoUserEntryInWhitelist() { ID: "test_user_3", } - decision, err := s.whitelistService.GetDecision(testDecisionContext, testUserContext) + decision, err := s.whitelistService.GetDecision(testDecisionContext, testUserContext, s.options, s.reasons) s.NoError(err) s.Nil(decision.Variation) @@ -81,7 +86,7 @@ func (s *ExperimentWhitelistServiceTestSuite) TestEmptyWhitelist() { ID: "test_user_1", } - decision, err := s.whitelistService.GetDecision(testDecisionContext, testUserContext) + decision, err := s.whitelistService.GetDecision(testDecisionContext, testUserContext, s.options, s.reasons) s.NoError(err) s.Nil(decision.Variation) @@ -99,7 +104,7 @@ func (s *ExperimentWhitelistServiceTestSuite) TestInvalidVariationInUserEntry() ID: "test_user_2", } - decision, err := s.whitelistService.GetDecision(testDecisionContext, testUserContext) + decision, err := s.whitelistService.GetDecision(testDecisionContext, testUserContext, s.options, s.reasons) s.NoError(err) s.Nil(decision.Variation) @@ -116,7 +121,7 @@ func (s *ExperimentWhitelistServiceTestSuite) TestNoExperimentInDecisionContext( ID: "test_user_1", } - decision, err := s.whitelistService.GetDecision(testDecisionContext, testUserContext) + decision, err := s.whitelistService.GetDecision(testDecisionContext, testUserContext, s.options, s.reasons) s.Error(err) s.Nil(decision.Variation) diff --git a/pkg/decision/feature_experiment_service.go b/pkg/decision/feature_experiment_service.go index 8890c10e7..a0238079f 100644 --- a/pkg/decision/feature_experiment_service.go +++ b/pkg/decision/feature_experiment_service.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -20,6 +20,7 @@ package decision import ( "fmt" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) @@ -33,13 +34,13 @@ type FeatureExperimentService struct { // NewFeatureExperimentService returns a new instance of the FeatureExperimentService func NewFeatureExperimentService(logger logging.OptimizelyLogProducer, compositeExperimentService ExperimentService) *FeatureExperimentService { return &FeatureExperimentService{ - logger: logger, + logger: logger, compositeExperimentService: compositeExperimentService, } } // GetDecision returns a decision for the given feature test and user context -func (f FeatureExperimentService) GetDecision(decisionContext FeatureDecisionContext, userContext entities.UserContext) (FeatureDecision, error) { +func (f FeatureExperimentService) GetDecision(decisionContext FeatureDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (FeatureDecision, error) { feature := decisionContext.Feature // @TODO this can be improved by getting group ID first and determining experiment and then bucketing in experiment for _, featureExperiment := range feature.FeatureExperiments { @@ -49,7 +50,7 @@ func (f FeatureExperimentService) GetDecision(decisionContext FeatureDecisionCon ProjectConfig: decisionContext.ProjectConfig, } - experimentDecision, err := f.compositeExperimentService.GetDecision(experimentDecisionContext, userContext) + experimentDecision, err := f.compositeExperimentService.GetDecision(experimentDecisionContext, userContext, options, reasons) f.logger.Debug(fmt.Sprintf( `Decision made for feature test with key "%s" for user "%s" with the following reason: "%s".`, feature.Key, diff --git a/pkg/decision/feature_experiment_service_test.go b/pkg/decision/feature_experiment_service_test.go index cb68ad885..16e036fd5 100644 --- a/pkg/decision/feature_experiment_service_test.go +++ b/pkg/decision/feature_experiment_service_test.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -19,6 +19,7 @@ package decision import ( "testing" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" @@ -30,6 +31,8 @@ type FeatureExperimentServiceTestSuite struct { mockConfig *mockProjectConfig testFeatureDecisionContext FeatureDecisionContext mockExperimentService *MockExperimentDecisionService + options decide.OptimizelyDecideOptions + reasons decide.DecisionReasons } func (s *FeatureExperimentServiceTestSuite) SetupTest() { @@ -40,6 +43,8 @@ func (s *FeatureExperimentServiceTestSuite) SetupTest() { Variable: testVariable, } s.mockExperimentService = new(MockExperimentDecisionService) + s.options = decide.OptimizelyDecideOptions{} + s.reasons = decide.NewDecisionReasons(s.options) } func (s *FeatureExperimentServiceTestSuite) TestGetDecision() { @@ -55,11 +60,11 @@ func (s *FeatureExperimentServiceTestSuite) TestGetDecision() { Experiment: &testExp1113, ProjectConfig: s.mockConfig, } - s.mockExperimentService.On("GetDecision", testExperimentDecisionContext, testUserContext).Return(returnExperimentDecision, nil) + s.mockExperimentService.On("GetDecision", testExperimentDecisionContext, testUserContext, s.options, s.reasons).Return(returnExperimentDecision, nil) featureExperimentService := &FeatureExperimentService{ compositeExperimentService: s.mockExperimentService, - logger:logging.GetLogger("sdkKey", "FeatureExperimentService"), + logger: logging.GetLogger("sdkKey", "FeatureExperimentService"), } expectedFeatureDecision := FeatureDecision{ @@ -67,7 +72,7 @@ func (s *FeatureExperimentServiceTestSuite) TestGetDecision() { Variation: &expectedVariation, Source: FeatureTest, } - decision, err := featureExperimentService.GetDecision(s.testFeatureDecisionContext, testUserContext) + decision, err := featureExperimentService.GetDecision(s.testFeatureDecisionContext, testUserContext, s.options, s.reasons) s.Equal(expectedFeatureDecision, decision) s.NoError(err) s.mockExperimentService.AssertExpectations(s.T()) @@ -84,7 +89,7 @@ func (s *FeatureExperimentServiceTestSuite) TestGetDecisionMutex() { Experiment: &testExp1113, ProjectConfig: s.mockConfig, } - s.mockExperimentService.On("GetDecision", testExperimentDecisionContext1, testUserContext).Return(nilDecision, nil) + s.mockExperimentService.On("GetDecision", testExperimentDecisionContext1, testUserContext, s.options, s.reasons).Return(nilDecision, nil) // second experiment returns a valid decision to simulate user being bucketed into this experiment in the group expectedVariation := testExp1114.Variations["2225"] @@ -95,7 +100,7 @@ func (s *FeatureExperimentServiceTestSuite) TestGetDecisionMutex() { Experiment: &testExp1114, ProjectConfig: s.mockConfig, } - s.mockExperimentService.On("GetDecision", testExperimentDecisionContext2, testUserContext).Return(returnExperimentDecision, nil) + s.mockExperimentService.On("GetDecision", testExperimentDecisionContext2, testUserContext, s.options, s.reasons).Return(returnExperimentDecision, nil) expectedFeatureDecision := FeatureDecision{ Experiment: *testExperimentDecisionContext2.Experiment, @@ -104,16 +109,16 @@ func (s *FeatureExperimentServiceTestSuite) TestGetDecisionMutex() { } featureExperimentService := &FeatureExperimentService{ compositeExperimentService: s.mockExperimentService, - logger:logging.GetLogger("sdkKey", "FeatureExperimentService"), + logger: logging.GetLogger("sdkKey", "FeatureExperimentService"), } - decision, err := featureExperimentService.GetDecision(s.testFeatureDecisionContext, testUserContext) + decision, err := featureExperimentService.GetDecision(s.testFeatureDecisionContext, testUserContext, s.options, s.reasons) s.Equal(expectedFeatureDecision, decision) s.NoError(err) s.mockExperimentService.AssertExpectations(s.T()) } func (s *FeatureExperimentServiceTestSuite) TestNewFeatureExperimentService() { - compositeExperimentService := &CompositeExperimentService{logger:logging.GetLogger("sdkKey", "CompositeExperimentService")} + compositeExperimentService := &CompositeExperimentService{logger: logging.GetLogger("sdkKey", "CompositeExperimentService")} featureExperimentService := NewFeatureExperimentService(logging.GetLogger("", ""), compositeExperimentService) s.IsType(compositeExperimentService, featureExperimentService.compositeExperimentService) } diff --git a/pkg/decision/flag_notification.go b/pkg/decision/flag_notification.go new file mode 100644 index 000000000..f3d02f8cd --- /dev/null +++ b/pkg/decision/flag_notification.go @@ -0,0 +1,48 @@ +/**************************************************************************** + * Copyright 2020, 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 ( + "github.com/optimizely/go-sdk/pkg/entities" + "github.com/optimizely/go-sdk/pkg/notification" +) + +// FlagNotification constructs default flag notification +func FlagNotification(flagKey, variationKey, ruleKey string, enabled, decisionEventDispatched bool, userContext *entities.UserContext, variables map[string]interface{}, reasons []string) *notification.DecisionNotification { + + if flagKey == "" { + return nil + } + + decisionInfo := map[string]interface{}{ + "flagKey": flagKey, + "enabled": enabled, + "variables": variables, + "variationKey": variationKey, + "ruleKey": ruleKey, + "reasons": reasons, + "decisionEventDispatched": decisionEventDispatched, + } + + decisionNotification := ¬ification.DecisionNotification{ + DecisionInfo: decisionInfo, + Type: notification.Flag, + UserContext: *userContext, + } + return decisionNotification +} diff --git a/pkg/decision/helpers_test.go b/pkg/decision/helpers_test.go index 48f78163a..76530af2d 100644 --- a/pkg/decision/helpers_test.go +++ b/pkg/decision/helpers_test.go @@ -23,6 +23,7 @@ package decision import ( "github.com/optimizely/go-sdk/pkg/config" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/stretchr/testify/mock" ) @@ -57,8 +58,8 @@ type MockExperimentDecisionService struct { mock.Mock } -func (m *MockExperimentDecisionService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext) (ExperimentDecision, error) { - args := m.Called(decisionContext, userContext) +func (m *MockExperimentDecisionService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (ExperimentDecision, error) { + args := m.Called(decisionContext, userContext, options, reasons) return args.Get(0).(ExperimentDecision), args.Error(1) } @@ -66,8 +67,8 @@ type MockFeatureDecisionService struct { mock.Mock } -func (m *MockFeatureDecisionService) GetDecision(decisionContext FeatureDecisionContext, userContext entities.UserContext) (FeatureDecision, error) { - args := m.Called(decisionContext, userContext) +func (m *MockFeatureDecisionService) GetDecision(decisionContext FeatureDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (FeatureDecision, error) { + args := m.Called(decisionContext, userContext, options, reasons) return args.Get(0).(FeatureDecision), args.Error(1) } @@ -89,8 +90,8 @@ func (m *MockUserProfileService) Save(userProfile UserProfile) { m.Called(userProfile) } -func (m *MockAudienceTreeEvaluator) Evaluate(node *entities.TreeNode, condTreeParams *entities.TreeParameters) (evalResult, isValid bool) { - args := m.Called(node, condTreeParams) +func (m *MockAudienceTreeEvaluator) Evaluate(node *entities.TreeNode, condTreeParams *entities.TreeParameters, reasons decide.DecisionReasons) (evalResult, isValid bool) { + args := m.Called(node, condTreeParams, reasons) return args.Bool(0), args.Bool(1) } diff --git a/pkg/decision/interface.go b/pkg/decision/interface.go index de4af1a62..051bea214 100644 --- a/pkg/decision/interface.go +++ b/pkg/decision/interface.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -18,26 +18,27 @@ package decision import ( + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/notification" ) // Service interface is used to make a decision for a given feature or experiment type Service interface { - GetFeatureDecision(FeatureDecisionContext, entities.UserContext) (FeatureDecision, error) - GetExperimentDecision(ExperimentDecisionContext, entities.UserContext) (ExperimentDecision, error) + GetFeatureDecision(FeatureDecisionContext, entities.UserContext, decide.OptimizelyDecideOptions, decide.DecisionReasons) (FeatureDecision, error) + GetExperimentDecision(ExperimentDecisionContext, entities.UserContext, decide.OptimizelyDecideOptions, decide.DecisionReasons) (ExperimentDecision, error) OnDecision(func(notification.DecisionNotification)) (int, error) RemoveOnDecision(id int) error } // ExperimentService can make a decision about an experiment type ExperimentService interface { - GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext) (ExperimentDecision, error) + GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (ExperimentDecision, error) } // FeatureService can make a decision about a Feature Flag (can be feature test or rollout) type FeatureService interface { - GetDecision(decisionContext FeatureDecisionContext, userContext entities.UserContext) (FeatureDecision, error) + GetDecision(decisionContext FeatureDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (FeatureDecision, error) } // UserProfileService is used to save and retrieve past bucketing decisions for users diff --git a/pkg/decision/persisting_experiment_service.go b/pkg/decision/persisting_experiment_service.go index 114ebc75f..0715c7f8b 100644 --- a/pkg/decision/persisting_experiment_service.go +++ b/pkg/decision/persisting_experiment_service.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -20,6 +20,7 @@ package decision import ( "fmt" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) @@ -30,13 +31,13 @@ import ( type PersistingExperimentService struct { experimentBucketedService ExperimentService userProfileService UserProfileService - logger logging.OptimizelyLogProducer + logger logging.OptimizelyLogProducer } // NewPersistingExperimentService returns a new instance of the PersistingExperimentService func NewPersistingExperimentService(userProfileService UserProfileService, experimentBucketerService ExperimentService, logger logging.OptimizelyLogProducer) *PersistingExperimentService { persistingExperimentService := &PersistingExperimentService{ - logger: logger, + logger: logger, experimentBucketedService: experimentBucketerService, userProfileService: userProfileService, } @@ -45,29 +46,29 @@ func NewPersistingExperimentService(userProfileService UserProfileService, exper } // GetDecision returns the decision with the variation the user is bucketed into -func (p PersistingExperimentService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext) (experimentDecision ExperimentDecision, err error) { - if p.userProfileService == nil { - return p.experimentBucketedService.GetDecision(decisionContext, userContext) +func (p PersistingExperimentService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (experimentDecision ExperimentDecision, err error) { + if p.userProfileService == nil || options.IgnoreUserProfileService { + return p.experimentBucketedService.GetDecision(decisionContext, userContext, options, reasons) } var userProfile UserProfile // check to see if there is a saved decision for the user - experimentDecision, userProfile = p.getSavedDecision(decisionContext, userContext) + experimentDecision, userProfile = p.getSavedDecision(decisionContext, userContext, reasons) if experimentDecision.Variation != nil { return experimentDecision, nil } - experimentDecision, err = p.experimentBucketedService.GetDecision(decisionContext, userContext) + experimentDecision, err = p.experimentBucketedService.GetDecision(decisionContext, userContext, options, reasons) if experimentDecision.Variation != nil { // save decision if a user profile service is provided userProfile.ID = userContext.ID - p.saveDecision(userProfile, decisionContext.Experiment, experimentDecision) + p.saveDecision(userProfile, decisionContext.Experiment, experimentDecision, reasons) } return experimentDecision, err } -func (p PersistingExperimentService) getSavedDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext) (ExperimentDecision, UserProfile) { +func (p PersistingExperimentService) getSavedDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext, _ decide.DecisionReasons) (ExperimentDecision, UserProfile) { experimentDecision := ExperimentDecision{} userProfile := p.userProfileService.Lookup(userContext.ID) @@ -89,7 +90,7 @@ func (p PersistingExperimentService) getSavedDecision(decisionContext Experiment return experimentDecision, userProfile } -func (p PersistingExperimentService) saveDecision(userProfile UserProfile, experiment *entities.Experiment, decision ExperimentDecision) { +func (p PersistingExperimentService) saveDecision(userProfile UserProfile, experiment *entities.Experiment, decision ExperimentDecision, _ decide.DecisionReasons) { if p.userProfileService != nil { decisionKey := NewUserDecisionKey(experiment.ID) if userProfile.ExperimentBucketMap == nil { diff --git a/pkg/decision/persisting_experiment_service_test.go b/pkg/decision/persisting_experiment_service_test.go index a9a750e3b..41382dafd 100644 --- a/pkg/decision/persisting_experiment_service_test.go +++ b/pkg/decision/persisting_experiment_service_test.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -20,6 +20,7 @@ package decision import ( "testing" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" @@ -38,6 +39,8 @@ type PersistingExperimentServiceTestSuite struct { mockUserProfileService *MockUserProfileService testComputedDecision ExperimentDecision testDecisionContext ExperimentDecisionContext + options decide.OptimizelyDecideOptions + reasons decide.DecisionReasons } func (s *PersistingExperimentServiceTestSuite) SetupTest() { @@ -53,12 +56,14 @@ func (s *PersistingExperimentServiceTestSuite) SetupTest() { s.testComputedDecision = ExperimentDecision{ Variation: &computedVariation, } - s.mockExperimentService.On("GetDecision", s.testDecisionContext, testUserContext).Return(s.testComputedDecision, nil) + s.options = decide.OptimizelyDecideOptions{} + s.reasons = decide.NewDecisionReasons(s.options) + s.mockExperimentService.On("GetDecision", s.testDecisionContext, testUserContext, s.options, s.reasons).Return(s.testComputedDecision, nil) } func (s *PersistingExperimentServiceTestSuite) TestNilUserProfileService() { persistingExperimentService := NewPersistingExperimentService(nil, s.mockExperimentService, logging.GetLogger("", "NewPersistingExperimentService")) - decision, err := persistingExperimentService.GetDecision(s.testDecisionContext, testUserContext) + decision, err := persistingExperimentService.GetDecision(s.testDecisionContext, testUserContext, s.options, s.reasons) s.Equal(s.testComputedDecision, decision) s.NoError(err) s.mockExperimentService.AssertExpectations(s.T()) @@ -74,7 +79,7 @@ func (s *PersistingExperimentServiceTestSuite) TestSavedVariationFound() { s.mockUserProfileService.On("Save", mock.Anything) persistingExperimentService := NewPersistingExperimentService(s.mockUserProfileService, s.mockExperimentService, logging.GetLogger("", "NewPersistingExperimentService")) - decision, err := persistingExperimentService.GetDecision(s.testDecisionContext, testUserContext) + decision, err := persistingExperimentService.GetDecision(s.testDecisionContext, testUserContext, s.options, s.reasons) savedDecision := ExperimentDecision{ Variation: &testExp1113Var2224, } @@ -94,7 +99,7 @@ func (s *PersistingExperimentServiceTestSuite) TestNoSavedVariation() { s.mockUserProfileService.On("Save", updatedUserProfile) persistingExperimentService := NewPersistingExperimentService(s.mockUserProfileService, s.mockExperimentService, logging.GetLogger("", "NewPersistingExperimentService")) - decision, err := persistingExperimentService.GetDecision(s.testDecisionContext, testUserContext) + decision, err := persistingExperimentService.GetDecision(s.testDecisionContext, testUserContext, s.options, s.reasons) s.Equal(s.testComputedDecision, decision) s.NoError(err) s.mockExperimentService.AssertExpectations(s.T()) @@ -115,7 +120,7 @@ func (s *PersistingExperimentServiceTestSuite) TestSavedVariationNoLongerValid() } s.mockUserProfileService.On("Save", updatedUserProfile) persistingExperimentService := NewPersistingExperimentService(s.mockUserProfileService, s.mockExperimentService, logging.GetLogger("", "NewPersistingExperimentService")) - decision, err := persistingExperimentService.GetDecision(s.testDecisionContext, testUserContext) + decision, err := persistingExperimentService.GetDecision(s.testDecisionContext, testUserContext, s.options, s.reasons) s.Equal(s.testComputedDecision, decision) s.NoError(err) s.mockExperimentService.AssertExpectations(s.T()) diff --git a/pkg/decision/rollout_service.go b/pkg/decision/rollout_service.go index 8bddceea5..70389e95f 100644 --- a/pkg/decision/rollout_service.go +++ b/pkg/decision/rollout_service.go @@ -21,8 +21,9 @@ import ( "fmt" "strconv" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision/evaluator" - "github.com/optimizely/go-sdk/pkg/decision/reasons" + pkgReasons "github.com/optimizely/go-sdk/pkg/decision/reasons" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" ) @@ -45,8 +46,7 @@ func NewRolloutService(sdkKey string) *RolloutService { } // GetDecision returns a decision for the given feature and user context -func (r RolloutService) GetDecision(decisionContext FeatureDecisionContext, userContext entities.UserContext) (FeatureDecision, error) { - +func (r RolloutService) GetDecision(decisionContext FeatureDecisionContext, userContext entities.UserContext, options decide.OptimizelyDecideOptions, reasons decide.DecisionReasons) (FeatureDecision, error) { featureDecision := FeatureDecision{ Source: Rollout, } @@ -56,9 +56,9 @@ func (r RolloutService) GetDecision(decisionContext FeatureDecisionContext, user evaluateConditionTree := func(experiment *entities.Experiment, loggingKey string) bool { condTreeParams := entities.NewTreeParameters(&userContext, decisionContext.ProjectConfig.GetAudienceMap()) r.logger.Debug(fmt.Sprintf(logging.EvaluatingAudiencesForRollout.String(), loggingKey)) - evalResult, _ := r.audienceTreeEvaluator.Evaluate(experiment.AudienceConditionTree, condTreeParams) + evalResult, _ := r.audienceTreeEvaluator.Evaluate(experiment.AudienceConditionTree, condTreeParams, reasons) if !evalResult { - featureDecision.Reason = reasons.FailedRolloutTargeting + featureDecision.Reason = pkgReasons.FailedRolloutTargeting } return evalResult } @@ -66,10 +66,10 @@ func (r RolloutService) GetDecision(decisionContext FeatureDecisionContext, user getFeatureDecision := func(experiment *entities.Experiment, decision *ExperimentDecision) (FeatureDecision, error) { // translate the experiment reason into a more rollouts-appropriate reason switch decision.Reason { - case reasons.NotBucketedIntoVariation: - featureDecision.Decision = Decision{Reason: reasons.FailedRolloutBucketing} - case reasons.BucketedIntoVariation: - featureDecision.Decision = Decision{Reason: reasons.BucketedIntoRollout} + case pkgReasons.NotBucketedIntoVariation: + featureDecision.Decision = Decision{Reason: pkgReasons.FailedRolloutBucketing} + case pkgReasons.BucketedIntoVariation: + featureDecision.Decision = Decision{Reason: pkgReasons.BucketedIntoRollout} default: featureDecision.Decision = decision.Decision } @@ -90,13 +90,13 @@ func (r RolloutService) GetDecision(decisionContext FeatureDecisionContext, user } if rollout.ID == "" { - featureDecision.Reason = reasons.NoRolloutForFeature + featureDecision.Reason = pkgReasons.NoRolloutForFeature return featureDecision, nil } numberOfExperiments := len(rollout.Experiments) if numberOfExperiments == 0 { - featureDecision.Reason = reasons.RolloutHasNoExperiments + featureDecision.Reason = pkgReasons.RolloutHasNoExperiments return featureDecision, nil } @@ -114,7 +114,7 @@ func (r RolloutService) GetDecision(decisionContext FeatureDecisionContext, user continue } - decision, _ := r.experimentBucketerService.GetDecision(experimentDecisionContext, userContext) + decision, _ := r.experimentBucketerService.GetDecision(experimentDecisionContext, userContext, options, reasons) if decision.Variation == nil { // Evaluate fall back rule / last rule now break @@ -130,9 +130,8 @@ func (r RolloutService) GetDecision(decisionContext FeatureDecisionContext, user r.logger.Debug(fmt.Sprintf(logging.RolloutAudiencesEvaluatedTo.String(), "Everyone Else", evaluationResult)) if evaluationResult { - decision, err := r.experimentBucketerService.GetDecision(experimentDecisionContext, userContext) + decision, err := r.experimentBucketerService.GetDecision(experimentDecisionContext, userContext, options, reasons) if err == nil { - r.logger.Debug(fmt.Sprintf(logging.UserInEveryoneElse.String(), userContext.ID)) } return getFeatureDecision(experiment, &decision) diff --git a/pkg/decision/rollout_service_test.go b/pkg/decision/rollout_service_test.go index 1013e9728..a23a6f493 100644 --- a/pkg/decision/rollout_service_test.go +++ b/pkg/decision/rollout_service_test.go @@ -20,12 +20,14 @@ import ( "fmt" "testing" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision/evaluator" "github.com/optimizely/go-sdk/pkg/decision/reasons" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/logging" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) @@ -39,6 +41,8 @@ type RolloutServiceTestSuite struct { testConditionTreeParams *entities.TreeParameters testUserContext entities.UserContext mockLogger *MockLogger + options decide.OptimizelyDecideOptions + reasons decide.DecisionReasons } func (s *RolloutServiceTestSuite) SetupTest() { @@ -65,6 +69,8 @@ func (s *RolloutServiceTestSuite) SetupTest() { s.testConditionTreeParams = entities.NewTreeParameters(&s.testUserContext, testAudienceMap) s.mockConfig.On("GetAudienceMap").Return(testAudienceMap) s.mockLogger = new(MockLogger) + s.options = decide.OptimizelyDecideOptions{} + s.reasons = decide.NewDecisionReasons(s.options) } func (s *RolloutServiceTestSuite) TestGetDecisionWithEmptyRolloutID() { @@ -72,6 +78,7 @@ func (s *RolloutServiceTestSuite) TestGetDecisionWithEmptyRolloutID() { testRolloutService := RolloutService{ logger: s.mockLogger, } + s.mockLogger.On("Info", `The feature flag "test_feature_rollout_3334_key" is not used in a rollout.`) feature := testFeatRollout3334 feature.Rollout.ID = "" featureDecisionContext := FeatureDecisionContext{ @@ -82,7 +89,7 @@ func (s *RolloutServiceTestSuite) TestGetDecisionWithEmptyRolloutID() { Source: Rollout, Decision: Decision{Reason: reasons.NoRolloutForFeature}, } - decision, _ := testRolloutService.GetDecision(featureDecisionContext, s.testUserContext) + decision, _ := testRolloutService.GetDecision(featureDecisionContext, s.testUserContext, s.options, s.reasons) s.Equal(expectedFeatureDecision, decision) } @@ -101,7 +108,7 @@ func (s *RolloutServiceTestSuite) TestGetDecisionWithNoExperiments() { Source: Rollout, Decision: Decision{Reason: reasons.RolloutHasNoExperiments}, } - decision, _ := testRolloutService.GetDecision(featureDecisionContext, s.testUserContext) + decision, _ := testRolloutService.GetDecision(featureDecisionContext, s.testUserContext, s.options, s.reasons) s.Equal(expectedFeatureDecision, decision) } @@ -111,8 +118,8 @@ func (s *RolloutServiceTestSuite) TestGetDecisionHappyPath() { Variation: &testExp1112Var2222, Decision: Decision{Reason: reasons.BucketedIntoVariation}, } - s.mockAudienceTreeEvaluator.On("Evaluate", testExp1112.AudienceConditionTree, s.testConditionTreeParams).Return(true, true) - s.mockExperimentService.On("GetDecision", s.testExperiment1112DecisionContext, s.testUserContext).Return(testExperimentBucketerDecision, nil) + s.mockAudienceTreeEvaluator.On("Evaluate", testExp1112.AudienceConditionTree, s.testConditionTreeParams, mock.Anything).Return(true, true) + s.mockExperimentService.On("GetDecision", s.testExperiment1112DecisionContext, s.testUserContext, s.options, mock.Anything).Return(testExperimentBucketerDecision, nil) testRolloutService := RolloutService{ audienceTreeEvaluator: s.mockAudienceTreeEvaluator, @@ -128,7 +135,7 @@ func (s *RolloutServiceTestSuite) TestGetDecisionHappyPath() { s.mockLogger.On("Debug", fmt.Sprintf(logging.EvaluatingAudiencesForRollout.String(), "1")) s.mockLogger.On("Debug", fmt.Sprintf(logging.RolloutAudiencesEvaluatedTo.String(), "1", true)) s.mockLogger.On("Debug", `Decision made for user "test_user" for feature rollout with key "test_feature_rollout_3334_key": Bucketed into feature rollout.`) - decision, _ := testRolloutService.GetDecision(s.testFeatureDecisionContext, s.testUserContext) + decision, _ := testRolloutService.GetDecision(s.testFeatureDecisionContext, s.testUserContext, s.options, s.reasons) s.Equal(expectedFeatureDecision, decision) s.mockAudienceTreeEvaluator.AssertExpectations(s.T()) s.mockExperimentService.AssertExpectations(s.T()) @@ -149,10 +156,10 @@ func (s *RolloutServiceTestSuite) TestGetDecisionFallbacksToLastWhenFailsBucketi Experiment: &testExp1118, ProjectConfig: s.mockConfig, } - s.mockAudienceTreeEvaluator.On("Evaluate", testExp1112.AudienceConditionTree, s.testConditionTreeParams).Return(true, true) - s.mockAudienceTreeEvaluator.On("Evaluate", testExp1118.AudienceConditionTree, s.testConditionTreeParams).Return(true, true) - s.mockExperimentService.On("GetDecision", s.testExperiment1112DecisionContext, s.testUserContext).Return(testExperiment1112BucketerDecision, nil) - s.mockExperimentService.On("GetDecision", experiment1118DecisionContext, s.testUserContext).Return(testExperiment1118BucketerDecision, nil) + s.mockAudienceTreeEvaluator.On("Evaluate", testExp1112.AudienceConditionTree, s.testConditionTreeParams, mock.Anything).Return(true, true) + s.mockAudienceTreeEvaluator.On("Evaluate", testExp1118.AudienceConditionTree, s.testConditionTreeParams, mock.Anything).Return(true, true) + s.mockExperimentService.On("GetDecision", s.testExperiment1112DecisionContext, s.testUserContext, s.options, mock.Anything).Return(testExperiment1112BucketerDecision, nil) + s.mockExperimentService.On("GetDecision", experiment1118DecisionContext, s.testUserContext, s.options, mock.Anything).Return(testExperiment1118BucketerDecision, nil) testRolloutService := RolloutService{ audienceTreeEvaluator: s.mockAudienceTreeEvaluator, @@ -171,7 +178,7 @@ func (s *RolloutServiceTestSuite) TestGetDecisionFallbacksToLastWhenFailsBucketi s.mockLogger.On("Debug", fmt.Sprintf(logging.RolloutAudiencesEvaluatedTo.String(), "Everyone Else", true)) s.mockLogger.On("Debug", fmt.Sprintf(logging.UserInEveryoneElse.String(), "test_user")) s.mockLogger.On("Debug", `Decision made for user "test_user" for feature rollout with key "test_feature_rollout_3334_key": Bucketed into feature rollout.`) - decision, _ := testRolloutService.GetDecision(s.testFeatureDecisionContext, s.testUserContext) + decision, _ := testRolloutService.GetDecision(s.testFeatureDecisionContext, s.testUserContext, s.options, s.reasons) s.Equal(expectedFeatureDecision, decision) s.mockAudienceTreeEvaluator.AssertExpectations(s.T()) s.mockExperimentService.AssertExpectations(s.T()) @@ -188,10 +195,10 @@ func (s *RolloutServiceTestSuite) TestGetDecisionWhenFallbackBucketingFails() { Experiment: &testExp1118, ProjectConfig: s.mockConfig, } - s.mockAudienceTreeEvaluator.On("Evaluate", testExp1112.AudienceConditionTree, s.testConditionTreeParams).Return(true, true) - s.mockAudienceTreeEvaluator.On("Evaluate", testExp1118.AudienceConditionTree, s.testConditionTreeParams).Return(true, true) - s.mockExperimentService.On("GetDecision", s.testExperiment1112DecisionContext, s.testUserContext).Return(testExperiment1112BucketerDecision, nil) - s.mockExperimentService.On("GetDecision", testExperiment1118DecisionContext, s.testUserContext).Return(testExperiment1112BucketerDecision, nil) + s.mockAudienceTreeEvaluator.On("Evaluate", testExp1112.AudienceConditionTree, s.testConditionTreeParams, mock.Anything).Return(true, true) + s.mockAudienceTreeEvaluator.On("Evaluate", testExp1118.AudienceConditionTree, s.testConditionTreeParams, mock.Anything).Return(true, true) + s.mockExperimentService.On("GetDecision", s.testExperiment1112DecisionContext, s.testUserContext, s.options, mock.Anything).Return(testExperiment1112BucketerDecision, nil) + s.mockExperimentService.On("GetDecision", testExperiment1118DecisionContext, s.testUserContext, s.options, mock.Anything).Return(testExperiment1112BucketerDecision, nil) testRolloutService := RolloutService{ audienceTreeEvaluator: s.mockAudienceTreeEvaluator, @@ -209,7 +216,7 @@ func (s *RolloutServiceTestSuite) TestGetDecisionWhenFallbackBucketingFails() { s.mockLogger.On("Debug", fmt.Sprintf(logging.RolloutAudiencesEvaluatedTo.String(), "Everyone Else", true)) s.mockLogger.On("Debug", fmt.Sprintf(logging.UserInEveryoneElse.String(), "test_user")) s.mockLogger.On("Debug", `Decision made for user "test_user" for feature rollout with key "test_feature_rollout_3334_key": Not bucketed into rollout.`) - decision, _ := testRolloutService.GetDecision(s.testFeatureDecisionContext, s.testUserContext) + decision, _ := testRolloutService.GetDecision(s.testFeatureDecisionContext, s.testUserContext, s.options, s.reasons) s.Equal(expectedFeatureDecision, decision) s.mockAudienceTreeEvaluator.AssertExpectations(s.T()) s.mockExperimentService.AssertExpectations(s.T()) @@ -217,8 +224,9 @@ func (s *RolloutServiceTestSuite) TestGetDecisionWhenFallbackBucketingFails() { } func (s *RolloutServiceTestSuite) TestEvaluatesNextIfPreviousTargetingFails() { - s.mockAudienceTreeEvaluator.On("Evaluate", testExp1112.AudienceConditionTree, s.testConditionTreeParams).Return(false, true) - s.mockAudienceTreeEvaluator.On("Evaluate", testExp1117.AudienceConditionTree, s.testConditionTreeParams).Return(true, true) + s.mockAudienceTreeEvaluator.On("Evaluate", testExp1112.AudienceConditionTree, s.testConditionTreeParams, mock.Anything).Return(false, true) + s.mockAudienceTreeEvaluator.On("Evaluate", testExp1117.AudienceConditionTree, s.testConditionTreeParams, mock.Anything).Return(true, true) + experiment1117DecisionContext := ExperimentDecisionContext{ Experiment: &testExp1117, ProjectConfig: s.mockConfig, @@ -227,7 +235,7 @@ func (s *RolloutServiceTestSuite) TestEvaluatesNextIfPreviousTargetingFails() { Variation: &testExp1117Var2223, Decision: Decision{Reason: reasons.BucketedIntoVariation}, } - s.mockExperimentService.On("GetDecision", experiment1117DecisionContext, s.testUserContext).Return(testExperimentBucketerDecision, nil) + s.mockExperimentService.On("GetDecision", experiment1117DecisionContext, s.testUserContext, s.options, mock.Anything).Return(testExperimentBucketerDecision, nil) testRolloutService := RolloutService{ audienceTreeEvaluator: s.mockAudienceTreeEvaluator, @@ -246,7 +254,7 @@ func (s *RolloutServiceTestSuite) TestEvaluatesNextIfPreviousTargetingFails() { s.mockLogger.On("Debug", fmt.Sprintf(logging.EvaluatingAudiencesForRollout.String(), "2")) s.mockLogger.On("Debug", fmt.Sprintf(logging.RolloutAudiencesEvaluatedTo.String(), "2", true)) s.mockLogger.On("Debug", `Decision made for user "test_user" for feature rollout with key "test_feature_rollout_3334_key": Bucketed into feature rollout.`) - decision, _ := testRolloutService.GetDecision(s.testFeatureDecisionContext, s.testUserContext) + decision, _ := testRolloutService.GetDecision(s.testFeatureDecisionContext, s.testUserContext, s.options, s.reasons) s.Equal(expectedFeatureDecision, decision) s.mockAudienceTreeEvaluator.AssertExpectations(s.T()) s.mockExperimentService.AssertExpectations(s.T()) @@ -254,9 +262,9 @@ func (s *RolloutServiceTestSuite) TestEvaluatesNextIfPreviousTargetingFails() { } func (s *RolloutServiceTestSuite) TestGetDecisionFailsTargeting() { - s.mockAudienceTreeEvaluator.On("Evaluate", testExp1112.AudienceConditionTree, s.testConditionTreeParams).Return(false, true) - s.mockAudienceTreeEvaluator.On("Evaluate", testExp1117.AudienceConditionTree, s.testConditionTreeParams).Return(false, true) - s.mockAudienceTreeEvaluator.On("Evaluate", testExp1118.AudienceConditionTree, s.testConditionTreeParams).Return(false, true) + s.mockAudienceTreeEvaluator.On("Evaluate", testExp1112.AudienceConditionTree, s.testConditionTreeParams, mock.Anything).Return(false, true) + s.mockAudienceTreeEvaluator.On("Evaluate", testExp1117.AudienceConditionTree, s.testConditionTreeParams, mock.Anything).Return(false, true) + s.mockAudienceTreeEvaluator.On("Evaluate", testExp1118.AudienceConditionTree, s.testConditionTreeParams, mock.Anything).Return(false, true) testRolloutService := RolloutService{ audienceTreeEvaluator: s.mockAudienceTreeEvaluator, experimentBucketerService: s.mockExperimentService, @@ -276,7 +284,7 @@ func (s *RolloutServiceTestSuite) TestGetDecisionFailsTargeting() { s.mockLogger.On("Debug", fmt.Sprintf(logging.UserNotInRollout.String(), "test_user", "2")) s.mockLogger.On("Debug", fmt.Sprintf(logging.EvaluatingAudiencesForRollout.String(), "Everyone Else")) s.mockLogger.On("Debug", fmt.Sprintf(logging.RolloutAudiencesEvaluatedTo.String(), "Everyone Else", false)) - decision, _ := testRolloutService.GetDecision(s.testFeatureDecisionContext, s.testUserContext) + decision, _ := testRolloutService.GetDecision(s.testFeatureDecisionContext, s.testUserContext, s.options, s.reasons) s.Equal(expectedFeatureDecision, decision) s.mockAudienceTreeEvaluator.AssertExpectations(s.T()) s.mockExperimentService.AssertExpectations(s.T()) diff --git a/pkg/notification/entities.go b/pkg/notification/entities.go index 89c071221..edb801c9c 100644 --- a/pkg/notification/entities.go +++ b/pkg/notification/entities.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019-2020, 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. * @@ -47,6 +47,8 @@ const ( FeatureVariable DecisionNotificationType = "feature-variable" // AllFeatureVariables is used when the decision is returned as part of evaluating a feature with all variables AllFeatureVariables DecisionNotificationType = "all-feature-variables" + // Flag is used when the decision is returned using decide api + Flag DecisionNotificationType = "flag" ) // DecisionNotification is a notification triggered when a decision is made for either a feature or an experiment