Skip to content

Commit 34fbd4d

Browse files
rashidspmikeproeng37
authored andcommitted
Feature/event listener (#76)
1 parent fc5731e commit 34fbd4d

File tree

7 files changed

+769
-90
lines changed

7 files changed

+769
-90
lines changed

lib/optimizely.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
require_relative 'optimizely/helpers/validator'
2525
require_relative 'optimizely/helpers/variable_type'
2626
require_relative 'optimizely/logger'
27+
require_relative 'optimizely/notification_center'
2728
require_relative 'optimizely/project_config'
2829

2930
module Optimizely
@@ -38,6 +39,7 @@ class Project
3839
attr_reader :event_builder
3940
attr_reader :event_dispatcher
4041
attr_reader :logger
42+
attr_reader :notification_center
4143

4244
def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = nil, skip_json_validation = false, user_profile_service = nil)
4345
# Constructor for Projects.
@@ -83,6 +85,7 @@ def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = n
8385

8486
@decision_service = DecisionService.new(@config, @user_profile_service)
8587
@event_builder = EventBuilder.new(@config)
88+
@notification_center = NotificationCenter.new(@logger, @error_handler)
8689
end
8790

8891
def activate(experiment_key, user_id, attributes = nil)
@@ -231,6 +234,10 @@ def track(event_key, user_id, attributes = nil, event_tags = nil)
231234
rescue => e
232235
@logger.log(Logger::ERROR, "Unable to dispatch conversion event. Error: #{e}")
233236
end
237+
@notification_center.send_notifications(
238+
NotificationCenter::NOTIFICATION_TYPES[:TRACK],
239+
event_key, user_id, attributes, event_tags, conversion_event
240+
)
234241
end
235242

236243
def is_feature_enabled(feature_flag_key, user_id, attributes = nil)
@@ -525,6 +532,11 @@ def send_impression(experiment, variation_key, user_id, attributes = nil)
525532
rescue => e
526533
@logger.log(Logger::ERROR, "Unable to dispatch impression event. Error: #{e}")
527534
end
535+
variation = @config.get_variation_from_id(experiment_key, variation_id)
536+
@notification_center.send_notifications(
537+
NotificationCenter::NOTIFICATION_TYPES[:ACTIVATE],
538+
experiment,user_id, attributes, variation, impression_event
539+
)
528540
end
529541
end
530542
end

lib/optimizely/decision_service.rb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ def get_variation(experiment_key, user_id, attributes = nil)
5151
# user_id - String ID for user
5252
# attributes - Hash representing user attributes
5353
#
54-
# Returns variation ID where visitor will be bucketed (nil if experiment is inactive or user does not meet audience conditions)
54+
# Returns variation ID where visitor will be bucketed
55+
# (nil if experiment is inactive or user does not meet audience conditions)
5556

5657
# By default, the bucketing ID should be the user ID
5758
bucketing_id = get_bucketing_id(user_id, attributes)
@@ -122,10 +123,8 @@ def get_variation_for_feature(feature_flag, user_id, attributes = nil)
122123
Logger::INFO,
123124
"User '#{user_id}' is bucketed into a rollout for feature flag '#{feature_flag_key}'."
124125
)
125-
126126
return decision
127127
end
128-
129128
@config.logger.log(
130129
Logger::INFO,
131130
"User '#{user_id}' is not bucketed into a rollout for feature flag '#{feature_flag_key}'."
@@ -192,7 +191,6 @@ def get_variation_for_feature_rollout(feature_flag, user_id, attributes = nil)
192191
# attributes - Hash representing user attributes
193192
#
194193
# Returns the Decision struct or nil if not bucketed into any of the targeting rules
195-
196194
bucketing_id = get_bucketing_id(user_id, attributes)
197195
rollout_id = feature_flag['rolloutId']
198196
if rollout_id.nil? || rollout_id.empty?
@@ -262,7 +260,6 @@ def get_variation_for_feature_rollout(feature_flag, user_id, attributes = nil)
262260
Logger::DEBUG,
263261
"User '#{user_id}' was excluded from the 'Everyone Else' rule for feature flag"
264262
)
265-
266263
nil
267264
end
268265

@@ -371,7 +368,6 @@ def get_bucketing_id(user_id, attributes)
371368
#
372369
# user_id - String user ID
373370
# attributes - Hash user attributes
374-
375371
# By default, the bucketing ID should be the user ID
376372
bucketing_id = user_id
377373

lib/optimizely/exceptions.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,13 @@ def initialize(type)
9595
super("Provided #{type} is in an invalid format.")
9696
end
9797
end
98+
99+
class InvalidNotificationType < Error
100+
# Raised when an invalid notification type is provided
101+
102+
def initialize(msg = 'Provided notification type is invalid.')
103+
super
104+
end
105+
end
106+
98107
end

