optimizely-sdk 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|