Skip to content

Commit c98a162

Browse files
Feature/event listener (optimizely#88)
* first pass at event listener * added event listener access methods * added two tests * get event listener to actually be called * tests for add_event_listener and calling * tests for add, remove, clear listener * update listener unit tests * finished unit tests for event listeners * reformat python code * comply with python 3.4 * add test for invalid listener * added event to on activate call as well as on track event * initial implementation of notification center * update comments * added logging and exception handling * added feature accessed callback. * added feature access callback and tests * updated after talking to Ali and Josh about rollouts and experiments. I definitely agree with activate and track being called the same. * make changes to work with both python 3 and 2 * check parameters of callbacks in non-strongly typed languages * use builtin str for python 2 and 3 * add testing of notification listener parameter types * remove FEATURE_ROLLOUT and FEATURE_EXPERIMENT
1 parent ebb0ad7 commit c98a162

File tree

4 files changed

+638
-197
lines changed

4 files changed

+638
-197
lines changed

optimizely/helpers/enums.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,16 @@ class Errors(object):
4141
INVALID_VARIATION_ERROR = 'Provided variation is not in datafile.'
4242
UNSUPPORTED_DATAFILE_VERSION = 'Provided datafile has unsupported version. ' \
4343
'Please use SDK version 1.1.0 or earlier for datafile version 1.'
44+
45+
46+
class NotificationTypes(object):
47+
""" NotificationTypes for the notification_center.NotificationCenter
48+
format is NOTIFICATION TYPE: list of parameters to callback.
49+
50+
ACTIVATE notification listener has the following parameters:
51+
Experiment experiment, str user_id, dict attributes (can be None), Variation variation, Event event
52+
TRACK notification listener has the following parameters:
53+
str event_key, str user_id, dict attributes (can be None), event_tags (can be None), Event event
54+
"""
55+
ACTIVATE = "ACTIVATE:experiment, user_id, attributes, variation, event"
56+
TRACK = "TRACK:event_key, user_id, attributes, event_tags, event"

optimizely/notification_center.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Copyright 2017, Optimizely
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
import sys
14+
15+
from functools import reduce
16+
from .helpers import enums
17+
18+
19+
class NotificationCenter(object):
20+
""" Class encapsulating Broadcast Notifications. The enums.NotifcationTypes includes predefined notifications."""
21+
22+
def __init__(self, logger):
23+
self.notification_id = 1
24+
self.notifications = {}
25+
for (attr, value) in enums.NotificationTypes.__dict__.items():
26+
self.notifications[value] = []
27+
self.logger = logger
28+
29+
def add_notification_listener(self, notification_type, notification_callback):
30+
""" Add a notification callback to the notification center.
31+
32+
Args:
33+
notification_type: A string representing the notification type from .helpers.enums.NotificationTypes
34+
notification_callback: closure of function to call when event is triggered.
35+
36+
Returns:
37+
Integer notification id used to remove the notification or -1 if the notification has already been added.
38+
"""
39+
40+
if notification_type not in self.notifications:
41+
self.notifications[notification_type] = [(self.notification_id, notification_callback)]
42+
else:
43+
if reduce(lambda a, b: a + 1,
44+
filter(lambda tup: tup[1] == notification_callback, self.notifications[notification_type]),
45+
0) > 0:
46+
return -1
47+
self.notifications[notification_type].append((self.notification_id, notification_callback))
48+
49+
ret_val = self.notification_id
50+
51+
self.notification_id += 1
52+
53+
return ret_val
54+
55+
def remove_notification_listener(self, notification_id):
56+
""" Remove a previously added notification callback.
57+
58+
Args:
59+
notification_id: The numeric id passed back from add_notification_listener
60+
61+
Returns:
62+
The function returns boolean true if found and removed, false otherwise.
63+
"""
64+
65+
for v in self.notifications.values():
66+
toRemove = list(filter(lambda tup: tup[0] == notification_id, v))
67+
if len(toRemove) > 0:
68+
v.remove(toRemove[0])
69+
return True
70+
71+
return False
72+
73+
def clear_all_notifications(self):
74+
""" Remove all notifications """
75+
for key in self.notifications.keys():
76+
self.notifications[key] = []
77+
78+
def clear_notifications(self, notification_type):
79+
""" Remove notifications for a certain notification type
80+
81+
Args:
82+
notification_type: key to the list of notifications .helpers.enums.NotificationTypes
83+
"""
84+
85+
self.notifications[notification_type] = []
86+
87+
def send_notifications(self, notification_type, *args):
88+
""" Fires off the notification for the specific event. Uses var args to pass in a
89+
arbitrary list of parameter according to which notification type was fired.
90+
91+
Args:
92+
notification_type: Type of notification to fire (String from .helpers.enums.NotificationTypes)
93+
args: variable list of arguments to the callback.
94+
"""
95+
96+
if notification_type in self.notifications:
97+
for notification_id, callback in self.notifications[notification_type]:
98+
try:
99+
callback(*args)
100+
except:
101+
error = sys.exc_info()[1]
102+
self.logger.log(enums.LogLevels.ERROR, 'Problem calling notify callback. Error: %s' % str(error))

optimizely/optimizely.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .helpers import validator
2626
from .logger import NoOpLogger as noop_logger
2727
from .logger import SimpleLogger
28+
from .notification_center import NotificationCenter as notification_center
2829

2930

3031
class Optimizely(object):
@@ -80,6 +81,7 @@ def __init__(self,
8081

8182
self.event_builder = event_builder.EventBuilder(self.config)
8283
self.decision_service = decision_service.DecisionService(self.config, user_profile_service)
84+
self.notification_center = notification_center(self.logger)
8385

8486
def _validate_instantiation_options(self, datafile, skip_json_validation):
8587
""" Helper method to validate all instantiation parameters.
@@ -177,6 +179,8 @@ def _send_impression_event(self, experiment, variation, user_id, attributes):
177179
except:
178180
error = sys.exc_info()[1]
179181
self.logger.log(enums.LogLevels.ERROR, 'Unable to dispatch impression event. Error: %s' % str(error))
182+
self.notification_center.send_notifications(enums.NotificationTypes.ACTIVATE,
183+
experiment, user_id, attributes, variation, impression_event)
180184

181185
def _get_feature_variable_for_type(self, feature_key, variable_key, variable_type, user_id, attributes):
182186
""" Helper method to determine value for a certain variable attached to a feature flag based on type of variable.
@@ -316,7 +320,8 @@ def track(self, event_key, user_id, attributes=None, event_tags=None):
316320
except:
317321
error = sys.exc_info()[1]
318322
self.logger.log(enums.LogLevels.ERROR, 'Unable to dispatch conversion event. Error: %s' % str(error))
319-
323+
self.notification_center.send_notifications(enums.NotificationTypes.TRACK, event_key, user_id,
324+
attributes, event_tags, conversion_event)
320325
else:
321326
self.logger.log(enums.LogLevels.INFO, 'There are no valid experiments for event "%s" to track.' % event_key)
322327

@@ -383,6 +388,7 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None):
383388
decision.variation,
384389
user_id,
385390
attributes)
391+
386392
return True
387393

388394
self.logger.log(enums.LogLevels.INFO, 'Feature "%s" is not enabled for user "%s".' % (feature_key, user_id))
@@ -504,12 +510,12 @@ def set_forced_variation(self, experiment_key, user_id, variation_key):
504510
def get_forced_variation(self, experiment_key, user_id):
505511
""" Gets the forced variation for a given user and experiment.
506512
507-
Args:
508-
experiment_key: A string key identifying the experiment.
509-
user_id: The user ID.
513+
Args:
514+
experiment_key: A string key identifying the experiment.
515+
user_id: The user ID.
510516
511-
Returns:
512-
The forced variation key. None if no forced variation key.
517+
Returns:
518+
The forced variation key. None if no forced variation key.
513519
"""
514520

515521
forced_variation = self.config.get_forced_variation(experiment_key, user_id)

0 commit comments

Comments
 (0)