optimizely-sdk 2.0.0.beta → 2.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|