Skip to content

feat(decide): Adding support for decide options and decision reasons. #298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 38 commits into from
Dec 15, 2020
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
2f52077
initial commit.
yasirfolio3 Oct 14, 2020
e568bac
Adding unit tests.
yasirfolio3 Oct 16, 2020
5792f2c
linter warnings fixed.
yasirfolio3 Oct 16, 2020
26a25ec
nit fixed.
yasirfolio3 Oct 16, 2020
d98e193
fixes.
yasirfolio3 Oct 19, 2020
c7a0fd9
fixes.
yasirfolio3 Oct 19, 2020
431f97a
Merge branch 'master' into yasir/user-context-2
yasirfolio3 Oct 19, 2020
b223b56
Internal changes to accomodate decide api.
yasirfolio3 Oct 21, 2020
d8c4f49
Merge branch 'yasir/user-context-2' into yasir/decide-api
yasirfolio3 Oct 21, 2020
6157c71
linter warnings fixed.
yasirfolio3 Oct 21, 2020
880fb08
linter warnings fixed.
yasirfolio3 Oct 21, 2020
91c1beb
removing added reasons, to be added in a separate PR.
yasirfolio3 Oct 22, 2020
8fc5bc5
linter warnings fixed.
yasirfolio3 Oct 22, 2020
564679e
removing redundant info method.
yasirfolio3 Oct 23, 2020
f257a20
suggested changes made.
yasirfolio3 Oct 23, 2020
4ab5403
fixes.
yasirfolio3 Oct 23, 2020
7d688c1
Merge branch 'yasir/user-context-2' into yasir/decide-api-2
yasirfolio3 Oct 23, 2020
f08ac01
fixes.
yasirfolio3 Oct 26, 2020
23cb870
Merge branch 'yasir/user-context-2' into yasir/decide-api-2
yasirfolio3 Oct 26, 2020
b841442
Suggested changes made.
yasirfolio3 Oct 27, 2020
9b7153a
Merge branch 'yasir/user-context-2' into yasir/decide-api-2
yasirfolio3 Oct 27, 2020
d413572
removing unnecessary code.
yasirfolio3 Oct 27, 2020
55dec9b
nit fixed.
yasirfolio3 Oct 28, 2020
e28ed7c
Updating decision reasons to a pointer.
yasirfolio3 Oct 28, 2020
f63b7f5
nits fixed.
yasirfolio3 Oct 28, 2020
0b6d43a
Merge branch 'yasir/user-context-2' into yasir/decide-api-2
yasirfolio3 Oct 28, 2020
ea619fa
fixes.
yasirfolio3 Oct 28, 2020
bd34bd7
Merge branch 'yasir/user-context-2' into yasir/decide-api-2.
yasirfolio3 Oct 28, 2020
ef47d2a
suggested changes made.
yasirfolio3 Nov 3, 2020
6482bda
Merge branch 'master' into yasir/user-context-2
yasirfolio3 Nov 3, 2020
1ffd78c
Updates and fixes.
yasirfolio3 Nov 3, 2020
5f4a517
fixes.
yasirfolio3 Nov 9, 2020
319dfd2
linter warnings fixed.
yasirfolio3 Nov 9, 2020
b460a9a
Merge branch 'master' into yasir/user-context-2
yasirfolio3 Nov 24, 2020
e113a70
Merge branch 'yasir/user-context-2' into yasir/decide-api-2
yasirfolio3 Nov 24, 2020
7f4b735
Merge branch 'master' into yasir/decide-api-2
msohailhussain Dec 10, 2020
5fc5e9f
Added test for reasons with arguments.
yasirfolio3 Dec 11, 2020
5084ef8
suggested changes made.
yasirfolio3 Dec 15, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"strconv"

