optimizely-sdk 0.0.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/optimizely.rb +179 -0
- data/lib/optimizely/audience.rb +37 -0
- data/lib/optimizely/bucketer.rb +143 -0
- data/lib/optimizely/condition.rb +118 -0
- data/lib/optimizely/error_handler.rb +24 -0
- data/lib/optimizely/event_builder.rb +200 -0
- data/lib/optimizely/event_dispatcher.rb +18 -0
- data/lib/optimizely/exceptions.rb +83 -0
- data/lib/optimizely/helpers/constants.rb +173 -0
- data/lib/optimizely/helpers/group.rb +14 -0
- data/lib/optimizely/helpers/validator.rb +60 -0
- data/lib/optimizely/logger.rb +30 -0
- data/lib/optimizely/params.rb +14 -0
- data/lib/optimizely/project_config.rb +255 -0
- data/lib/optimizely/version.rb +3 -0
- data/lib/start.rb +6 -0
- metadata +161 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
module Optimizely
|
2
|
+
class BaseErrorHandler
|
3
|
+
# Class encapsulating exception handling functionality.
|
4
|
+
# Override with your own exception handler providing a handle_error method.
|
5
|
+
|
6
|
+
def handle_error(_error)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class NoOpErrorHandler < BaseErrorHandler
|
11
|
+
# Class providing handle_error method that suppresses errors.
|
12
|
+
|
13
|
+
def handle_error(_error)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class RaiseErrorHandler < BaseErrorHandler
|
18
|
+
# Class providing a handle_error method that raises exceptions.
|
19
|
+
|
20
|
+
def handle_error(error)
|
21
|
+
raise error
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
require_relative './audience'
|
2
|
+
require_relative './params'
|
3
|
+
require_relative './version'
|
4
|
+
|
5
|
+
module Optimizely
|
6
|
+
class Event
|
7
|
+
# Representation of an event which can be sent to the Optimizely logging endpoint.
|
8
|
+
|
9
|
+
# Event API format
|
10
|
+
OFFLINE_API_PATH = 'https://%{project_id}.log.optimizely.com/event'
|
11
|
+
|
12
|
+
# Gets/Sets event params.
|
13
|
+
attr_accessor :params
|
14
|
+
|
15
|
+
def initialize(params)
|
16
|
+
@params = params
|
17
|
+
end
|
18
|
+
|
19
|
+
def url
|
20
|
+
# URL for sending impression/conversion event.
|
21
|
+
#
|
22
|
+
# project_id - ID for the project.
|
23
|
+
#
|
24
|
+
# Returns URL for event API.
|
25
|
+
|
26
|
+
sprintf(OFFLINE_API_PATH, project_id: @params[Params::PROJECT_ID])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class EventBuilder
|
31
|
+
# Class which encapsulates methods to build events for tracking impressions and conversions.
|
32
|
+
|
33
|
+
# Attribute mapping format
|
34
|
+
ATTRIBUTE_PARAM_FORMAT = '%{segment_prefix}%{segment_id}'
|
35
|
+
|
36
|
+
# Experiment mapping format
|
37
|
+
EXPERIMENT_PARAM_FORMAT = '%{experiment_prefix}%{experiment_id}'
|
38
|
+
|
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
|
48
|
+
|
49
|
+
def create_impression_event(experiment_key, variation_id, user_id, attributes)
|
50
|
+
# Create conversion Event to be sent to the logging endpoint.
|
51
|
+
#
|
52
|
+
# experiment_key - Experiment for which impression needs to be recorded.
|
53
|
+
# variation_id - ID for variation which would be presented to user.
|
54
|
+
# user_id - ID for user.
|
55
|
+
# attributes - Hash representing user attributes and values which need to be recorded.
|
56
|
+
#
|
57
|
+
# Returns event hash encapsulating the impression event.
|
58
|
+
|
59
|
+
@params = {}
|
60
|
+
add_common_params(user_id, attributes)
|
61
|
+
add_impression_goal(experiment_key)
|
62
|
+
add_experiment(experiment_key, variation_id)
|
63
|
+
Event.new(@params)
|
64
|
+
end
|
65
|
+
|
66
|
+
def create_conversion_event(event_key, user_id, attributes, event_value, experiment_keys)
|
67
|
+
# Create conversion Event to be sent to the logging endpoint.
|
68
|
+
#
|
69
|
+
# event_key - Goal key representing the event which needs to be recorded.
|
70
|
+
# user_id - ID for user.
|
71
|
+
# attributes - Hash representing user attributes and values which need to be recorded.
|
72
|
+
# event_value - Value associated with the event. Can be used to represent revenue in cents.
|
73
|
+
# experiment_keys - Array of valid experiment keys for the goal
|
74
|
+
|
75
|
+
@params = {}
|
76
|
+
add_common_params(user_id, attributes)
|
77
|
+
add_conversion_goal(event_key, event_value)
|
78
|
+
add_experiment_variation_params(user_id, experiment_keys)
|
79
|
+
Event.new(@params)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def add_project_id
|
85
|
+
# Add project ID to the event.
|
86
|
+
|
87
|
+
@params[Params::PROJECT_ID] = @config.project_id
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_account_id
|
91
|
+
# Add account ID to the event.
|
92
|
+
|
93
|
+
@params[Params::ACCOUNT_ID] = @config.account_id
|
94
|
+
end
|
95
|
+
|
96
|
+
def add_user_id(user_id)
|
97
|
+
# Add user ID to the event.
|
98
|
+
|
99
|
+
@params[Params::END_USER_ID] = user_id
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_attributes(attributes)
|
103
|
+
# Add attribute(s) information to the event.
|
104
|
+
#
|
105
|
+
# attributes - Hash representing user attributes and values which need to be recorded.
|
106
|
+
|
107
|
+
return if attributes.nil?
|
108
|
+
|
109
|
+
attributes.keys.each do |attribute_key|
|
110
|
+
attribute_value = attributes[attribute_key]
|
111
|
+
next unless attribute_value
|
112
|
+
segment_id = @config.attribute_key_map[attribute_key]['segmentId']
|
113
|
+
segment_param = sprintf(ATTRIBUTE_PARAM_FORMAT,
|
114
|
+
segment_prefix: Params::SEGMENT_PREFIX, segment_id: segment_id)
|
115
|
+
params[segment_param] = attribute_value
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def add_source
|
120
|
+
# Add source information to the event.
|
121
|
+
|
122
|
+
@params[Params::SOURCE] = sprintf('ruby-sdk-%{version}', version: VERSION)
|
123
|
+
end
|
124
|
+
|
125
|
+
def add_time
|
126
|
+
# Add time information to the event.
|
127
|
+
|
128
|
+
@params[Params::TIME] = Time.now.strftime('%s').to_i
|
129
|
+
end
|
130
|
+
|
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
|
+
|
145
|
+
def add_impression_goal(experiment_key)
|
146
|
+
# Add impression goal information to the event.
|
147
|
+
#
|
148
|
+
# experiment_key - Experiment which is being activated.
|
149
|
+
|
150
|
+
# For tracking impressions, goal ID is set equal to experiment ID of experiment being activated.
|
151
|
+
@params[Params::GOAL_ID] = @config.get_experiment_id(experiment_key)
|
152
|
+
@params[Params::GOAL_NAME] = 'visitor-event'
|
153
|
+
end
|
154
|
+
|
155
|
+
def add_experiment(experiment_key, variation_id)
|
156
|
+
# Add experiment to variation mapping to the impression event.
|
157
|
+
#
|
158
|
+
# experiment_key - Experiment which is being activated.
|
159
|
+
# variation_id - ID for variation which would be presented to user.
|
160
|
+
|
161
|
+
experiment_id = @config.get_experiment_id(experiment_key)
|
162
|
+
experiment_param = sprintf(EXPERIMENT_PARAM_FORMAT,
|
163
|
+
experiment_prefix: Params::EXPERIMENT_PREFIX, experiment_id: experiment_id)
|
164
|
+
@params[experiment_param] = variation_id
|
165
|
+
end
|
166
|
+
|
167
|
+
def add_experiment_variation_params(user_id, experiment_keys)
|
168
|
+
# Maps experiment and corresponding variation as parameters to be used in the event tracking call.
|
169
|
+
#
|
170
|
+
# user_id - ID for user.
|
171
|
+
# experiment_keys - Array of valid experiment keys for the goal
|
172
|
+
|
173
|
+
experiment_keys.each do |experiment_key|
|
174
|
+
variation_id = @bucketer.bucket(experiment_key, user_id)
|
175
|
+
experiment_id = @config.experiment_key_map[experiment_key]['id']
|
176
|
+
experiment_param = sprintf(EXPERIMENT_PARAM_FORMAT,
|
177
|
+
experiment_prefix: Params::EXPERIMENT_PREFIX, experiment_id: experiment_id)
|
178
|
+
@params[experiment_param] = variation_id
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def add_conversion_goal(event_key, event_value)
|
183
|
+
# Add conversion goal information to the event.
|
184
|
+
#
|
185
|
+
# event_key - Goal key representing the event which needs to be recorded.
|
186
|
+
# event_value - Value associated with the event. Can be used to represent revenue in cents.
|
187
|
+
|
188
|
+
goal_id = @config.event_key_map[event_key]['id']
|
189
|
+
event_ids = goal_id
|
190
|
+
|
191
|
+
if event_value
|
192
|
+
event_ids = sprintf('%{goal_id},%{revenue_id}', goal_id: goal_id, revenue_id: @config.get_revenue_goal_id)
|
193
|
+
@params[Params::EVENT_VALUE] = event_value
|
194
|
+
end
|
195
|
+
|
196
|
+
@params[Params::GOAL_ID] = event_ids
|
197
|
+
@params[Params::GOAL_NAME] = event_key
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
|
3
|
+
module Optimizely
|
4
|
+
class EventDispatcher
|
5
|
+
REQUEST_TIMEOUT = 10
|
6
|
+
|
7
|
+
def dispatch_event(url, params)
|
8
|
+
# Dispatch the event being represented by the Event object.
|
9
|
+
#
|
10
|
+
# url - URL to send impression/conversion event to.
|
11
|
+
# params - Params to be sent to the impression/conversion event.
|
12
|
+
|
13
|
+
HTTParty.get(url, query: params, timeout: REQUEST_TIMEOUT)
|
14
|
+
rescue Timeout::Error => e
|
15
|
+
return e
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Optimizely
|
2
|
+
class Error < StandardError; end
|
3
|
+
|
4
|
+
class InvalidAudienceError < Error
|
5
|
+
# Raised when an invalid audience is provided
|
6
|
+
|
7
|
+
def initialize(msg = 'Provided audience is not in datafile.')
|
8
|
+
super
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class InvalidAttributeError < Error
|
13
|
+
# Raised when an invalid attribute is provided
|
14
|
+
|
15
|
+
def initialize(msg = 'Provided attribute is not in datafile.')
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class InvalidAttributeFormatError < Error
|
21
|
+
# Raised when attributes are provided in an invalid format (e.g. not a Hash)
|
22
|
+
|
23
|
+
def initialize(msg = 'Attributes provided are in an invalid format.')
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class InvalidDatafileError < Error
|
29
|
+
# Raised when an invalid datafile is provided
|
30
|
+
|
31
|
+
def initialize(msg = 'Provided datafile is in an invalid format.')
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class InvalidErrorHandlerError < Error
|
37
|
+
# Raised when an invalid error handler is provided
|
38
|
+
|
39
|
+
def initialize(msg = 'Provided error_handler is in an invalid format.')
|
40
|
+
super
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class InvalidEventDispatcherError < Error
|
45
|
+
# Raised when an invalid event dispatcher is provided
|
46
|
+
|
47
|
+
def initialize(msg = 'Provided event_dispatcher is in an invalid format.')
|
48
|
+
super
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class InvalidExperimentError < Error
|
53
|
+
# Raised when an invalid experiment key is provided
|
54
|
+
|
55
|
+
def initialize(msg = 'Provided experiment is not in datafile.')
|
56
|
+
super
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class InvalidGoalError < Error
|
61
|
+
# Raised when an invalid event key is provided
|
62
|
+
|
63
|
+
def initialize(msg = 'Provided event is not in datafile.')
|
64
|
+
super
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class InvalidLoggerError < Error
|
69
|
+
# Raised when an invalid logger is provided
|
70
|
+
|
71
|
+
def initialize(msg = 'Provided logger is in an invalid format.')
|
72
|
+
super
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class InvalidVariationError < Error
|
77
|
+
# Raised when an invalid variation key or ID is provided
|
78
|
+
|
79
|
+
def initialize(msg = 'Provided variation is not in datafile.')
|
80
|
+
super
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
module Optimizely
|
2
|
+
module Helpers
|
3
|
+
module Constants
|
4
|
+
JSON_SCHEMA = {
|
5
|
+
'type' => 'object',
|
6
|
+
'properties' => {
|
7
|
+
'projectId' => {
|
8
|
+
'type' => 'string'
|
9
|
+
},
|
10
|
+
'accountId' => {
|
11
|
+
'type' => 'string'
|
12
|
+
},
|
13
|
+
'experiments' => {
|
14
|
+
'type' => 'array',
|
15
|
+
'items' => {
|
16
|
+
'type' => 'object',
|
17
|
+
'properties' => {
|
18
|
+
'id' => {
|
19
|
+
'type' => 'string'
|
20
|
+
},
|
21
|
+
'key' => {
|
22
|
+
'type' => 'string'
|
23
|
+
},
|
24
|
+
'variations' => {
|
25
|
+
'type' => 'array',
|
26
|
+
'items' => {
|
27
|
+
'type' => 'object',
|
28
|
+
'properties' => {
|
29
|
+
'id' => {
|
30
|
+
'type' => 'string'
|
31
|
+
},
|
32
|
+
'key' => {
|
33
|
+
'type' => 'string'
|
34
|
+
}
|
35
|
+
},
|
36
|
+
'required' => [
|
37
|
+
'id',
|
38
|
+
'key'
|
39
|
+
]
|
40
|
+
}
|
41
|
+
},
|
42
|
+
'trafficAllocation' => {
|
43
|
+
'type' => 'array',
|
44
|
+
'items' => {
|
45
|
+
'type' => 'object',
|
46
|
+
'properties' => {
|
47
|
+
'entityId' => {
|
48
|
+
'type' => 'string'
|
49
|
+
},
|
50
|
+
'endOfRange' => {
|
51
|
+
'type' => 'integer'
|
52
|
+
}
|
53
|
+
},
|
54
|
+
'required' => [
|
55
|
+
'entityId',
|
56
|
+
'endOfRange'
|
57
|
+
]
|
58
|
+
}
|
59
|
+
},
|
60
|
+
'percentageIncluded' => {
|
61
|
+
'type' => 'integer'
|
62
|
+
},
|
63
|
+
'audienceIds' => {
|
64
|
+
'type' => 'array',
|
65
|
+
'items' => {
|
66
|
+
'type' => 'string'
|
67
|
+
}
|
68
|
+
},
|
69
|
+
'forcedVariations' => {
|
70
|
+
'type' => 'object'
|
71
|
+
}
|
72
|
+
},
|
73
|
+
'required' => [
|
74
|
+
'id',
|
75
|
+
'key',
|
76
|
+
'variations',
|
77
|
+
'trafficAllocation',
|
78
|
+
'percentageIncluded',
|
79
|
+
'audienceIds',
|
80
|
+
'forcedVariations'
|
81
|
+
]
|
82
|
+
}
|
83
|
+
},
|
84
|
+
'events' => {
|
85
|
+
'type' => 'array',
|
86
|
+
'items' => {
|
87
|
+
'type' => 'object',
|
88
|
+
'properties' => {
|
89
|
+
'key' => {
|
90
|
+
'type' => 'string'
|
91
|
+
},
|
92
|
+
'experimentIds' => {
|
93
|
+
'type' => 'array',
|
94
|
+
'items' => {
|
95
|
+
'type' => 'string'
|
96
|
+
}
|
97
|
+
},
|
98
|
+
'id' => {
|
99
|
+
'type' => 'string'
|
100
|
+
}
|
101
|
+
},
|
102
|
+
'required' => [
|
103
|
+
'key',
|
104
|
+
'experimentIds',
|
105
|
+
'id'
|
106
|
+
]
|
107
|
+
}
|
108
|
+
},
|
109
|
+
'audiences' => {
|
110
|
+
'type' => 'array',
|
111
|
+
'items' => {
|
112
|
+
'type' => 'object',
|
113
|
+
'properties' => {
|
114
|
+
'id' => {
|
115
|
+
'type' => 'string'
|
116
|
+
},
|
117
|
+
'name' => {
|
118
|
+
'type' => 'string'
|
119
|
+
},
|
120
|
+
'conditions' => {
|
121
|
+
'type' => 'string'
|
122
|
+
}
|
123
|
+
},
|
124
|
+
'required' => [
|
125
|
+
'id',
|
126
|
+
'name',
|
127
|
+
'conditions'
|
128
|
+
]
|
129
|
+
}
|
130
|
+
},
|
131
|
+
'dimensions' => {
|
132
|
+
'type' => 'array',
|
133
|
+
'items' => {
|
134
|
+
'type' => 'object',
|
135
|
+
'properties' => {
|
136
|
+
'id' => {
|
137
|
+
'type' => 'string'
|
138
|
+
},
|
139
|
+
'key' => {
|
140
|
+
'type' => 'string'
|
141
|
+
},
|
142
|
+
'segmentId' => {
|
143
|
+
'type' => 'string'
|
144
|
+
}
|
145
|
+
},
|
146
|
+
'required' => [
|
147
|
+
'id',
|
148
|
+
'key',
|
149
|
+
'segmentId'
|
150
|
+
]
|
151
|
+
}
|
152
|
+
},
|
153
|
+
'version' => {
|
154
|
+
'type' => 'string'
|
155
|
+
},
|
156
|
+
'revision' => {
|
157
|
+
'type' => 'string'
|
158
|
+
}
|
159
|
+
},
|
160
|
+
'required' => [
|
161
|
+
'projectId',
|
162
|
+
'accountId',
|
163
|
+
'experiments',
|
164
|
+
'events',
|
165
|
+
'audiences',
|
166
|
+
'dimensions',
|
167
|
+
'version',
|
168
|
+
'revision'
|
169
|
+
]
|
170
|
+
}
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|