optimizely-sdk 2.0.0.beta → 2.0.0.beta1
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 +4 -4
- data/lib/optimizely.rb +166 -73
- data/lib/optimizely/audience.rb +6 -3
- data/lib/optimizely/bucketer.rb +21 -16
- data/lib/optimizely/condition.rb +4 -2
- data/lib/optimizely/decision_service.rb +141 -141
- data/lib/optimizely/error_handler.rb +5 -5
- data/lib/optimizely/event_builder.rb +158 -147
- data/lib/optimizely/event_dispatcher.rb +7 -6
- data/lib/optimizely/exceptions.rb +12 -1
- data/lib/optimizely/helpers/constants.rb +64 -63
- data/lib/optimizely/helpers/event_tag_utils.rb +86 -11
- data/lib/optimizely/helpers/group.rb +3 -1
- data/lib/optimizely/helpers/validator.rb +9 -1
- data/lib/optimizely/helpers/variable_type.rb +11 -7
- data/lib/optimizely/logger.rb +5 -5
- data/lib/optimizely/notification_center.rb +150 -0
- data/lib/optimizely/params.rb +3 -1
- data/lib/optimizely/project_config.rb +128 -24
- data/lib/optimizely/user_profile_service.rb +2 -0
- data/lib/optimizely/version.rb +4 -1
- metadata +15 -16
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
#
|
2
|
-
# Copyright 2016, Optimizely and contributors
|
4
|
+
# Copyright 2016-2017, Optimizely and contributors
|
3
5
|
#
|
4
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
7
|
# you may not use this file except in compliance with the License.
|
@@ -18,15 +20,13 @@ module Optimizely
|
|
18
20
|
# Class encapsulating exception handling functionality.
|
19
21
|
# Override with your own exception handler providing a handle_error method.
|
20
22
|
|
21
|
-
def handle_error(_error)
|
22
|
-
end
|
23
|
+
def handle_error(_error); end
|
23
24
|
end
|
24
25
|
|
25
26
|
class NoOpErrorHandler < BaseErrorHandler
|
26
27
|
# Class providing handle_error method that suppresses errors.
|
27
28
|
|
28
|
-
def handle_error(_error)
|
29
|
-
end
|
29
|
+
def handle_error(_error); end
|
30
30
|
end
|
31
31
|
|
32
32
|
class RaiseErrorHandler < BaseErrorHandler
|
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
#
|
2
|
-
# Copyright 2016-
|
4
|
+
# Copyright 2016-2018, Optimizely and contributors
|
3
5
|
#
|
4
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
7
|
# you may not use this file except in compliance with the License.
|
@@ -17,8 +19,11 @@ require_relative './audience'
|
|
17
19
|
require_relative './params'
|
18
20
|
require_relative './version'
|
19
21
|
require_relative '../optimizely/helpers/event_tag_utils'
|
22
|
+
require 'securerandom'
|
20
23
|
|
21
24
|
module Optimizely
|
25
|
+
RESERVED_ATTRIBUTE_KEY_BUCKETING_ID_EVENT_PARAM_KEY = 'optimizely_bucketing_id'
|
26
|
+
|
22
27
|
class Event
|
23
28
|
# Representation of an event which can be sent to the Optimizely logging endpoint.
|
24
29
|
|
@@ -35,204 +40,210 @@ module Optimizely
|
|
35
40
|
end
|
36
41
|
|
37
42
|
# Override equality operator to make two events with the same contents equal for testing purposes
|
38
|
-
def ==(
|
39
|
-
@http_verb ==
|
43
|
+
def ==(other)
|
44
|
+
@http_verb == other.http_verb && @url == other.url && @params == other.params && @headers == other.headers
|
40
45
|
end
|
41
46
|
end
|
42
47
|
|
43
48
|
class BaseEventBuilder
|
49
|
+
CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom'
|
50
|
+
|
44
51
|
attr_reader :config
|
45
|
-
attr_reader :
|
52
|
+
attr_reader :logger
|
46
53
|
|
47
|
-
def initialize(config)
|
54
|
+
def initialize(config, logger)
|
48
55
|
@config = config
|
49
|
-
@
|
56
|
+
@logger = logger
|
50
57
|
end
|
51
58
|
|
52
59
|
private
|
53
60
|
|
54
|
-
def
|
55
|
-
#
|
61
|
+
def get_common_params(user_id, attributes)
|
62
|
+
# Get params which are used in both conversion and impression events.
|
63
|
+
#
|
64
|
+
# user_id - +String+ ID for user
|
65
|
+
# attributes - +Hash+ representing user attributes and values which need to be recorded.
|
56
66
|
#
|
57
|
-
#
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
67
|
+
# Returns +Hash+ Common event params
|
68
|
+
|
69
|
+
visitor_attributes = []
|
70
|
+
|
71
|
+
attributes&.keys&.each do |attribute_key|
|
72
|
+
# Omit null attribute value
|
73
|
+
attribute_value = attributes[attribute_key]
|
74
|
+
next if attribute_value.nil?
|
75
|
+
|
76
|
+
if attribute_key.eql? RESERVED_ATTRIBUTE_KEY_BUCKETING_ID
|
77
|
+
# TODO: (Copied from PHP-SDK) (Alda): the type for bucketing ID attribute may change so
|
78
|
+
# that custom attributes are not overloaded
|
79
|
+
feature = {
|
80
|
+
entity_id: RESERVED_ATTRIBUTE_KEY_BUCKETING_ID,
|
81
|
+
key: RESERVED_ATTRIBUTE_KEY_BUCKETING_ID_EVENT_PARAM_KEY,
|
82
|
+
type: CUSTOM_ATTRIBUTE_FEATURE_TYPE,
|
83
|
+
value: attribute_value
|
84
|
+
}
|
85
|
+
else
|
86
|
+
# Skip attributes not in the datafile
|
87
|
+
attribute_id = @config.get_attribute_id(attribute_key)
|
88
|
+
next unless attribute_id
|
89
|
+
|
90
|
+
feature = {
|
91
|
+
entity_id: attribute_id,
|
92
|
+
key: attribute_key,
|
93
|
+
type: CUSTOM_ATTRIBUTE_FEATURE_TYPE,
|
94
|
+
value: attribute_value
|
95
|
+
}
|
96
|
+
|
97
|
+
end
|
98
|
+
visitor_attributes.push(feature)
|
99
|
+
end
|
100
|
+
|
101
|
+
common_params = {
|
102
|
+
account_id: @config.account_id,
|
103
|
+
project_id: @config.project_id,
|
104
|
+
visitors: [
|
105
|
+
{
|
106
|
+
attributes: visitor_attributes,
|
107
|
+
snapshots: [],
|
108
|
+
visitor_id: user_id
|
109
|
+
}
|
110
|
+
],
|
111
|
+
anonymize_ip: @config.anonymize_ip,
|
112
|
+
revision: @config.revision,
|
113
|
+
client_name: CLIENT_ENGINE,
|
114
|
+
client_version: VERSION
|
115
|
+
}
|
116
|
+
|
117
|
+
common_params
|
66
118
|
end
|
67
119
|
end
|
68
120
|
|
69
121
|
class EventBuilder < BaseEventBuilder
|
70
|
-
|
71
|
-
|
72
|
-
|
122
|
+
ENDPOINT = 'https://logx.optimizely.com/v1/events'
|
123
|
+
POST_HEADERS = {'Content-Type' => 'application/json'}.freeze
|
124
|
+
ACTIVATE_EVENT_KEY = 'campaign_activated'
|
73
125
|
|
74
126
|
def create_impression_event(experiment, variation_id, user_id, attributes)
|
75
|
-
# Create
|
127
|
+
# Create impression Event to be sent to the logging endpoint.
|
76
128
|
#
|
77
|
-
# experiment - Experiment for which impression needs to be recorded.
|
78
|
-
# variation_id - ID for variation which would be presented to user.
|
79
|
-
# user_id - ID for user.
|
80
|
-
# attributes -
|
129
|
+
# experiment - +Object+ Experiment for which impression needs to be recorded.
|
130
|
+
# variation_id - +String+ ID for variation which would be presented to user.
|
131
|
+
# user_id - +String+ ID for user.
|
132
|
+
# attributes - +Hash+ representing user attributes and values which need to be recorded.
|
81
133
|
#
|
82
|
-
# Returns
|
134
|
+
# Returns +Event+ encapsulating the impression event.
|
135
|
+
|
136
|
+
event_params = get_common_params(user_id, attributes)
|
137
|
+
impression_params = get_impression_params(experiment, variation_id)
|
138
|
+
event_params[:visitors][0][:snapshots].push(impression_params)
|
83
139
|
|
84
|
-
|
85
|
-
add_common_params(user_id, attributes)
|
86
|
-
add_decision(experiment, variation_id)
|
87
|
-
add_attributes(attributes)
|
88
|
-
Event.new(:post, IMPRESSION_EVENT_ENDPOINT, @params, POST_HEADERS)
|
140
|
+
Event.new(:post, ENDPOINT, event_params, POST_HEADERS)
|
89
141
|
end
|
90
142
|
|
91
143
|
def create_conversion_event(event_key, user_id, attributes, event_tags, experiment_variation_map)
|
92
144
|
# Create conversion Event to be sent to the logging endpoint.
|
93
145
|
#
|
94
|
-
# event_key - Event key representing the event which needs to be recorded.
|
95
|
-
# user_id - ID for user.
|
96
|
-
# attributes -
|
97
|
-
# event_tags -
|
98
|
-
# experiment_variation_map - Map of experiment ID to the ID of the variation that the user is bucketed into.
|
146
|
+
# event_key - +String+ Event key representing the event which needs to be recorded.
|
147
|
+
# user_id - +String+ ID for user.
|
148
|
+
# attributes - +Hash+ representing user attributes and values which need to be recorded.
|
149
|
+
# event_tags - +Hash+ representing metadata associated with the event.
|
150
|
+
# experiment_variation_map - +Map+ of experiment ID to the ID of the variation that the user is bucketed into.
|
99
151
|
#
|
100
|
-
# Returns
|
101
|
-
|
102
|
-
@params = {}
|
103
|
-
add_common_params(user_id, attributes)
|
104
|
-
add_conversion_event(event_key)
|
105
|
-
add_event_tags(event_tags)
|
106
|
-
add_layer_states(experiment_variation_map)
|
107
|
-
Event.new(:post, CONVERSION_EVENT_ENDPOINT, @params, POST_HEADERS)
|
108
|
-
end
|
109
|
-
|
110
|
-
private
|
111
|
-
|
112
|
-
def add_common_params(user_id, attributes)
|
113
|
-
super
|
114
|
-
@params['isGlobalHoldback'] = false
|
115
|
-
end
|
116
|
-
|
117
|
-
def add_project_id
|
118
|
-
@params['projectId'] = @config.project_id
|
119
|
-
end
|
152
|
+
# Returns +Event+ encapsulating the conversion event.
|
120
153
|
|
121
|
-
|
122
|
-
|
123
|
-
|
154
|
+
event_params = get_common_params(user_id, attributes)
|
155
|
+
conversion_params = get_conversion_params(event_key, event_tags, experiment_variation_map)
|
156
|
+
event_params[:visitors][0][:snapshots] = conversion_params
|
124
157
|
|
125
|
-
|
126
|
-
@params['visitorId'] = user_id
|
158
|
+
Event.new(:post, ENDPOINT, event_params, POST_HEADERS)
|
127
159
|
end
|
128
160
|
|
129
|
-
|
130
|
-
@params['userFeatures'] = []
|
131
|
-
|
132
|
-
return if attributes.nil?
|
161
|
+
private
|
133
162
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
next unless attribute_id
|
142
|
-
|
143
|
-
feature = {
|
144
|
-
'id' => attribute_id,
|
145
|
-
'name' => attribute_key,
|
146
|
-
'type' => 'custom',
|
147
|
-
'value' => attribute_value,
|
148
|
-
'shouldIndex' => true,
|
149
|
-
}
|
150
|
-
@params['userFeatures'].push(feature)
|
151
|
-
end
|
152
|
-
end
|
163
|
+
def get_impression_params(experiment, variation_id)
|
164
|
+
# Creates object of params specific to impression events
|
165
|
+
#
|
166
|
+
# experiment - +Hash+ experiment for which impression needs to be recorded
|
167
|
+
# variation_id - +string+ ID for variation which would be presented to user
|
168
|
+
#
|
169
|
+
# Returns +Hash+ Impression event params
|
153
170
|
|
154
|
-
def add_decision(experiment, variation_id)
|
155
171
|
experiment_key = experiment['key']
|
156
172
|
experiment_id = experiment['id']
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
173
|
+
|
174
|
+
impression_event_params = {
|
175
|
+
decisions: [{
|
176
|
+
campaign_id: @config.experiment_key_map[experiment_key]['layerId'],
|
177
|
+
experiment_id: experiment_id,
|
178
|
+
variation_id: variation_id
|
179
|
+
}],
|
180
|
+
events: [{
|
181
|
+
entity_id: @config.experiment_key_map[experiment_key]['layerId'],
|
182
|
+
timestamp: create_timestamp,
|
183
|
+
key: ACTIVATE_EVENT_KEY,
|
184
|
+
uuid: create_uuid
|
185
|
+
}]
|
162
186
|
}
|
187
|
+
|
188
|
+
impression_event_params
|
163
189
|
end
|
164
190
|
|
165
|
-
def
|
166
|
-
|
167
|
-
|
191
|
+
def get_conversion_params(event_key, event_tags, experiment_variation_map)
|
192
|
+
# Creates object of params specific to conversion events
|
193
|
+
#
|
194
|
+
# event_key - +String+ Key representing the event which needs to be recorded
|
195
|
+
# event_tags - +Hash+ Values associated with the event.
|
196
|
+
# experiment_variation_map - +Hash+ Map of experiment IDs to bucketed variation IDs
|
197
|
+
#
|
198
|
+
# Returns +Hash+ Impression event params
|
199
|
+
|
200
|
+
conversion_event_params = []
|
201
|
+
|
202
|
+
experiment_variation_map.each do |experiment_id, variation_id|
|
203
|
+
single_snapshot = {
|
204
|
+
decisions: [{
|
205
|
+
campaign_id: @config.experiment_id_map[experiment_id]['layerId'],
|
206
|
+
experiment_id: experiment_id,
|
207
|
+
variation_id: variation_id
|
208
|
+
}],
|
209
|
+
events: []
|
210
|
+
}
|
168
211
|
|
169
|
-
|
212
|
+
event_object = {
|
213
|
+
entity_id: @config.event_key_map[event_key]['id'],
|
214
|
+
timestamp: create_timestamp,
|
215
|
+
uuid: create_uuid,
|
216
|
+
key: event_key
|
217
|
+
}
|
170
218
|
|
171
|
-
|
172
|
-
|
219
|
+
if event_tags
|
220
|
+
revenue_value = Helpers::EventTagUtils.get_revenue_value(event_tags, @logger)
|
221
|
+
event_object[:revenue] = revenue_value if revenue_value
|
173
222
|
|
174
|
-
|
175
|
-
|
176
|
-
'type' => 'custom',
|
177
|
-
'value' => event_tag_value,
|
178
|
-
'shouldIndex' => false,
|
179
|
-
}
|
180
|
-
@params['eventFeatures'].push(event_feature)
|
223
|
+
numeric_value = Helpers::EventTagUtils.get_numeric_value(event_tags, @logger)
|
224
|
+
event_object[:value] = numeric_value if numeric_value
|
181
225
|
|
182
|
-
|
226
|
+
event_object[:tags] = event_tags
|
227
|
+
end
|
183
228
|
|
184
|
-
|
229
|
+
single_snapshot[:events] = [event_object]
|
185
230
|
|
186
|
-
|
187
|
-
event_metric = {
|
188
|
-
'name' => 'revenue',
|
189
|
-
'value' => event_value
|
190
|
-
}
|
191
|
-
@params['eventMetrics'].push(event_metric)
|
231
|
+
conversion_event_params.push(single_snapshot)
|
192
232
|
end
|
193
233
|
|
234
|
+
conversion_event_params
|
194
235
|
end
|
195
236
|
|
196
|
-
def
|
197
|
-
#
|
198
|
-
#
|
199
|
-
# event_key - Event key representing the event which needs to be recorded.
|
200
|
-
|
201
|
-
event_id = @config.event_key_map[event_key]['id']
|
202
|
-
event_name = @config.event_key_map[event_key]['key']
|
237
|
+
def create_timestamp
|
238
|
+
# Returns +Integer+ Current timestamp
|
203
239
|
|
204
|
-
|
205
|
-
@params['eventName'] = event_name
|
240
|
+
(Time.now.to_f * 1000).to_i
|
206
241
|
end
|
207
242
|
|
208
|
-
def
|
209
|
-
#
|
210
|
-
#
|
211
|
-
# experiments_map - Hash with experiment ID as a key and variation ID as a value.
|
212
|
-
|
213
|
-
@params['layerStates'] = []
|
214
|
-
|
215
|
-
experiments_map.each do |experiment_id, variation_id|
|
216
|
-
layer_state = {
|
217
|
-
'layerId' => @config.experiment_id_map[experiment_id]['layerId'],
|
218
|
-
'decision' => {
|
219
|
-
'variationId' => variation_id,
|
220
|
-
'experimentId' => experiment_id,
|
221
|
-
'isLayerHoldback' => false,
|
222
|
-
},
|
223
|
-
'actionTriggered' => true,
|
224
|
-
}
|
225
|
-
@params['layerStates'].push(layer_state)
|
226
|
-
end
|
227
|
-
end
|
228
|
-
|
229
|
-
def add_source
|
230
|
-
@params['clientEngine'] = 'ruby-sdk'
|
231
|
-
@params['clientVersion'] = VERSION
|
232
|
-
end
|
243
|
+
def create_uuid
|
244
|
+
# Returns +String+ Random UUID
|
233
245
|
|
234
|
-
|
235
|
-
@params['timestamp'] = (Time.now.to_f * 1000).to_i
|
246
|
+
SecureRandom.uuid
|
236
247
|
end
|
237
248
|
end
|
238
249
|
end
|
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
#
|
2
|
-
# Copyright 2016, Optimizely and contributors
|
4
|
+
# Copyright 2016-2017, Optimizely and contributors
|
3
5
|
#
|
4
6
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
7
|
# you may not use this file except in compliance with the License.
|
@@ -19,8 +21,7 @@ module Optimizely
|
|
19
21
|
class NoOpEventDispatcher
|
20
22
|
# Class providing dispatch_event method which does nothing.
|
21
23
|
|
22
|
-
def dispatch_event(event)
|
23
|
-
end
|
24
|
+
def dispatch_event(event); end
|
24
25
|
end
|
25
26
|
|
26
27
|
class EventDispatcher
|
@@ -40,9 +41,9 @@ module Optimizely
|
|
40
41
|
elsif event.http_verb == :post
|
41
42
|
begin
|
42
43
|
HTTParty.post(event.url,
|
43
|
-
|
44
|
-
|
45
|
-
|
44
|
+
body: event.params.to_json,
|
45
|
+
headers: event.headers,
|
46
|
+
timeout: REQUEST_TIMEOUT)
|
46
47
|
rescue Timeout::Error => e
|
47
48
|
return e
|
48
49
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
#
|
2
4
|
# Copyright 2016-2017, Optimizely and contributors
|
3
5
|
#
|
@@ -83,7 +85,8 @@ module Optimizely
|
|
83
85
|
class InvalidDatafileVersionError < Error
|
84
86
|
# Raised when a datafile with an unsupported version is provided
|
85
87
|
|
86
|
-
def initialize(msg = 'Provided datafile is an unsupported version. Please use SDK version 1.1.2 or earlier
|
88
|
+
def initialize(msg = 'Provided datafile is an unsupported version. Please use SDK version 1.1.2 or earlier '\
|
89
|
+
'for datafile version 1.')
|
87
90
|
super
|
88
91
|
end
|
89
92
|
end
|
@@ -95,4 +98,12 @@ module Optimizely
|
|
95
98
|
super("Provided #{type} is in an invalid format.")
|
96
99
|
end
|
97
100
|
end
|
101
|
+
|
102
|
+
class InvalidNotificationType < Error
|
103
|
+
# Raised when an invalid notification type is provided
|
104
|
+
|
105
|
+
def initialize(msg = 'Provided notification type is invalid.')
|
106
|
+
super
|
107
|
+
end
|
108
|
+
end
|
98
109
|
end
|