Skip to content

Commit 183ff36

Browse files
author
Michael Ng
authored
feat(notifications): Add notifications for decisions. (#50)
1 parent 1e30183 commit 183ff36

File tree

10 files changed

+306
-6
lines changed

10 files changed

+306
-6
lines changed

optimizely/client/client_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ func (p *MockProjectConfigManager) GetConfig() optimizely.ProjectConfig {
4848
}
4949

5050
type MockDecisionService struct {
51+
decision.DecisionService
5152
mock.Mock
5253
}
5354

optimizely/client/factory.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121
"errors"
2222
"fmt"
2323

24+
"github.com/optimizely/go-sdk/optimizely/notification"
25+
2426
"github.com/optimizely/go-sdk/optimizely"
2527
"github.com/optimizely/go-sdk/optimizely/config"
2628
"github.com/optimizely/go-sdk/optimizely/decision"
@@ -88,6 +90,8 @@ func (f OptimizelyFactory) ClientWithOptions(clientOptions Options) (*Optimizely
8890
client.cancelFunc = cancel
8991
}
9092

93+
notificationCenter := notification.NewNotificationCenter()
94+
9195
if clientOptions.ProjectConfigManager != nil {
9296
client.configManager = clientOptions.ProjectConfigManager
9397
} else if f.SDKKey != "" {
@@ -102,7 +106,7 @@ func (f OptimizelyFactory) ClientWithOptions(clientOptions Options) (*Optimizely
102106
}
103107

104108
// @TODO: allow decision service to be passed in via options
105-
client.decisionService = decision.NewCompositeService()
109+
client.decisionService = decision.NewCompositeService(notificationCenter)
106110
client.isValid = true
107111
return client, nil
108112
}

optimizely/decision/composite_service.go

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,39 +17,74 @@
1717
package decision
1818

1919
import (
20+
"fmt"
21+
2022
"github.com/optimizely/go-sdk/optimizely/entities"
23+
"github.com/optimizely/go-sdk/optimizely/logging"
24+
"github.com/optimizely/go-sdk/optimizely/notification"
2125
)
2226

27+
var csLogger = logging.GetLogger("CompositeDecisionService")
28+
2329
// CompositeService is the entrypoint into the decision service. It provides out of the box decision making for Features and Experiments.
2430
type CompositeService struct {
2531
experimentDecisionServices []ExperimentDecisionService
2632
featureDecisionServices []FeatureDecisionService
33+
notificationCenter notification.Center
2734
}
2835

2936
// NewCompositeService returns a new instance of the DefeaultDecisionEngine
30-
func NewCompositeService() *CompositeService {
37+
func NewCompositeService(notificationCenter notification.Center) *CompositeService {
3138
featureDecisionService := NewCompositeFeatureService()
3239
return &CompositeService{
3340
featureDecisionServices: []FeatureDecisionService{featureDecisionService},
41+
notificationCenter: notificationCenter,
3442
}
3543
}
3644

3745
// GetFeatureDecision returns a decision for the given feature key
3846
func (s CompositeService) GetFeatureDecision(featureDecisionContext FeatureDecisionContext, userContext entities.UserContext) (FeatureDecision, error) {
3947
var featureDecision FeatureDecision
40-
48+
var err error
4149
// loop through the different features decision services until we get a decision
4250
for _, decisionService := range s.featureDecisionServices {
43-
featureDecision, err := decisionService.GetDecision(featureDecisionContext, userContext)
51+
featureDecision, err = decisionService.GetDecision(featureDecisionContext, userContext)
4452
if err != nil {
4553
// @TODO: log error
4654
}
4755

4856
if featureDecision.DecisionMade {
49-
return featureDecision, err
57+
break
5058
}
5159
}
5260

5361
// @TODO: add errors
54-
return featureDecision, nil
62+
if s.notificationCenter != nil {
63+
decisionInfo := map[string]interface{}{
64+
"feature": map[string]interface{}{
65+
"feature_key": featureDecisionContext.Feature.Key,
66+
"feature_enabled": featureDecision.Variation.FeatureEnabled,
67+
"source": featureDecision.Source,
68+
},
69+
}
70+
decisionNotification := notification.DecisionNotification{
71+
DecisionInfo: decisionInfo,
72+
Type: notification.Feature,
73+
UserContext: userContext,
74+
}
75+
s.notificationCenter.Send(notification.Decision, decisionNotification)
76+
}
77+
return featureDecision, err
78+
}
79+
80+
// OnDecision registers a handler for Decision notifications
81+
func (s CompositeService) OnDecision(callback func(notification.DecisionNotification)) {
82+
handler := func(payload interface{}) {
83+
if decisionNotification, ok := payload.(notification.DecisionNotification); ok {
84+
callback(decisionNotification)
85+
} else {
86+
csLogger.Warning(fmt.Sprintf("Unable to convert notification payload %v into DecisionNotification", payload))
87+
}
88+
}
89+
s.notificationCenter.AddHandler(notification.Decision, handler)
5590
}

optimizely/decision/interface.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ package decision
1818

1919
import (
2020
"github.com/optimizely/go-sdk/optimizely/entities"
21+
"github.com/optimizely/go-sdk/optimizely/notification"
2122
)
2223

2324
// DecisionService interface is used to make a decision for a given feature or experiment
2425
type DecisionService interface {
2526
GetFeatureDecision(FeatureDecisionContext, entities.UserContext) (FeatureDecision, error)
27+
OnDecision(func(notification.DecisionNotification))
2628
}
2729

2830
// ExperimentDecisionService can make a decision about an experiment

optimizely/notification/center.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package notification
18+
19+
import "fmt"
20+
21+
// Center handles all notification listeners. It keeps track of the Manager for each type of notification.
22+
type Center interface {
23+
AddHandler(Type, func(interface{})) (int, error)
24+
Send(Type, interface{}) error
25+
}
26+
27+
// DefaultCenter contains all the notification managers
28+
type DefaultCenter struct {
29+
managerMap map[Type]Manager
30+
}
31+
32+
// NewNotificationCenter returns a new notification center
33+
func NewNotificationCenter() *DefaultCenter {
34+
decisionNotificationManager := NewAtomicManager()
35+
managerMap := make(map[Type]Manager)
36+
managerMap[Decision] = decisionNotificationManager
37+
return &DefaultCenter{
38+
managerMap: managerMap,
39+
}
40+
}
41+
42+
// AddHandler adds a handler for the given notification type
43+
func (c *DefaultCenter) AddHandler(notificationType Type, handler func(interface{})) (int, error) {
44+
if manager, ok := c.managerMap[notificationType]; ok {
45+
return manager.AddHandler(handler)
46+
}
47+
48+
return -1, fmt.Errorf("No notification manager found for type %s", notificationType)
49+
}
50+
51+
// Send sends the given notification payload to all listeners of type
52+
func (c *DefaultCenter) Send(notificationType Type, notification interface{}) error {
53+
if manager, ok := c.managerMap[notificationType]; ok {
54+
manager.Send(notification)
55+
return nil
56+
}
57+
58+
return fmt.Errorf("No notification manager found for type %s", notificationType)
59+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package notification
2+
3+
import (
4+
"testing"
5+
6+
"github.com/optimizely/go-sdk/optimizely/entities"
7+
"github.com/stretchr/testify/mock"
8+
)
9+
10+
type MockReceiver struct {
11+
mock.Mock
12+
}
13+
14+
func (m *MockReceiver) handleNotification(notification interface{}) {
15+
m.Called(notification)
16+
}
17+
18+
func TestNotificationCenter(t *testing.T) {
19+
mockReceiver := new(MockReceiver)
20+
mockReceiver2 := new(MockReceiver)
21+
22+
testUser := entities.UserContext{}
23+
testDecisionNotification := DecisionNotification{
24+
Type: Feature,
25+
UserContext: testUser,
26+
DecisionInfo: map[string]interface{}{
27+
"feature": map[string]interface{}{
28+
"source": "Rollout",
29+
},
30+
},
31+
}
32+
mockReceiver.On("handleNotification", testDecisionNotification)
33+
mockReceiver2.On("handleNotification", testDecisionNotification)
34+
notificationCenter := NewNotificationCenter()
35+
notificationCenter.AddHandler(Decision, mockReceiver.handleNotification)
36+
notificationCenter.AddHandler(Decision, mockReceiver2.handleNotification)
37+
notificationCenter.Send(Decision, testDecisionNotification)
38+
39+
mockReceiver.AssertExpectations(t)
40+
mockReceiver2.AssertExpectations(t)
41+
}

optimizely/notification/entities.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package notification
18+
19+
import "github.com/optimizely/go-sdk/optimizely/entities"
20+
21+
// Type is the type of notification
22+
type Type string
23+
24+
const (
25+
// Decision notification type
26+
Decision Type = "decision"
27+
)
28+
29+
// DecisionNotificationType is the type of decision notification
30+
type DecisionNotificationType string
31+
32+
const (
33+
// Feature is used when the decision is returned as part of evaluating a feature
34+
Feature DecisionNotificationType = "feature"
35+
)
36+
37+
// DecisionNotification is a notification triggered when a decision is made for either a feature or an experiment
38+
type DecisionNotification struct {
39+
Type DecisionNotificationType
40+
UserContext entities.UserContext
41+
DecisionInfo map[string]interface{}
42+
}

optimizely/notification/handler.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package notification
18+
19+
// Handler is a generic interface for Optimizely notification listeners
20+
type Handler interface {
21+
handle(interface{})
22+
}

optimizely/notification/manager.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package notification
18+
19+
import (
20+
"sync/atomic"
21+
)
22+
23+
// Manager is a generic interface for managing notifications of a particular type
24+
type Manager interface {
25+
AddHandler(func(interface{})) (int, error)
26+
Send(message interface{})
27+
}
28+
29+
// AtomicManager adds handlers atomically
30+
type AtomicManager struct {
31+
handlers map[uint32]func(interface{})
32+
counter uint32
33+
}
34+
35+
// NewAtomicManager creates a new instance of the atomic manager
36+
func NewAtomicManager() *AtomicManager {
37+
return &AtomicManager{
38+
handlers: make(map[uint32]func(interface{})),
39+
}
40+
}
41+
42+
// AddHandler adds the given handler
43+
func (am *AtomicManager) AddHandler(newHandler func(interface{})) (int, error) {
44+
atomic.AddUint32(&am.counter, 1)
45+
am.handlers[am.counter] = newHandler
46+
return int(am.counter), nil
47+
}
48+
49+
// Send sends the notification to the registered handlers
50+
func (am *AtomicManager) Send(notification interface{}) {
51+
for _, handler := range am.handlers {
52+
handler(notification)
53+
}
54+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package notification
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/mock"
8+
)
9+
10+
type managerMockReceiver struct {
11+
mock.Mock
12+
}
13+
14+
func (m *managerMockReceiver) handle(notification interface{}) {
15+
m.Called(notification)
16+
}
17+
18+
func (m *managerMockReceiver) handleBetter(notification interface{}) {
19+
m.Called(notification)
20+
}
21+
22+
func TestAtomicManager(t *testing.T) {
23+
payload := map[string]interface{}{
24+
"key": "test",
25+
}
26+
27+
mockReceiver := new(managerMockReceiver)
28+
mockReceiver.On("handle", payload)
29+
mockReceiver.On("handleBetter", payload)
30+
31+
atomicManager := NewAtomicManager()
32+
result, _ := atomicManager.AddHandler(mockReceiver.handle)
33+
assert.Equal(t, 1, result)
34+
35+
result, _ = atomicManager.AddHandler(mockReceiver.handleBetter)
36+
assert.Equal(t, 2, result)
37+
38+
atomicManager.Send(payload)
39+
mockReceiver.AssertExpectations(t)
40+
}

0 commit comments

Comments
 (0)