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.
@@ -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