Skip to content

Commit b091387

Browse files
author
Michael Ng
authored
feat(client): Allow user profile service and overrides to be passed in. (#168)
1 parent 83a7cb3 commit b091387

9 files changed

+203
-62
lines changed

pkg/client/factory.go

Lines changed: 81 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,72 @@ import (
3232
type OptimizelyFactory struct {
3333
SDKKey string
3434
Datafile []byte
35+
36+
configManager pkg.ProjectConfigManager
37+
decisionService decision.Service
38+
eventProcessor event.Processor
39+
executionCtx utils.ExecutionCtx
40+
userProfileService decision.UserProfileService
41+
overrideStore decision.ExperimentOverrideStore
3542
}
3643

3744
// OptionFunc is a type to a proper func
38-
type OptionFunc func(*OptimizelyClient)
45+
type OptionFunc func(*OptimizelyFactory)
3946

4047
// Client gets client and sets some parameters
4148
func (f OptimizelyFactory) Client(clientOptions ...OptionFunc) (*OptimizelyClient, error) {
49+
// extract options
50+
for _, opt := range clientOptions {
51+
opt(&f)
52+
}
4253

43-
executionCtx := utils.NewCancelableExecutionCtx()
54+
if f.SDKKey == "" && f.Datafile == nil && f.configManager == nil {
55+
return nil, errors.New("unable to instantiate client: no project config manager, SDK key, or a Datafile provided")
56+
}
4457

45-
appClient := &OptimizelyClient{
46-
executionCtx: executionCtx,
47-
DecisionService: decision.NewCompositeService(f.SDKKey),
48-
EventProcessor: event.NewBatchEventProcessor(event.WithBatchSize(event.DefaultBatchSize),
49-
event.WithQueueSize(event.DefaultEventQueueSize), event.WithFlushInterval(event.DefaultEventFlushInterval),
50-
event.WithSDKKey(f.SDKKey)),
58+
var executionCtx utils.ExecutionCtx
59+
if f.executionCtx != nil {
60+
executionCtx = f.executionCtx
61+
} else {
62+
executionCtx = utils.NewCancelableExecutionCtx()
5163
}
5264

53-
for _, opt := range clientOptions {
54-
opt(appClient)
65+
appClient := &OptimizelyClient{executionCtx: executionCtx}
66+
67+
if f.configManager != nil {
68+
appClient.ConfigManager = f.configManager
69+
} else {
70+
appClient.ConfigManager = config.NewPollingProjectConfigManager(
71+
f.SDKKey,
72+
config.InitialDatafile(f.Datafile),
73+
config.PollingInterval(config.DefaultPollingInterval),
74+
)
5575
}
5676

57-
if f.SDKKey == "" && f.Datafile == nil && appClient.ConfigManager == nil {
58-
return nil, errors.New("unable to instantiate client: no project config manager, SDK key, or a Datafile provided")
77+
if f.eventProcessor != nil {
78+
appClient.EventProcessor = f.eventProcessor
79+
} else {
80+
appClient.EventProcessor = event.NewBatchEventProcessor(
81+
event.WithBatchSize(event.DefaultBatchSize),
82+
event.WithQueueSize(event.DefaultEventQueueSize),
83+
event.WithFlushInterval(event.DefaultEventFlushInterval),
84+
event.WithSDKKey(f.SDKKey),
85+
)
5986
}
6087

61-
if appClient.ConfigManager == nil { // if it was not passed then assign here
62-
appClient.ConfigManager = config.NewPollingProjectConfigManager(f.SDKKey,
63-
config.InitialDatafile(f.Datafile), config.PollingInterval(config.DefaultPollingInterval))
88+
if f.decisionService != nil {
89+
appClient.DecisionService = f.decisionService
90+
} else {
91+
experimentServiceOptions := []decision.CESOptionFunc{}
92+
if f.userProfileService != nil {
93+
experimentServiceOptions = append(experimentServiceOptions, decision.WithUserProfileService(f.userProfileService))
94+
}
95+
if f.overrideStore != nil {
96+
experimentServiceOptions = append(experimentServiceOptions, decision.WithOverrideStore(f.overrideStore))
97+
}
98+
compositeExperimentService := decision.NewCompositeExperimentService(experimentServiceOptions...)
99+
compositeService := decision.NewCompositeService(f.SDKKey, decision.WithCompositeExperimentService(compositeExperimentService))
100+
appClient.DecisionService = compositeService
64101
}
65102

66103
// Initialize the default services with the execution context
@@ -77,59 +114,73 @@ func (f OptimizelyFactory) Client(clientOptions ...OptionFunc) (*OptimizelyClien
77114

78115
// WithPollingConfigManager sets polling config manager on a client
79116
func WithPollingConfigManager(sdkKey string, pollingInterval time.Duration, initDataFile []byte) OptionFunc {
80-
return func(f *OptimizelyClient) {
81-
f.ConfigManager = config.NewPollingProjectConfigManager(sdkKey, config.InitialDatafile(initDataFile),
117+
return func(f *OptimizelyFactory) {
118+
f.configManager = config.NewPollingProjectConfigManager(sdkKey, config.InitialDatafile(initDataFile),
82119
config.PollingInterval(pollingInterval))
83120
}
84121
}
85122

86123
// WithPollingConfigManagerRequester sets polling config manager on a client
87124
func WithPollingConfigManagerRequester(requester utils.Requester, pollingInterval time.Duration, initDataFile []byte) OptionFunc {
88-
return func(f *OptimizelyClient) {
89-
f.ConfigManager = config.NewPollingProjectConfigManager("", config.InitialDatafile(initDataFile),
125+
return func(f *OptimizelyFactory) {
126+
f.configManager = config.NewPollingProjectConfigManager("", config.InitialDatafile(initDataFile),
90127
config.PollingInterval(pollingInterval), config.Requester(requester))
91128
}
92129
}
93130

94131
// WithConfigManager sets polling config manager on a client
95132
func WithConfigManager(configManager pkg.ProjectConfigManager) OptionFunc {
96-
return func(f *OptimizelyClient) {
97-
f.ConfigManager = configManager
133+
return func(f *OptimizelyFactory) {
134+
f.configManager = configManager
98135
}
99136
}
100137

101138
// WithCompositeDecisionService sets decision service on a client
102139
func WithCompositeDecisionService(sdkKey string) OptionFunc {
103-
return func(f *OptimizelyClient) {
104-
f.DecisionService = decision.NewCompositeService(sdkKey)
140+
return func(f *OptimizelyFactory) {
141+
f.decisionService = decision.NewCompositeService(sdkKey)
105142
}
106143
}
107144

108145
// WithDecisionService sets decision service on a client
109146
func WithDecisionService(decisionService decision.Service) OptionFunc {
110-
return func(f *OptimizelyClient) {
111-
f.DecisionService = decisionService
147+
return func(f *OptimizelyFactory) {
148+
f.decisionService = decisionService
149+
}
150+
}
151+
152+
// WithUserProfileService sets the user profile service on the decision service
153+
func WithUserProfileService(userProfileService decision.UserProfileService) OptionFunc {
154+
return func(f *OptimizelyFactory) {
155+
f.userProfileService = userProfileService
156+
}
157+
}
158+
159+
// WithExperimentOverrides sets the experiment override store on the decision service
160+
func WithExperimentOverrides(overrideStore decision.ExperimentOverrideStore) OptionFunc {
161+
return func(f *OptimizelyFactory) {
162+
f.overrideStore = overrideStore
112163
}
113164
}
114165

115166
// WithBatchEventProcessor sets event processor on a client
116167
func WithBatchEventProcessor(batchSize, queueSize int, flushInterval time.Duration) OptionFunc {
117-
return func(f *OptimizelyClient) {
118-
f.EventProcessor = event.NewBatchEventProcessor(event.WithBatchSize(batchSize),
168+
return func(f *OptimizelyFactory) {
169+
f.eventProcessor = event.NewBatchEventProcessor(event.WithBatchSize(batchSize),
119170
event.WithQueueSize(queueSize), event.WithFlushInterval(flushInterval))
120171
}
121172
}
122173

123174
// WithEventProcessor sets event processor on a client
124175
func WithEventProcessor(eventProcessor event.Processor) OptionFunc {
125-
return func(f *OptimizelyClient) {
126-
f.EventProcessor = eventProcessor
176+
return func(f *OptimizelyFactory) {
177+
f.eventProcessor = eventProcessor
127178
}
128179
}
129180

130181
// WithExecutionContext allows user to pass in their own execution context to override the default one in the client
131182
func WithExecutionContext(executionContext utils.ExecutionCtx) OptionFunc {
132-
return func(f *OptimizelyClient) {
183+
return func(f *OptimizelyFactory) {
133184
f.executionCtx = executionContext
134185
}
135186
}

pkg/client/factory_test.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
"github.com/optimizely/go-sdk/pkg/config"
2929
"github.com/optimizely/go-sdk/pkg/config/datafileprojectconfig"
30+
"github.com/optimizely/go-sdk/pkg/decision"
3031
"github.com/optimizely/go-sdk/pkg/event"
3132
"github.com/optimizely/go-sdk/pkg/utils"
3233

@@ -111,7 +112,7 @@ func TestClientWithDecisionServiceAndEventProcessorInOptions(t *testing.T) {
111112
MaxQueueSize: 100,
112113
FlushInterval: 100,
113114
Q: event.NewInMemoryQueue(100),
114-
EventDispatcher: &MockDispatcher{Events:[]event.LogEvent{}},
115+
EventDispatcher: &MockDispatcher{Events: []event.LogEvent{}},
115116
}
116117

117118
optimizelyClient, err := factory.Client(WithConfigManager(configManager), WithDecisionService(decisionService), WithEventProcessor(processor))
@@ -145,3 +146,16 @@ func TestStaticClient(t *testing.T) {
145146
assert.Error(t, err)
146147
assert.Nil(t, optlyClient)
147148
}
149+
150+
func TestClientWithCustomDecisionServiceOptions(t *testing.T) {
151+
factory := OptimizelyFactory{SDKKey: "1212"}
152+
153+
mockUserProfileService := new(MockUserProfileService)
154+
mockOverrideStore := new(decision.MapOverridesStore)
155+
optimizelyClient, err := factory.Client(
156+
WithUserProfileService(mockUserProfileService),
157+
WithExperimentOverrides(mockOverrideStore),
158+
)
159+
assert.NoError(t, err)
160+
assert.NotNil(t, optimizelyClient.DecisionService)
161+
}

pkg/client/fixtures_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ func (m *PanickingDecisionService) RemoveOnDecision(id int) error {
151151
panic("I'm panicking")
152152
}
153153

154+
type MockUserProfileService struct {
155+
decision.UserProfileService
156+
mock.Mock
157+
}
158+
154159
// Helper methods for creating test entities
155160
func makeTestExperiment(experimentKey string) entities.Experiment {
156161
return entities.Experiment{

pkg/decision/composite_experiment_service.go

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,60 @@ import (
2727

2828
var ceLogger = logging.GetLogger("CompositeExperimentService")
2929

30+
// CESOptionFunc is used to assign optional configuration options
31+
type CESOptionFunc func(*CompositeExperimentService)
32+
33+
// WithUserProfileService adds a user profile service
34+
func WithUserProfileService(userProfileService UserProfileService) CESOptionFunc {
35+
return func(f *CompositeExperimentService) {
36+
f.userProfileService = userProfileService
37+
}
38+
}
39+
40+
// WithOverrideStore adds an experiment override store
41+
func WithOverrideStore(overrideStore ExperimentOverrideStore) CESOptionFunc {
42+
return func(f *CompositeExperimentService) {
43+
f.overrideStore = overrideStore
44+
}
45+
}
46+
3047
// CompositeExperimentService bridges together the various experiment decision services that ship by default with the SDK
3148
type CompositeExperimentService struct {
3249
experimentServices []ExperimentService
50+
overrideStore ExperimentOverrideStore
51+
userProfileService UserProfileService
3352
}
3453

3554
// NewCompositeExperimentService creates a new instance of the CompositeExperimentService
36-
func NewCompositeExperimentService() *CompositeExperimentService {
55+
func NewCompositeExperimentService(options ...CESOptionFunc) *CompositeExperimentService {
3756
// These decision services are applied in order:
38-
// 1. Whitelist
39-
// 2. Bucketing
40-
// @TODO(mng): Prepend forced variation
41-
return &CompositeExperimentService{
42-
experimentServices: []ExperimentService{
43-
NewExperimentWhitelistService(),
44-
NewExperimentBucketerService(),
45-
},
57+
// 1. Overrides (if supplied)
58+
// 2. Whitelist
59+
// 3. Bucketing (with User profile integration if supplied)
60+
compositeExperimentService := &CompositeExperimentService{}
61+
for _, opt := range options {
62+
opt(compositeExperimentService)
4663
}
64+
experimentServices := []ExperimentService{
65+
NewExperimentWhitelistService(),
66+
}
67+
68+
// Prepend overrides if supplied
69+
if compositeExperimentService.overrideStore != nil {
70+
overrideService := NewExperimentOverrideService(compositeExperimentService.overrideStore)
71+
experimentServices = append([]ExperimentService{overrideService}, experimentServices...)
72+
}
73+
74+
experimentBucketerService := NewExperimentBucketerService()
75+
if compositeExperimentService.userProfileService != nil {
76+
persistingExperimentService := NewPersistingExperimentService(experimentBucketerService, compositeExperimentService.userProfileService)
77+
experimentServices = append(experimentServices, persistingExperimentService)
78+
} else {
79+
experimentServices = append(experimentServices, experimentBucketerService)
80+
}
81+
compositeExperimentService.experimentServices = experimentServices
82+
83+
return compositeExperimentService
4784
}
4885

4986
// GetDecision returns a decision for the given experiment and user context

pkg/decision/composite_experiment_service_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,17 @@ func (s *CompositeExperimentTestSuite) TestNewCompositeExperimentService() {
158158
s.IsType(&ExperimentBucketerService{}, compositeExperimentService.experimentServices[1])
159159
}
160160

161+
func (s *CompositeExperimentTestSuite) TestNewCompositeExperimentServiceWithCustomOptions() {
162+
mockUserProfileService := new(MockUserProfileService)
163+
mockExperimentOverrideStore := new(MapOverridesStore)
164+
compositeExperimentService := NewCompositeExperimentService(
165+
WithUserProfileService(mockUserProfileService),
166+
WithOverrideStore(mockExperimentOverrideStore),
167+
)
168+
s.Equal(mockUserProfileService, compositeExperimentService.userProfileService)
169+
s.Equal(mockExperimentOverrideStore, compositeExperimentService.overrideStore)
170+
}
171+
161172
func TestCompositeExperimentTestSuite(t *testing.T) {
162173
suite.Run(t, new(CompositeExperimentTestSuite))
163174
}

pkg/decision/composite_service.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,32 @@ type CompositeService struct {
3535
notificationCenter notification.Center
3636
}
3737

38+
// CSOptionFunc is used to pass config options into the CompositeService
39+
type CSOptionFunc func(*CompositeService)
40+
41+
// WithCompositeExperimentService sets the composite experiment service on the CompositeService
42+
func WithCompositeExperimentService(compositeExperimentService ExperimentService) CSOptionFunc {
43+
return func(f *CompositeService) {
44+
f.compositeExperimentService = compositeExperimentService
45+
}
46+
}
47+
3848
// NewCompositeService returns a new instance of the CompositeService with the defaults
39-
func NewCompositeService(sdkKey string) *CompositeService {
40-
// @TODO: add factory method with option funcs to accept custom feature and experiment services
41-
compositeExperimentService := NewCompositeExperimentService()
42-
compositeFeatureDecisionService := NewCompositeFeatureService(compositeExperimentService)
43-
return &CompositeService{
44-
compositeExperimentService: compositeExperimentService,
45-
compositeFeatureService: compositeFeatureDecisionService,
46-
notificationCenter: registry.GetNotificationCenter(sdkKey),
49+
func NewCompositeService(sdkKey string, options ...CSOptionFunc) *CompositeService {
50+
compositeService := &CompositeService{
51+
notificationCenter: registry.GetNotificationCenter(sdkKey),
4752
}
53+
54+
for _, opts := range options {
55+
opts(compositeService)
56+
}
57+
58+
if compositeService.compositeExperimentService == nil {
59+
compositeService.compositeExperimentService = NewCompositeExperimentService()
60+
}
61+
compositeService.compositeFeatureService = NewCompositeFeatureService(compositeService.compositeExperimentService)
62+
63+
return compositeService
4864
}
4965

5066
// GetFeatureDecision returns a decision for the given feature key

pkg/decision/composite_service_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ func (s *CompositeServiceFeatureTestSuite) TestNewCompositeService() {
9898
s.IsType(&CompositeFeatureService{}, compositeService.compositeFeatureService)
9999
}
100100

101+
func (s *CompositeServiceFeatureTestSuite) TestNewCompositeServiceWithCustomOptions() {
102+
compositeExperimentService := NewCompositeExperimentService()
103+
compositeService := NewCompositeService("sdk_key", WithCompositeExperimentService(compositeExperimentService))
104+
s.IsType(compositeExperimentService, compositeService.compositeExperimentService)
105+
s.IsType(&CompositeFeatureService{}, compositeService.compositeFeatureService)
106+
}
107+
101108
type CompositeServiceExperimentTestSuite struct {
102109
suite.Suite
103110
decisionContext ExperimentDecisionContext

pkg/decision/helpers_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,20 @@ type MockAudienceTreeEvaluator struct {
7575
mock.Mock
7676
}
7777

78+
type MockUserProfileService struct {
79+
UserProfileService
80+
mock.Mock
81+
}
82+
83+
func (m *MockUserProfileService) Lookup(userID string) UserProfile {
84+
args := m.Called(userID)
85+
return args.Get(0).(UserProfile)
86+
}
87+
88+
func (m *MockUserProfileService) Save(userProfile UserProfile) {
89+
m.Called(userProfile)
90+
}
91+
7892
func (m *MockAudienceTreeEvaluator) Evaluate(node *entities.TreeNode, condTreeParams *entities.TreeParameters) bool {
7993
args := m.Called(node, condTreeParams)
8094
return args.Bool(0)

0 commit comments

Comments
 (0)