Skip to content

Commit 6f2b5fe

Browse files
authored
Add support for V2 endpoint; increment version to 0.1.2
1 parent 8329d4f commit 6f2b5fe

17 files changed

+1334
-107
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.idea/*
2+
pkg/*
23
/.bundle/
34
/.yardoc
45
Gemfile.lock

CHANGELOG

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
-------------------------------------------------------------------------------
2+
0.1.2
3+
* Add support for V2 datafile and event endpoint.
4+
* Change EventDispatcher / Event signature. The EventDispatcher's dispatch_event method now takes an Event with four properties: url (string URL to dispatch the Event to), params (Hash of params to send), http_verb (either :get or :post), and headers (Hash of headers to send with the request).
5+
-------------------------------------------------------------------------------
6+
17
-------------------------------------------------------------------------------
28
0.1.1
39
* Add option to skip JSON schema validation of datafile.

lib/optimizely.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ class Project
1818
attr_accessor :logger
1919
attr_accessor :error_handler
2020

21+
EVENT_BUILDERS_BY_VERSION = {
22+
Optimizely::V1_CONFIG_VERSION => EventBuilderV1,
23+
Optimizely::V2_CONFIG_VERSION => EventBuilderV2
24+
}
25+
2126
def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = nil, skip_json_validation = false)
2227
# Constructor for Projects.
2328
#
@@ -35,7 +40,7 @@ def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = n
3540

3641
@config = ProjectConfig.new(datafile, @logger, @error_handler)
3742
@bucketer = Bucketer.new(@config)
38-
@event_builder = EventBuilder.new(@config, @bucketer)
43+
@event_builder = EVENT_BUILDERS_BY_VERSION[@config.version].new(@config, @bucketer)
3944
end
4045

4146
def activate(experiment_key, user_id, attributes = nil)
@@ -70,7 +75,7 @@ def activate(experiment_key, user_id, attributes = nil)
7075
@logger.log(Logger::INFO,
7176
'Dispatching impression event to URL %s with params %s.' % [impression_event.url,
7277
impression_event.params])
73-
@event_dispatcher.dispatch_event(impression_event.url, impression_event.params)
78+
@event_dispatcher.dispatch_event(impression_event)
7479

7580
@config.get_variation_key_from_id(experiment_key, variation_id)
7681
end
@@ -136,7 +141,7 @@ def track(event_key, user_id, attributes = nil, event_value = nil)
136141
@logger.log(Logger::INFO,
137142
'Dispatching conversion event to URL %s with params %s.' % [conversion_event.url,
138143
conversion_event.params])
139-
@event_dispatcher.dispatch_event(conversion_event.url, conversion_event.params)
144+
@event_dispatcher.dispatch_event(conversion_event)
140145
end
141146

142147
private

lib/optimizely/event_builder.rb

Lines changed: 187 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,198 @@ module Optimizely
66
class Event
77
# Representation of an event which can be sent to the Optimizely logging endpoint.
88

9-
# Event API format
10-
OFFLINE_API_PATH = 'https://%{project_id}.log.optimizely.com/event'
9+
attr_reader :http_verb
10+
attr_reader :params
11+
attr_reader :url
12+
attr_reader :headers
13+
14+
def initialize(http_verb, url, params, headers)
15+
@http_verb = http_verb
16+
@url = url
17+
@params = params
18+
@headers = headers
19+
end
20+
21+
# Override equality operator to make two events with the same contents equal for testing purposes
22+
def ==(event)
23+
@http_verb == event.http_verb && @url == event.url && @params == event.params && @headers == event.headers
24+
end
25+
end
1126

12-
# Gets/Sets event params.
27+
class BaseEventBuilder
28+
attr_reader :config
29+
attr_reader :bucketer
1330
attr_accessor :params
1431

15-
def initialize(params)
16-
@params = params
32+
def initialize(config, bucketer)
33+
@config = config
34+
@bucketer = bucketer
35+
@params = {}
1736
end
1837

19-
def url
20-
# URL for sending impression/conversion event.
38+
private
39+
40+
def add_common_params(user_id, attributes)
41+
# Add params which are used in both conversion and impression events.
2142
#
22-
# project_id - ID for the project.
43+
# user_id - ID for user.
44+
# attributes - Hash representing user attributes and values which need to be recorded.
45+
46+
add_project_id
47+
add_account_id
48+
add_user_id(user_id)
49+
add_attributes(attributes)
50+
add_source
51+
add_time
52+
end
53+
end
54+
55+
class EventBuilderV2 < BaseEventBuilder
56+
CONVERSION_EVENT_ENDPOINT = 'https://p13nlog.dz.optimizely.com/log/event'
57+
IMPRESSION_EVENT_ENDPOINT = 'https://p13nlog.dz.optimizely.com/log/decision'
58+
POST_HEADERS = { 'Content-Type' => 'application/json' }
59+
60+
def create_impression_event(experiment_key, variation_id, user_id, attributes)
61+
# Create conversion Event to be sent to the logging endpoint.
62+
#
63+
# experiment_key - Experiment for which impression needs to be recorded.
64+
# variation_id - ID for variation which would be presented to user.
65+
# user_id - ID for user.
66+
# attributes - Hash representing user attributes and values which need to be recorded.
67+
#
68+
# Returns event hash encapsulating the impression event.
69+
70+
@params = {}
71+
add_common_params(user_id, attributes)
72+
add_decision(experiment_key, variation_id)
73+
add_attributes(attributes)
74+
Event.new(:post, IMPRESSION_EVENT_ENDPOINT, @params, POST_HEADERS)
75+
end
76+
77+
def create_conversion_event(event_key, user_id, attributes, event_value, experiment_keys)
78+
# Create conversion Event to be sent to the logging endpoint.
79+
#
80+
# event_key - Event key representing the event which needs to be recorded.
81+
# user_id - ID for user.
82+
# attributes - Hash representing user attributes and values which need to be recorded.
83+
# event_value - Value associated with the event. Can be used to represent revenue in cents.
84+
# experiment_keys - Array of valid experiment keys for the event
85+
#
86+
# Returns event hash encapsulating the conversion event.
87+
88+
@params = {}
89+
add_common_params(user_id, attributes)
90+
add_conversion_event(event_key, event_value)
91+
add_layer_states(user_id, experiment_keys)
92+
Event.new(:post, CONVERSION_EVENT_ENDPOINT, @params, POST_HEADERS)
93+
end
94+
95+
private
96+
97+
def add_common_params(user_id, attributes)
98+
super
99+
@params['isGlobalHoldback'] = false
100+
end
101+
102+
def add_project_id
103+
@params['projectId'] = @config.project_id
104+
end
105+
106+
def add_account_id
107+
@params['accountId'] = @config.account_id
108+
end
109+
110+
def add_user_id(user_id)
111+
@params['visitorId'] = user_id
112+
end
113+
114+
def add_attributes(attributes)
115+
@params['userFeatures'] = []
116+
117+
return if attributes.nil?
118+
119+
attributes.keys.each do |attribute_key|
120+
# Omit falsy attribute values
121+
attribute_value = attributes[attribute_key]
122+
next unless attribute_value
123+
124+
# Skip attributes not in the datafile
125+
attribute_id = @config.get_attribute_id(attribute_key)
126+
next unless attribute_id
127+
128+
feature = {
129+
'id' => attribute_id,
130+
'name' => attribute_key,
131+
'type' => 'custom',
132+
'value' => attribute_value,
133+
'shouldIndex' => true,
134+
}
135+
@params['userFeatures'].push(feature)
136+
end
137+
end
138+
139+
def add_decision(experiment_key, variation_id)
140+
experiment_id = @config.get_experiment_id(experiment_key)
141+
@params['layerId'] = @config.experiment_key_map[experiment_key]['layerId']
142+
@params['decision'] = {
143+
'variationId' => variation_id,
144+
'experimentId' => experiment_id,
145+
'isLayerHoldback' => false,
146+
}
147+
end
148+
149+
def add_conversion_event(event_key, event_value)
150+
# Add conversion event information to the event.
23151
#
24-
# Returns URL for event API.
152+
# event_key - Event key representing the event which needs to be recorded.
153+
# event_value - Value associated with the event. Can be used to represent revenue in cents.
154+
155+
event_id = @config.event_key_map[event_key]['id']
156+
event_name = @config.event_key_map[event_key]['key']
157+
158+
@params['eventEntityId'] = event_id
159+
@params['eventFeatures'] = []
160+
@params['eventName'] = event_name
161+
@params['eventMetrics'] = []
25162

26-
sprintf(OFFLINE_API_PATH, project_id: @params[Params::PROJECT_ID])
163+
if event_value
164+
@params['eventMetrics'].push({
165+
'name' => 'revenue',
166+
'value' => event_value,
167+
})
168+
end
169+
end
170+
171+
def add_layer_states(user_id, experiment_keys)
172+
@params['layerStates'] = []
173+
174+
experiment_keys.each do |experiment_key|
175+
variation_id = @bucketer.bucket(experiment_key, user_id)
176+
experiment_id = @config.experiment_key_map[experiment_key]['id']
177+
layer_state = {
178+
'layerId' => @config.experiment_key_map[experiment_key]['layerId'],
179+
'decision' => {
180+
'variationId' => variation_id,
181+
'experimentId' => experiment_id,
182+
'isLayerHoldback' => false,
183+
},
184+
'actionTriggered' => true,
185+
}
186+
@params['layerStates'].push(layer_state)
187+
end
188+
end
189+
190+
def add_source
191+
@params['clientEngine'] = 'ruby-sdk'
192+
@params['clientVersion'] = VERSION
193+
end
194+
195+
def add_time
196+
@params['timestamp'] = (Time.now.to_f * 1000).to_i
27197
end
28198
end
29199

30-
class EventBuilder
200+
class EventBuilderV1 < BaseEventBuilder
31201
# Class which encapsulates methods to build events for tracking impressions and conversions.
32202

33203
# Attribute mapping format
@@ -36,15 +206,8 @@ class EventBuilder
36206
# Experiment mapping format
37207
EXPERIMENT_PARAM_FORMAT = '%{experiment_prefix}%{experiment_id}'
38208

39-
attr_accessor :config
40-
attr_accessor :bucketer
41-
attr_accessor :params
42-
43-
def initialize(config, bucketer)
44-
@config = config
45-
@bucketer = bucketer
46-
@params = {}
47-
end
209+
# Event endpoint path
210+
OFFLINE_API_PATH = 'https://%{project_id}.log.optimizely.com/event'
48211

49212
def create_impression_event(experiment_key, variation_id, user_id, attributes)
50213
# Create conversion Event to be sent to the logging endpoint.
@@ -60,7 +223,7 @@ def create_impression_event(experiment_key, variation_id, user_id, attributes)
60223
add_common_params(user_id, attributes)
61224
add_impression_goal(experiment_key)
62225
add_experiment(experiment_key, variation_id)
63-
Event.new(@params)
226+
Event.new(:get, sprintf(OFFLINE_API_PATH, project_id: @params[Params::PROJECT_ID]), @params, {})
64227
end
65228

66229
def create_conversion_event(event_key, user_id, attributes, event_value, experiment_keys)
@@ -71,12 +234,14 @@ def create_conversion_event(event_key, user_id, attributes, event_value, experim
71234
# attributes - Hash representing user attributes and values which need to be recorded.
72235
# event_value - Value associated with the event. Can be used to represent revenue in cents.
73236
# experiment_keys - Array of valid experiment keys for the goal
237+
#
238+
# Returns event hash encapsulating the conversion event.
74239

75240
@params = {}
76241
add_common_params(user_id, attributes)
77242
add_conversion_goal(event_key, event_value)
78243
add_experiment_variation_params(user_id, experiment_keys)
79-
Event.new(@params)
244+
Event.new(:get, sprintf(OFFLINE_API_PATH, project_id: @params[Params::PROJECT_ID]), @params, {})
80245
end
81246

82247
private
@@ -128,20 +293,6 @@ def add_time
128293
@params[Params::TIME] = Time.now.strftime('%s').to_i
129294
end
130295

131-
def add_common_params(user_id, attributes)
132-
# Add params which are used same in both conversion and impression events.
133-
#
134-
# user_id - ID for user.
135-
# attributes - Hash representing user attributes and values which need to be recorded.
136-
137-
add_project_id
138-
add_account_id
139-
add_user_id(user_id)
140-
add_attributes(attributes)
141-
add_source
142-
add_time
143-
end
144-
145296
def add_impression_goal(experiment_key)
146297
# Add impression goal information to the event.
147298
#

lib/optimizely/event_dispatcher.rb

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,37 @@
11
require 'httparty'
22

33
module Optimizely
4-
class BaseEventDispatcher
5-
# Class encapsulating event dispatching functionality.
6-
# Override with your own EventDispatcher providing dispatch_event method.
7-
8-
def dispatch_event(_url, _params)
9-
end
10-
end
11-
12-
class NoOpEventDispatcher < BaseEventDispatcher
4+
class NoOpEventDispatcher
135
# Class providing dispatch_event method which does nothing.
146

15-
def dispatch_event(_url, _params)
7+
def dispatch_event(event)
168
end
179
end
1810

19-
class EventDispatcher < BaseEventDispatcher
11+
class EventDispatcher
2012
REQUEST_TIMEOUT = 10
2113

22-
def dispatch_event(url, params)
14+
def dispatch_event(event)
2315
# Dispatch the event being represented by the Event object.
2416
#
25-
# url - URL to send impression/conversion event to.
26-
# params - Params to be sent to the impression/conversion event.
17+
# event - Event object
2718

28-
HTTParty.get(url, query: params, timeout: REQUEST_TIMEOUT)
29-
rescue Timeout::Error => e
30-
return e
19+
if event.http_verb == :get
20+
begin
21+
HTTParty.get(event.url, headers: event.headers, query: event.params, timeout: REQUEST_TIMEOUT)
22+
rescue Timeout::Error => e
23+
return e
24+
end
25+
elsif event.http_verb == :post
26+
begin
27+
HTTParty.post(event.url,
28+
body: event.params.to_json,
29+
headers: event.headers,
30+
timeout: REQUEST_TIMEOUT)
31+
rescue Timeout::Error => e
32+
return e
33+
end
34+
end
3135
end
3236
end
3337
end

0 commit comments

Comments
 (0)