lib/optimizely/notification_center.rb

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
#
2+
# Copyright 2017, Optimizely 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+
module Optimizely
17+
class NotificationCenter
18+
attr_reader :notifications
19+
attr_reader :notification_id
20+
21+
NOTIFICATION_TYPES = {
22+
ACTIVATE: 'ACTIVATE: experiment, user_id, attributes, variation, event',
23+
TRACK: 'TRACK: event_key, user_id, attributes, event_tags, event'
24+
}.freeze
25+
26+
def initialize(logger, error_handler)
27+
@notification_id = 1
28+
@notifications = {}
29+
NOTIFICATION_TYPES.values.each { |value| @notifications[value] = [] }
30+
@logger = logger
31+
@error_handler = error_handler
32+
end
33+
34+
def add_notification_listener(notification_type, notification_callback)
35+
# Adds notification callback to the notification center
36+
37+
# Args:
38+
# notification_type: one of the constants in NOTIFICATION_TYPES
39+
# notification_callback: function to call when the event is sent
40+
41+
# Returns:
42+
# notification ID used to remove the notification
43+
44+
return nil unless notification_type_valid?(notification_type)
45+
46+
unless notification_callback
47+
@logger.log Logger::ERROR, 'Callback can not be empty.'
48+
return nil
49+
end
50+
51+
unless notification_callback.is_a? Method
52+
@logger.log Logger::ERROR, 'Invalid notification callback given.'
53+
return nil
54+
end
55+
56+
@notifications[notification_type].each do |notification|
57+
return -1 if notification[:callback] == notification_callback
58+
end
59+
@notifications[notification_type].push(notification_id: @notification_id, callback: notification_callback)
60+
notification_id = @notification_id
61+
@notification_id += 1
62+
notification_id
63+
end
64+
65+
def remove_notification_listener(notification_id)
66+
# Removes previously added notification callback
67+
68+
# Args:
69+
# notification_id:
70+
# Returns:
71+
# The function returns true if found and removed, false otherwise
72+
unless notification_id
73+
@logger.log Logger::ERROR, 'Notification ID can not be empty.'
74+
return nil
75+
end
76+
@notifications.each do |key, _array|
77+
@notifications[key].each do |notification|
78+
if notification_id == notification[:notification_id]
79+
@notifications[key].delete(notification_id: notification_id, callback: notification[:callback])
80+
return true
81+
end
82+
end
83+
end
84+
false
85+
end
86+
87+
def clear_notifications(notification_type)
88+
# Removes notifications for a certain notification type
89+
#
90+
# Args:
91+
# notification_type: one of the constants in NOTIFICATION_TYPES
92+
93+
return nil unless notification_type_valid?(notification_type)
94+
95+
@notifications[notification_type] = []
96+
@logger.log Logger::INFO, "All callbacks for notification type #{notification_type} have been removed."
97+
end
98+
99+
def clean_all_notifications
100+
# Removes all notifications
101+
@notifications.keys.each { |key| @notifications[key] = [] }
102+
end
103+
104+
def send_notifications(notification_type, *args)
105+
# Sends off the notification for the specific event. Uses var args to pass in a
106+
# arbitrary list of parameters according to which notification type was sent
107+
108+
# Args:
109+
# notification_type: one of the constants in NOTIFICATION_TYPES
110+
# args: list of arguments to the callback
111+
return nil unless notification_type_valid?(notification_type)
112+
113+
@notifications[notification_type].each do |notification|
114+
begin
115+
notification_callback = notification[:callback]
116+
notification_callback.call(*args)
117+
@logger.log Logger::INFO, "Notification #{notification_type} sent successfully."
118+
rescue => e
119+
@logger.log(Logger::ERROR, "Problem calling notify callback. Error: #{e}")
120+
return nil
121+
end
122+
end
123+
end
124+
125+
private
126+
127+
def notification_type_valid?(notification_type)
128+
# Validates notification type
129+
130+
# Args:
131+
# notification_type: one of the constants in NOTIFICATION_TYPES
132+
133+
# Returns true if notification_type is valid, false otherwise
134+
135+
unless notification_type
136+
@logger.log Logger::ERROR, 'Notification type can not be empty.'
137+
return false
138+
end
139+
140+
unless @notifications.include?(notification_type)
141+
@logger.log Logger::ERROR, 'Invalid notification type.'
142+
@error_handler.handle_error InvalidNotificationType
143+
return false
144+
end
145+
true
146+
end
147+
end
148+
end

0 commit comments

Comments
 (0)