"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"
Expand All @@ -39,12 +40,21 @@ import (

// OptimizelyClient is the entry point to the Optimizely SDK
type OptimizelyClient struct {
ConfigManager config.ProjectConfigManager
DecisionService decision.Service
EventProcessor event.Processor
notificationCenter notification.Center
execGroup *utils.ExecGroup
logger logging.OptimizelyLogProducer
ConfigManager config.ProjectConfigManager
DecisionService decision.Service
EventProcessor event.Processor
notificationCenter notification.Center
execGroup *utils.ExecGroup
logger logging.OptimizelyLogProducer
defaultDecideOptions decide.OptimizelyDecideOptions
}

// Decide API
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove


// CreateUserContext creates a context of the user for which decision APIs will be called.
// A user context will be created successfully even when the SDK is not fully configured yet.
func (o *OptimizelyClient) CreateUserContext(userID string, attributes map[string]interface{}) OptimizelyUserContext {
return newOptimizelyUserContext(o, userID, attributes)
}

// Activate returns the key of the variation the user is bucketed into and queues up an impression event to be sent to
Expand Down Expand Up @@ -649,7 +659,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
Expand Down Expand Up @@ -679,7 +690,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
Expand Down
121 changes: 88 additions & 33 deletions pkg/client/client_test.go

Large diffs are not rendered by default.

34 changes: 23 additions & 11 deletions pkg/client/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"time"

"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/event"
"github.com/optimizely/go-sdk/pkg/logging"
Expand All @@ -37,14 +38,15 @@ type OptimizelyFactory struct {
Datafile []byte
DatafileAccessToken string

configManager config.ProjectConfigManager
ctx context.Context
decisionService decision.Service
eventDispatcher event.Dispatcher
eventProcessor event.Processor
userProfileService decision.UserProfileService
overrideStore decision.ExperimentOverrideStore
metricsRegistry metrics.Registry
configManager config.ProjectConfigManager
ctx context.Context
decisionService decision.Service
defaultDecideOptions decide.OptimizelyDecideOptions
eventDispatcher event.Dispatcher
eventProcessor event.Processor
userProfileService decision.UserProfileService
overrideStore decision.ExperimentOverrideStore
metricsRegistry metrics.Registry
}

// OptionFunc is used to provide custom client configuration to the OptimizelyFactory.
Expand Down Expand Up @@ -76,9 +78,12 @@ func (f *OptimizelyFactory) Client(clientOptions ...OptionFunc) (*OptimizelyClie
}

eg := utils.NewExecGroup(ctx, logging.GetLogger(f.SDKKey, "ExecGroup"))
appClient := &OptimizelyClient{execGroup: eg,
notificationCenter: registry.GetNotificationCenter(f.SDKKey),
logger: logging.GetLogger(f.SDKKey, "OptimizelyClient")}
appClient := &OptimizelyClient{
defaultDecideOptions: f.defaultDecideOptions,
execGroup: eg,
notificationCenter: registry.GetNotificationCenter(f.SDKKey),
logger: logging.GetLogger(f.SDKKey, "OptimizelyClient"),
}

if f.configManager != nil {
appClient.ConfigManager = f.configManager
Expand Down Expand Up @@ -167,6 +172,13 @@ func WithDecisionService(decisionService decision.Service) OptionFunc {
}
}

// WithDefaultDecideOptions sets default decide options on a client.
func WithDefaultDecideOptions(decideOptions decide.OptimizelyDecideOptions) OptionFunc {
return func(f *OptimizelyFactory) {
f.defaultDecideOptions = decideOptions
}
}

// WithUserProfileService sets the user profile service on the decision service.
func WithUserProfileService(userProfileService decision.UserProfileService) OptionFunc {
return func(f *OptimizelyFactory) {
Expand Down
33 changes: 33 additions & 0 deletions pkg/client/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"time"

"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/event"
"github.com/optimizely/go-sdk/pkg/metrics"
Expand Down Expand Up @@ -201,3 +202,35 @@ func TestClientWithDatafileAccessToken(t *testing.T) {

assert.Equal(t, accessToken, factory.DatafileAccessToken)
}

func TestClientWithDefaultDecideOptions(t *testing.T) {
decideOptions := decide.OptimizelyDecideOptions{
DisableDecisionEvent: true,
EnabledFlagsOnly: true,
}
factory := OptimizelyFactory{SDKKey: "1212"}
optimizelyClient, err := factory.Client(WithDefaultDecideOptions(decideOptions))
assert.NoError(t, err)
assert.Equal(t, decideOptions, optimizelyClient.defaultDecideOptions)

// Verify that defaultDecideOptions are initialized as empty by default
factory = OptimizelyFactory{SDKKey: "1212"}
optimizelyClient, err = factory.Client()
assert.NoError(t, err)
assert.Equal(t, decide.OptimizelyDecideOptions{}, optimizelyClient.defaultDecideOptions)
}

func TestModifyingDecideOptionsOutsideClient(t *testing.T) {
decideOptions := decide.OptimizelyDecideOptions{
DisableDecisionEvent: true,
EnabledFlagsOnly: true,
}
factory := OptimizelyFactory{SDKKey: "1212"}
optimizelyClient, err := factory.Client(WithDefaultDecideOptions(decideOptions))
assert.NoError(t, err)
decideOptions.IgnoreUserProfileService = true
assert.Equal(t, decide.OptimizelyDecideOptions{
DisableDecisionEvent: true,
EnabledFlagsOnly: true,
}, optimizelyClient.defaultDecideOptions)
}
15 changes: 8 additions & 7 deletions pkg/client/fixtures_test.go
Original file line number Diff line number Diff line change
@@ -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. *
Expand All @@ -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"
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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")
}

Expand Down
91 changes: 91 additions & 0 deletions pkg/client/optimizely_decision.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/****************************************************************************
* 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 client //
package client

import (
"github.com/optimizely/go-sdk/pkg/optimizelyjson"
)

// OptimizelyDecision defines the decision returned by decide api.
type OptimizelyDecision struct {
variationKey string
enabled bool
variables *optimizelyjson.OptimizelyJSON
ruleKey string
flagKey string
userContext OptimizelyUserContext
reasons []string
}

// NewOptimizelyDecision creates and returns a new instance of OptimizelyDecision
func NewOptimizelyDecision(variationKey, ruleKey, flagKey string, enabled bool, variables *optimizelyjson.OptimizelyJSON, userContext OptimizelyUserContext, reasons []string) OptimizelyDecision {
return OptimizelyDecision{
variationKey: variationKey,
enabled: enabled,
variables: variables,
ruleKey: ruleKey,
flagKey: flagKey,
userContext: userContext,
reasons: reasons,
}
}

// NewErrorDecision returns a decision with error
func NewErrorDecision(key string, user OptimizelyUserContext, err error) OptimizelyDecision {
return OptimizelyDecision{
flagKey: key,
userContext: user,
variables: &optimizelyjson.OptimizelyJSON{},
reasons: []string{err.Error()},
}
}

// GetVariationKey returns variation key for optimizely decision.
func (o OptimizelyDecision) GetVariationKey() string {
return o.variationKey
}

// GetEnabled returns the boolean value indicating if the flag is enabled or not.
func (o OptimizelyDecision) GetEnabled() bool {
return o.enabled
}

// GetVariables returns the collection of variables associated with the decision.
func (o OptimizelyDecision) GetVariables() *optimizelyjson.OptimizelyJSON {
return o.variables
}

// GetRuleKey returns the rule key of the decision.
func (o OptimizelyDecision) GetRuleKey() string {
return o.ruleKey
}

// GetFlagKey returns the flag key for which the decision was made.
func (o OptimizelyDecision) GetFlagKey() string {
return o.flagKey
}

// GetUserContext returns the user context for which the decision was made.
func (o OptimizelyDecision) GetUserContext() OptimizelyUserContext {
return o.userContext
}

// GetReasons returns an array of error/info/debug messages describing why the decision has been made.
func (o OptimizelyDecision) GetReasons() []string {
return o.reasons
}
80 changes: 80 additions & 0 deletions pkg/client/optimizely_decision_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/****************************************************************************
* 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 client

import (
"errors"
"testing"

"github.com/optimizely/go-sdk/pkg/optimizelyjson"

"github.com/stretchr/testify/suite"
)

type OptimizelyDecisionTestSuite struct {
suite.Suite
*OptimizelyClient
}

func (s *OptimizelyDecisionTestSuite) SetupTest() {
factory := OptimizelyFactory{SDKKey: "1212"}
s.OptimizelyClient, _ = factory.Client()
}

func (s *OptimizelyDecisionTestSuite) TestOptimizelyDecision() {
variationKey := "var1"
enabled := true
variables, _ := optimizelyjson.NewOptimizelyJSONfromString(`{"k1":"v1"}`)
var ruleKey string
flagKey := "flag1"
reasons := []string{}
userID := "testUser1"
attributes := map[string]interface{}{"key": 1212}

optimizelyUserContext := s.OptimizelyClient.CreateUserContext(userID, attributes)
decision := NewOptimizelyDecision(variationKey, ruleKey, flagKey, enabled, variables, optimizelyUserContext, reasons)

s.Equal(variationKey, decision.GetVariationKey())
s.Equal(enabled, decision.GetEnabled())
s.Equal(variables, decision.GetVariables())
s.Equal(ruleKey, decision.GetRuleKey())
s.Equal(flagKey, decision.GetFlagKey())
s.Equal(reasons, decision.GetReasons())
s.Equal(optimizelyUserContext, decision.GetUserContext())
}

func (s *OptimizelyDecisionTestSuite) TestNewErrorDecision() {
flagKey := "flag1"
errorString := "SDK has an error"
userID := "testUser1"
attributes := map[string]interface{}{"key": 1212}
optimizelyUserContext := s.OptimizelyClient.CreateUserContext(userID, attributes)
decision := NewErrorDecision(flagKey, optimizelyUserContext, errors.New(errorString))

s.Equal("", decision.GetVariationKey())
s.Equal(false, decision.GetEnabled())
s.Equal(&optimizelyjson.OptimizelyJSON{}, decision.GetVariables())
s.Equal("", decision.GetRuleKey())
s.Equal(flagKey, decision.GetFlagKey())
s.Equal(1, len(decision.GetReasons()))
s.Equal(optimizelyUserContext, decision.GetUserContext())
s.Equal(errorString, decision.GetReasons()[0])
}

func TestOptimizelyDecisionTestSuite(t *testing.T) {
suite.Run(t, new(OptimizelyDecisionTestSuite))
}
Loading