kameleoon-client-ruby 1.1.1 → 2.0.0
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/kameleoon/client.rb +481 -425
- data/lib/kameleoon/configuration/experiment.rb +42 -0
- data/lib/kameleoon/configuration/feature_flag.rb +41 -0
- data/lib/kameleoon/configuration/feature_flag_v2.rb +30 -0
- data/lib/kameleoon/configuration/rule.rb +45 -0
- data/lib/kameleoon/configuration/variable.rb +23 -0
- data/lib/kameleoon/configuration/variation.rb +31 -0
- data/lib/kameleoon/configuration/variation_exposition.rb +23 -0
- data/lib/kameleoon/cookie.rb +11 -4
- data/lib/kameleoon/data.rb +36 -22
- data/lib/kameleoon/exceptions.rb +46 -23
- data/lib/kameleoon/factory.rb +21 -18
- data/lib/kameleoon/request.rb +28 -15
- data/lib/kameleoon/storage/variation_storage.rb +42 -0
- data/lib/kameleoon/storage/visitor_variation.rb +20 -0
- data/lib/kameleoon/targeting/condition.rb +15 -3
- data/lib/kameleoon/targeting/condition_factory.rb +9 -2
- data/lib/kameleoon/targeting/conditions/custom_datum.rb +20 -36
- data/lib/kameleoon/targeting/conditions/exclusive_experiment.rb +29 -0
- data/lib/kameleoon/targeting/conditions/target_experiment.rb +44 -0
- data/lib/kameleoon/targeting/models.rb +36 -36
- data/lib/kameleoon/utils.rb +4 -1
- data/lib/kameleoon/version.rb +4 -2
- metadata +13 -3
- data/lib/kameleoon/query_graphql.rb +0 -76
data/lib/kameleoon/client.rb
CHANGED
@@ -4,7 +4,10 @@ require 'kameleoon/targeting/models'
|
|
4
4
|
require 'kameleoon/request'
|
5
5
|
require 'kameleoon/exceptions'
|
6
6
|
require 'kameleoon/cookie'
|
7
|
-
require 'kameleoon/
|
7
|
+
require 'kameleoon/configuration/feature_flag'
|
8
|
+
require 'kameleoon/configuration/variation'
|
9
|
+
require 'kameleoon/configuration/feature_flag_v2'
|
10
|
+
require 'kameleoon/storage/variation_storage'
|
8
11
|
require 'rufus/scheduler'
|
9
12
|
require 'yaml'
|
10
13
|
require 'json'
|
@@ -13,6 +16,7 @@ require 'em-synchrony/em-http'
|
|
13
16
|
require 'em-synchrony/fiber_iterator'
|
14
17
|
require 'objspace'
|
15
18
|
require 'time'
|
19
|
+
require 'ostruct'
|
16
20
|
|
17
21
|
module Kameleoon
|
18
22
|
##
|
@@ -26,10 +30,9 @@ module Kameleoon
|
|
26
30
|
##
|
27
31
|
# You should create Client with the Client Factory only.
|
28
32
|
#
|
29
|
-
def initialize(site_code, path_config_file,
|
33
|
+
def initialize(site_code, path_config_file, interval, default_timeout, client_id = nil, client_secret = nil)
|
30
34
|
config = YAML.load_file(path_config_file)
|
31
35
|
@site_code = site_code
|
32
|
-
@blocking = blocking
|
33
36
|
@default_timeout = config['default_timeout'] || default_timeout # in ms
|
34
37
|
refresh_interval = config['actions_configuration_refresh_interval']
|
35
38
|
@interval = refresh_interval.nil? ? interval : "#{refresh_interval}m"
|
@@ -42,7 +45,10 @@ module Kameleoon
|
|
42
45
|
@verbose_mode = config['verbose_mode'] || false
|
43
46
|
@experiments = []
|
44
47
|
@feature_flags = []
|
48
|
+
@feature_flags_v2 = []
|
45
49
|
@data = {}
|
50
|
+
@user_agents = {}
|
51
|
+
@variation_storage = Kameleoon::Storage::VariationStorage.new
|
46
52
|
end
|
47
53
|
|
48
54
|
##
|
@@ -69,16 +75,22 @@ module Kameleoon
|
|
69
75
|
#
|
70
76
|
# @example
|
71
77
|
# cookies = {'kameleoonVisitorCode' => '1234asdf4321fdsa'}
|
72
|
-
# visitor_code =
|
78
|
+
# visitor_code = get_visitor_code(cookies, 'my-domaine.com')
|
73
79
|
#
|
74
|
-
def
|
80
|
+
def get_visitor_code(cookies, top_level_domain, default_visitor_code = nil)
|
75
81
|
read_and_write(cookies, top_level_domain, cookies, default_visitor_code)
|
76
82
|
end
|
77
83
|
|
84
|
+
# DEPRECATED. Please use `get_visitor_code` instead.
|
85
|
+
def obtain_visitor_code(cookies, top_level_domain, default_visitor_code = nil)
|
86
|
+
warn '[DEPRECATION] `obtain_visitor_code` is deprecated. Please use `get_visitor_code` instead.'
|
87
|
+
get_visitor_code(cookies, top_level_domain, default_visitor_code)
|
88
|
+
end
|
89
|
+
|
78
90
|
##
|
79
91
|
# Trigger an experiment.
|
80
92
|
#
|
81
|
-
# If such a visitor_code has never been associated with any variation, the SDK returns a randomly selected variation
|
93
|
+
# If such a visitor_code has never been associated with any variation, the SDK returns a randomly selected variation
|
82
94
|
# If a user with a given visitor_code is already registered with a variation, it will detect the previously
|
83
95
|
# registered variation and return the variation_id.
|
84
96
|
# You have to make sure that proper error handling is set up in your code as shown in the example to the right to
|
@@ -90,57 +102,34 @@ module Kameleoon
|
|
90
102
|
# @return [Integer] Id of the variation
|
91
103
|
#
|
92
104
|
# @raise [Kameleoon::Exception::ExperimentConfigurationNotFound] Raise when experiment configuration is not found
|
93
|
-
# @raise [Kameleoon::Exception::
|
94
|
-
#
|
105
|
+
# @raise [Kameleoon::Exception::NotAllocated] The visitor triggered the experiment, but did not activate it.
|
106
|
+
# Usually, this happens because the user has been associated with excluded traffic
|
107
|
+
# @raise [Kameleoon::Exception::NotTargeted] The visitor is not targeted by the experiment, as the
|
108
|
+
# associated targeting segment conditions were not fulfilled. He should see the reference variation
|
95
109
|
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
96
110
|
#
|
97
|
-
def trigger_experiment(visitor_code, experiment_id
|
111
|
+
def trigger_experiment(visitor_code, experiment_id)
|
98
112
|
check_visitor_code(visitor_code)
|
99
|
-
experiment = @experiments.find { |
|
113
|
+
experiment = @experiments.find { |exp| exp.id.to_s == experiment_id.to_s }
|
100
114
|
if experiment.nil?
|
101
115
|
raise Exception::ExperimentConfigurationNotFound.new(experiment_id)
|
102
116
|
end
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
log "Trigger experiment connexion:" + connexion_options.inspect
|
113
|
-
request = EM::Synchrony.sync post(request_options, @tracking_url, connexion_options)
|
114
|
-
if is_successful(request)
|
115
|
-
variation_id = request.response
|
116
|
-
else
|
117
|
-
log "Failed to trigger experiment: " + request.inspect
|
118
|
-
raise Exception::ExperimentConfigurationNotFound.new(experiment_id) if variation_id.nil?
|
119
|
-
end
|
120
|
-
EM.stop
|
121
|
-
end
|
122
|
-
if variation_id.nil? || variation_id.to_s == "null" || variation_id.to_s == ""
|
123
|
-
raise Exception::NotTargeted.new(visitor_code)
|
124
|
-
elsif variation_id.to_s == "0"
|
125
|
-
raise Exception::NotActivated.new(visitor_code)
|
126
|
-
end
|
127
|
-
variation_id.to_i
|
128
|
-
else
|
129
|
-
check_site_code_enable(experiment)
|
130
|
-
visitor_data = @data.select { |key, value| key.to_s == visitor_code }.values.flatten! || []
|
131
|
-
if experiment['targetingSegment'].nil? || experiment['targetingSegment'].check_tree(visitor_data)
|
132
|
-
threshold = obtain_hash_double(visitor_code, experiment['respoolTime'], experiment['id'])
|
133
|
-
experiment['deviations'].each do |key, value|
|
134
|
-
threshold -= value
|
135
|
-
if threshold < 0
|
136
|
-
track_experiment(visitor_code, experiment_id, key)
|
137
|
-
return key.to_s.to_i
|
138
|
-
end
|
139
|
-
end
|
117
|
+
check_site_code_enable(experiment)
|
118
|
+
if check_targeting(visitor_code, experiment_id, experiment)
|
119
|
+
saved_variation = get_valid_saved_variation(visitor_code, experiment)
|
120
|
+
variation_key = saved_variation || variation_for_experiment(visitor_code, experiment)
|
121
|
+
if !variation_key.nil?
|
122
|
+
track_experiment(visitor_code, experiment_id, variation_key)
|
123
|
+
@variation_storage.update_variation(visitor_code, experiment_id, variation_key) if saved_variation.nil?
|
124
|
+
variation_key
|
125
|
+
else
|
140
126
|
track_experiment(visitor_code, experiment_id, REFERENCE, true)
|
141
|
-
raise Exception::
|
127
|
+
raise Exception::NotAllocated.new(visitor_code),
|
128
|
+
"Experiment #{experiment_id} is not active for visitor #{visitor_code}"
|
142
129
|
end
|
143
|
-
|
130
|
+
else
|
131
|
+
raise Exception::NotTargeted.new(visitor_code),
|
132
|
+
"Experiment #{experiment_id} is not targeted for visitor #{visitor_code}"
|
144
133
|
end
|
145
134
|
end
|
146
135
|
|
@@ -162,12 +151,13 @@ module Kameleoon
|
|
162
151
|
while ObjectSpace.memsize_of(@data) > @data_maximum_size * (2**20) do
|
163
152
|
@data.shift
|
164
153
|
end
|
165
|
-
|
166
|
-
if
|
167
|
-
|
168
|
-
|
169
|
-
@data[visitor_code] = args
|
154
|
+
args.each do |data_element|
|
155
|
+
if data_element.is_a?(UserAgent)
|
156
|
+
add_user_agent_data(visitor_code, data_element)
|
157
|
+
next
|
170
158
|
end
|
159
|
+
@data[visitor_code] = [] unless @data.key?(visitor_code)
|
160
|
+
@data[visitor_code].push(data_element)
|
171
161
|
end
|
172
162
|
end
|
173
163
|
|
@@ -175,8 +165,10 @@ module Kameleoon
|
|
175
165
|
# Track conversions on a particular goal
|
176
166
|
#
|
177
167
|
# This method requires visitor_code and goal_id to track conversion on this particular goal.
|
178
|
-
# In addition, this method also accepts revenue as a third optional argument to track revenue.
|
179
|
-
# The
|
168
|
+
# In addition, this method also accepts revenue as a third optional argument to track revenue.
|
169
|
+
# The visitor_code usually is identical to the one that was used when triggering the experiment.
|
170
|
+
# The track_conversion method doesn't return any value.
|
171
|
+
# This method is non-blocking as the server call is made asynchronously.
|
180
172
|
#
|
181
173
|
# @param [String] visitor_code Visitor code
|
182
174
|
# @param [Integer] goal_id Id of the goal
|
@@ -203,13 +195,17 @@ module Kameleoon
|
|
203
195
|
#
|
204
196
|
def flush(visitor_code = nil)
|
205
197
|
check_visitor_code(visitor_code) unless visitor_code.nil?
|
206
|
-
|
198
|
+
if !visitor_code.nil?
|
199
|
+
track_data(visitor_code)
|
200
|
+
else
|
201
|
+
@data.select { |_, values| values.any? { |data| !data.sent } }.each_key { |key| flush(key) }
|
202
|
+
end
|
207
203
|
end
|
208
204
|
|
209
205
|
##
|
210
206
|
# Obtain variation associated data.
|
211
207
|
#
|
212
|
-
# To retrieve JSON data associated with a variation, call the
|
208
|
+
# To retrieve JSON data associated with a variation, call the get_variation_associated_data method of our SDK.
|
213
209
|
# The JSON data usually represents some metadata of the variation, and can be configured on our web application
|
214
210
|
# interface or via our Automation API.
|
215
211
|
# This method takes the variationID as a parameter and will return the data as a json string.
|
@@ -221,74 +217,154 @@ module Kameleoon
|
|
221
217
|
#
|
222
218
|
# @raise [Kameleoon::Exception::VariationNotFound] Raise exception if the variation is not found.
|
223
219
|
#
|
224
|
-
def
|
225
|
-
variation = @experiments.map
|
220
|
+
def get_variation_associated_data(variation_id)
|
221
|
+
variation = @experiments.map(&:variations).flatten.select { |var| var['id'].to_i == variation_id.to_i }.first
|
226
222
|
if variation.nil?
|
227
|
-
raise Exception::VariationConfigurationNotFound.new(variation_id)
|
223
|
+
raise Exception::VariationConfigurationNotFound.new(variation_id),
|
224
|
+
"Variation key #{variation_id} not found"
|
228
225
|
else
|
229
226
|
JSON.parse(variation['customJson'])
|
230
227
|
end
|
231
228
|
end
|
232
229
|
|
233
|
-
#
|
230
|
+
# DEPRECATED. Please use `get_variation_associated_data` instead.
|
231
|
+
def obtain_variation_associated_data(variation_id)
|
232
|
+
warn '[DEPRECATION] `obtain_variation_associated_data` is deprecated.
|
233
|
+
Please use `get_variation_associated_data` instead.'
|
234
|
+
get_variation_associated_data(variation_id)
|
235
|
+
end
|
236
|
+
|
237
|
+
# #
|
234
238
|
# Activate a feature toggle.
|
239
|
+
|
240
|
+
# This method takes a visitor_code and feature_key (or feature_id) as mandatory arguments to check if the specified
|
241
|
+
# feature will be active for a given user.
|
242
|
+
# If such a user has never been associated with this feature flag, the SDK returns a boolean value randomly
|
243
|
+
# (true if the user should have this feature or false if not). If a user with a given visitorCode is already
|
244
|
+
# registered with this feature flag, it will detect the previous featureFlag value.
|
245
|
+
# You have to make sure that proper error handling is set up in your code as shown in the example
|
246
|
+
# to the right to catch potential exceptions.
|
247
|
+
|
248
|
+
# @param [String] visitor_code
|
249
|
+
# @param [String | Integer] feature_key
|
250
|
+
|
251
|
+
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound] Feature Flag isn't found in this configuration
|
252
|
+
# @raise [Kameleoon::Exception::NotTargeted] The visitor is not targeted by the experiment, as the
|
253
|
+
# associated targeting segment conditions were not fulfilled. He should see the reference variation
|
254
|
+
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
255
|
+
|
256
|
+
def activate_feature(visitor_code, feature_key)
|
257
|
+
check_visitor_code(visitor_code)
|
258
|
+
feature_flag = find_feature_flag(feature_key)
|
259
|
+
check_site_code_enable(feature_flag)
|
260
|
+
unless check_targeting(visitor_code, feature_flag.id.to_i, feature_flag)
|
261
|
+
raise Exception::NotTargeted.new(visitor_code),
|
262
|
+
"Visitor '#{visitor_code}' is not targeted for FF with key '#{feature_key}'"
|
263
|
+
end
|
264
|
+
|
265
|
+
if feature_flag.is_scheduled_active(Time.now.to_i)
|
266
|
+
threshold = obtain_hash_double(visitor_code, {}, feature_flag.id)
|
267
|
+
if threshold >= 1 - feature_flag.exposition_rate
|
268
|
+
track_experiment(visitor_code, feature_flag.id, feature_flag.variations.first['id'])
|
269
|
+
true
|
270
|
+
else
|
271
|
+
track_experiment(visitor_code, feature_flag.id, REFERENCE, none_variation: true)
|
272
|
+
false
|
273
|
+
end
|
274
|
+
else
|
275
|
+
false
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
##
|
280
|
+
# Check if feature is active for a given visitor code
|
281
|
+
#
|
282
|
+
# This method takes a visitor_code and feature_key as mandatory arguments to check if the specified
|
283
|
+
# feature will be active for a given user.
|
284
|
+
# If such a user has never been associated with this feature flag, the SDK returns a boolean value randomly
|
285
|
+
# (true if the user should have this feature or false if not). If a user with a given visitorCode is already
|
286
|
+
# registered with this feature flag, it will detect the previous feature flag value.
|
287
|
+
# You have to make sure that proper error handling is set up in your code as shown in the example
|
288
|
+
# to the right to catch potential exceptions.
|
289
|
+
#
|
290
|
+
# @param [String] visitor_code Unique identifier of the user. This field is mandatory.
|
291
|
+
# @param [String] feature_key Key of the feature flag you want to expose to a user. This field is mandatory.
|
292
|
+
#
|
293
|
+
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound] Feature Flag isn't found in this configuration
|
294
|
+
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
295
|
+
#
|
296
|
+
def feature_active?(visitor_code, feature_key)
|
297
|
+
_, variation_key = _get_feature_variation_key(visitor_code, feature_key)
|
298
|
+
variation_key != Kameleoon::Configuration::VariationType::VARIATION_OFF
|
299
|
+
end
|
300
|
+
|
235
301
|
#
|
236
|
-
#
|
237
|
-
#
|
238
|
-
#
|
302
|
+
# get_feature_variation_key returns a variation key for visitor code
|
303
|
+
#
|
304
|
+
# This method takes a visitorCode and featureKey as mandatory arguments and
|
305
|
+
# returns a variation assigned for a given visitor
|
306
|
+
# If such a user has never been associated with any feature flag rules, the SDK returns a default variation key
|
307
|
+
# You have to make sure that proper error handling is set up in your code as shown in the example to the right
|
308
|
+
# to catch potential exceptions.
|
239
309
|
#
|
240
310
|
# @param [String] visitor_code
|
241
|
-
# @param [String
|
311
|
+
# @param [String] feature_key
|
242
312
|
#
|
243
|
-
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound]
|
244
|
-
# @raise [Kameleoon::Exception::NotTargeted]
|
313
|
+
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound] Feature Flag isn't found in this configuration
|
245
314
|
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
246
315
|
#
|
247
|
-
def
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
if @blocking
|
252
|
-
result = nil
|
253
|
-
EM.synchrony do
|
254
|
-
connexion_options = { :connect_timeout => (timeout.to_f / 1000.0) }
|
255
|
-
request_options = {
|
256
|
-
:path => get_experiment_register_url(visitor_code, id),
|
257
|
-
:body => (data_not_sent(visitor_code).values.map { |data| data.obtain_full_post_text_line }.join("\n") || "").encode("UTF-8")
|
258
|
-
}
|
259
|
-
log "Activate feature request: " + request_options.inspect
|
260
|
-
log "Activate feature connexion:" + connexion_options.inspect
|
261
|
-
request = EM::Synchrony.sync post(request_options, @tracking_url, connexion_options)
|
262
|
-
if is_successful(request)
|
263
|
-
result = request.response
|
264
|
-
else
|
265
|
-
log "Failed to get activation:" + result.inspect
|
266
|
-
end
|
267
|
-
EM.stop
|
268
|
-
end
|
269
|
-
raise Exception::FeatureConfigurationNotFound.new(id) if result.nil?
|
270
|
-
result.to_s != "null"
|
316
|
+
def get_feature_variation_key(visitor_code, feature_key)
|
317
|
+
_, variation_key = _get_feature_variation_key(visitor_code, feature_key)
|
318
|
+
variation_key
|
319
|
+
end
|
271
320
|
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
321
|
+
##
|
322
|
+
# Retrieves a feature variable value from assigned for visitor variation
|
323
|
+
#
|
324
|
+
# A feature variable can be changed easily via our web application.
|
325
|
+
#
|
326
|
+
# @param [String] visitor_code
|
327
|
+
# @param [String] feature_key
|
328
|
+
# @param [String] variable_name
|
329
|
+
#
|
330
|
+
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound] Feature Flag isn't found in this configuration
|
331
|
+
# @raise [Kameleoon::Exception::FeatureVariableNotFound]
|
332
|
+
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
333
|
+
#
|
334
|
+
def get_feature_variable(visitor_code, feature_key, variable_name)
|
335
|
+
feature_flag, variation_key = _get_feature_variation_key(visitor_code, feature_key)
|
336
|
+
variation = feature_flag.get_variation_key(variation_key)
|
337
|
+
variable = variation&.get_variable_by_key(variable_name)
|
338
|
+
if variable.nil?
|
339
|
+
raise Exception::FeatureVariableNotFound.new(variable_name),
|
340
|
+
"Feature variable #{variable_name} not found"
|
341
|
+
end
|
342
|
+
_parse_feature_variable(variable)
|
343
|
+
end
|
278
344
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
345
|
+
##
|
346
|
+
# Retrieves all feature variable values for a given variation
|
347
|
+
#
|
348
|
+
# This method takes a feature_key and variation_key as mandatory arguments and
|
349
|
+
# returns a list of variables for a given variation key
|
350
|
+
# A feature variable can be changed easily via our web application.
|
351
|
+
#
|
352
|
+
# @param [String] feature_key
|
353
|
+
# @param [String] variation_key
|
354
|
+
#
|
355
|
+
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound] Feature Flag isn't found in this configuration
|
356
|
+
# @raise [Kameleoon::Exception::VariationConfigurationNotFound]
|
357
|
+
#
|
358
|
+
def get_feature_all_variables(feature_key, variation_key)
|
359
|
+
feature_flag = find_feature_flag_v2(feature_key)
|
360
|
+
variation = feature_flag.get_variation_key(variation_key)
|
361
|
+
if variation.nil?
|
362
|
+
raise Exception::VariationConfigurationNotFound.new(variation_key),
|
363
|
+
"Variation key #{variation_key} not found"
|
291
364
|
end
|
365
|
+
variables = {}
|
366
|
+
variation.variables.each { |var| variables[var.key] = _parse_feature_variable(var) }
|
367
|
+
variables
|
292
368
|
end
|
293
369
|
|
294
370
|
##
|
@@ -299,32 +375,34 @@ module Kameleoon
|
|
299
375
|
# @param [String | Integer] feature_key
|
300
376
|
# @param [String] variable_key
|
301
377
|
#
|
302
|
-
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound]
|
378
|
+
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound] Feature Flag isn't found in this configuration
|
303
379
|
# @raise [Kameleoon::Exception::FeatureVariableNotFound]
|
304
380
|
#
|
381
|
+
# DEPRECATED. Please use `get_feature_variable` instead.
|
305
382
|
def obtain_feature_variable(feature_key, variable_key)
|
306
|
-
|
307
|
-
|
383
|
+
warn '[DEPRECATION] `obtain_feature_variable` is deprecated. Please use `get_feature_variable` instead.'
|
384
|
+
feature_flag = find_feature_flag(feature_key)
|
385
|
+
custom_json = JSON.parse(feature_flag.variations.first['customJson'])[variable_key.to_s]
|
308
386
|
if custom_json.nil?
|
309
|
-
raise Exception::FeatureVariableNotFound.new(
|
387
|
+
raise Exception::FeatureVariableNotFound.new('Feature variable not found')
|
310
388
|
end
|
311
389
|
case custom_json['type']
|
312
|
-
when
|
313
|
-
|
314
|
-
when
|
315
|
-
|
316
|
-
when
|
317
|
-
|
318
|
-
when
|
319
|
-
|
390
|
+
when 'Boolean'
|
391
|
+
custom_json['value']
|
392
|
+
when 'String'
|
393
|
+
custom_json['value']
|
394
|
+
when 'Number'
|
395
|
+
custom_json['value'].to_i
|
396
|
+
when 'JSON'
|
397
|
+
JSON.parse(custom_json['value'])
|
320
398
|
else
|
321
|
-
raise TypeError.new(
|
399
|
+
raise TypeError.new('Unknown type for feature variable')
|
322
400
|
end
|
323
401
|
end
|
324
402
|
|
325
403
|
##
|
326
|
-
# The retrieved_data_from_remote_source method allows you to retrieve data (according to a key passed as argument)
|
327
|
-
# stored on a remote Kameleoon server. Usually data will be stored on our remote servers via the use of our Data API.
|
404
|
+
# The retrieved_data_from_remote_source method allows you to retrieve data (according to a key passed as argument)
|
405
|
+
# stored on a remote Kameleoon server. Usually data will be stored on our remote servers via the use of our Data API.
|
328
406
|
# This method, along with the availability of our highly scalable servers for this purpose, provides a convenient way
|
329
407
|
# to quickly store massive amounts of data that can be later retrieved for each of your visitors / users.
|
330
408
|
#
|
@@ -339,247 +417,136 @@ module Kameleoon
|
|
339
417
|
path = get_api_data_request_url(key)
|
340
418
|
log "Retrieve API Data connexion: #{connexion_options.inspect}"
|
341
419
|
response = get_sync(@api_data_url + path, connexion_options)
|
342
|
-
if
|
420
|
+
if successful_sync?(response)
|
343
421
|
JSON.parse(response.body) unless response.nil?
|
344
422
|
else
|
345
423
|
return nil
|
346
424
|
end
|
347
425
|
end
|
348
426
|
|
427
|
+
##
|
428
|
+
# Returns a list of all experiment ids
|
429
|
+
#
|
430
|
+
# @return [Array] array of all experiment ids
|
431
|
+
def get_experiment_list
|
432
|
+
@experiments.map { |it| it.id.to_i }
|
433
|
+
end
|
434
|
+
|
435
|
+
##
|
436
|
+
# Returns a list of all experiment ids targeted for a visitor
|
437
|
+
# if only_allocated is `true` returns a list of allocated experiments for a visitor
|
438
|
+
#
|
439
|
+
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
440
|
+
#
|
441
|
+
# @return [Array] array of all experiment ids accorging to a only_allocated parameter
|
442
|
+
def get_experiment_list_for_visitor(visitor_code, only_allocated: true)
|
443
|
+
list_ids = []
|
444
|
+
@experiments.each do |experiment|
|
445
|
+
next unless check_targeting(visitor_code, experiment.id.to_i, experiment)
|
446
|
+
next if only_allocated && variation_for_experiment(visitor_code, experiment).nil?
|
447
|
+
|
448
|
+
list_ids.push(experiment.id.to_i)
|
449
|
+
end
|
450
|
+
list_ids
|
451
|
+
end
|
452
|
+
|
453
|
+
##
|
454
|
+
# Returns a list of all feature flag keys
|
455
|
+
#
|
456
|
+
# @return [Array] array of all feature flag keys
|
457
|
+
def get_feature_list
|
458
|
+
@feature_flags_v2.map(&:feature_key)
|
459
|
+
end
|
460
|
+
|
461
|
+
##
|
462
|
+
# Returns a list of active feature flag keys for a visitor
|
463
|
+
#
|
464
|
+
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
465
|
+
#
|
466
|
+
# @return [Array] array of active feature flag keys for a visitor
|
467
|
+
def get_active_feature_list_for_visitor(visitor_code)
|
468
|
+
check_visitor_code(visitor_code)
|
469
|
+
list_keys = []
|
470
|
+
@feature_flags_v2.each do |feature_flag|
|
471
|
+
variation_key, = _calculate_variation_key_for_feature(visitor_code, feature_flag)
|
472
|
+
list_keys.push(feature_flag.feature_key) if variation_key != Kameleoon::Configuration::VariationType::VARIATION_OFF
|
473
|
+
end
|
474
|
+
list_keys
|
475
|
+
end
|
476
|
+
|
349
477
|
private
|
350
478
|
|
351
479
|
API_SSX_URL = 'https://api-ssx.kameleoon.com'
|
352
480
|
REFERENCE = 0
|
353
|
-
STATUS_ACTIVE = 'ACTIVE'
|
354
|
-
FEATURE_STATUS_DEACTIVATED = 'DEACTIVATED'
|
355
481
|
DEFAULT_ENVIRONMENT = 'production'
|
356
|
-
attr :site_code, :client_id, :client_secret, :access_token, :experiments, :feature_flags, :scheduler, :data,
|
357
|
-
:
|
482
|
+
attr :site_code, :client_id, :client_secret, :access_token, :experiments, :feature_flags, :feature_flags_v2, :scheduler, :data,
|
483
|
+
:tracking_url, :default_timeout, :interval, :memory_limit, :verbose_mode
|
358
484
|
|
359
485
|
def fetch_configuration
|
360
486
|
@scheduler = Rufus::Scheduler.singleton
|
361
487
|
@scheduler.every @interval do
|
362
488
|
log('Scheduled job to fetch configuration is starting.')
|
363
|
-
|
489
|
+
fetch_configuration_job
|
364
490
|
end
|
365
491
|
@scheduler.schedule '0s' do
|
366
492
|
log('Start-up, fetching is starting')
|
367
|
-
|
368
|
-
end
|
369
|
-
end
|
370
|
-
|
371
|
-
def fetch_configuration_job_graphql
|
372
|
-
EM.synchrony do
|
373
|
-
obtain_access_token
|
374
|
-
@experiments = obtain_experiments_graphql(@site_code) || @experiments
|
375
|
-
@feature_flags = obtain_feature_flags_graphql(@site_code, @environment) || @feature_flags
|
376
|
-
EM.stop
|
493
|
+
fetch_configuration_job
|
377
494
|
end
|
378
495
|
end
|
379
496
|
|
380
497
|
def fetch_configuration_job
|
381
498
|
EM.synchrony do
|
382
|
-
|
383
|
-
site = obtain_site
|
384
|
-
if site.nil? || site.empty?
|
385
|
-
@experiments ||= []
|
386
|
-
@feature_flags ||= []
|
387
|
-
else
|
388
|
-
site_id = site.first['id']
|
389
|
-
@experiments = obtain_tests(site_id) || @experiments
|
390
|
-
@feature_flags = obtain_feature_flags(site_id) || @feature_flags
|
391
|
-
end
|
499
|
+
obtain_configuration(@site_code, @environment)
|
392
500
|
EM.stop
|
393
501
|
end
|
394
502
|
end
|
395
503
|
|
396
|
-
def hash_headers
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
end
|
405
|
-
|
406
|
-
def hash_filter(field, operator, parameters)
|
407
|
-
|
408
|
-
end
|
409
|
-
|
410
|
-
def
|
411
|
-
log "Fetching
|
412
|
-
|
413
|
-
|
414
|
-
request =
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
if request != false
|
443
|
-
JSON.parse(request.response)
|
444
|
-
end
|
445
|
-
end
|
446
|
-
|
447
|
-
def obtain_segment(segment_id)
|
448
|
-
request = fetch_one('segments/' + segment_id.to_s)
|
449
|
-
if request != false
|
450
|
-
JSON.parse(request.response)
|
451
|
-
end
|
452
|
-
end
|
453
|
-
|
454
|
-
def complete_experiment(experiment)
|
455
|
-
unless experiment['variations'].nil?
|
456
|
-
experiment['variations'] = experiment['variations'].map { |variationId| obtain_variation(variationId) }
|
457
|
-
end
|
458
|
-
unless experiment['targetingSegmentId'].nil?
|
459
|
-
experiment['targetingSegment'] = Kameleoon::Targeting::Segment.new(obtain_segment(experiment['targetingSegmentId']))
|
460
|
-
end
|
461
|
-
experiment
|
462
|
-
end
|
463
|
-
|
464
|
-
# fetching segment for both types: experiments and feature_flags (campaigns)
|
465
|
-
def complete_campaign_graphql(campaign)
|
466
|
-
campaign['id'] = campaign['id'].to_i
|
467
|
-
campaign['status'] = campaign['status']
|
468
|
-
unless campaign['deviations'].nil?
|
469
|
-
campaign['deviations'] = Hash[*campaign['deviations'].map { |it| [ it['variationId'] == '0' ? 'origin' : it['variationId'], it['value'] ] }.flatten]
|
470
|
-
end
|
471
|
-
unless campaign['respoolTime'].nil?
|
472
|
-
campaign['respoolTime'] = Hash[*campaign['respoolTime'].map { |it| [ it['variationId'] == '0' ? 'origin' : it['variationId'], it['value'] ] }.flatten]
|
473
|
-
end
|
474
|
-
unless campaign['variations'].nil?
|
475
|
-
campaign['variations'] = campaign['variations'].map { |it| {'id' => it['id'].to_i, 'customJson' => it['customJson']} }
|
476
|
-
end
|
477
|
-
unless campaign['segment'].nil?
|
478
|
-
campaign['targetingSegment'] = Kameleoon::Targeting::Segment.new((campaign['segment']))
|
479
|
-
end
|
480
|
-
campaign
|
481
|
-
end
|
482
|
-
|
483
|
-
def complete_experiment(experiment)
|
484
|
-
unless experiment['variations'].nil?
|
485
|
-
experiment['variations'] = experiment['variations'].map { |variationId| obtain_variation(variationId) }
|
486
|
-
end
|
487
|
-
unless experiment['targetingSegmentId'].nil?
|
488
|
-
experiment['targetingSegment'] = Kameleoon::Targeting::Segment.new(obtain_segment(experiment['targetingSegmentId']))
|
489
|
-
end
|
490
|
-
experiment
|
491
|
-
end
|
492
|
-
|
493
|
-
def obtain_experiments_graphql(site_code, per_page = -1)
|
494
|
-
log "Fetching experiments GraphQL"
|
495
|
-
experiments = fetch_all_graphql("v1/graphql?perPage=#{per_page}", Kameleoon::Query.query_experiments(site_code)).map { |it| JSON.parse(it.response)['data']['experiments']['edges'] }.flatten.map do |experiment|
|
496
|
-
complete_campaign_graphql(experiment['node'])
|
497
|
-
end
|
498
|
-
log "Experiment are fetched: " + experiments.inspect
|
499
|
-
experiments
|
500
|
-
end
|
501
|
-
|
502
|
-
def obtain_tests(site_id, per_page = -1)
|
503
|
-
log "Fetching experiments"
|
504
|
-
query_values = { 'perPage' => per_page }
|
505
|
-
filters = [
|
506
|
-
hash_filter('siteId', 'EQUAL', [site_id]),
|
507
|
-
hash_filter('status', 'IN', ['ACTIVE', 'DEVIATED']),
|
508
|
-
hash_filter('type', 'IN', ['SERVER_SIDE', 'HYBRID'])
|
509
|
-
]
|
510
|
-
experiments = fetch_all('experiments', query_values, filters).map { |it| JSON.parse(it.response) }.flatten.map do |test|
|
511
|
-
complete_experiment(test)
|
512
|
-
end
|
513
|
-
log "Experiment are fetched: " + experiments.inspect
|
514
|
-
experiments
|
515
|
-
end
|
516
|
-
|
517
|
-
def obtain_feature_flags_graphql(site_id, environment = @environment, per_page = -1)
|
518
|
-
log "Fetching feature flags GraphQL"
|
519
|
-
feature_flags = fetch_all_graphql("v1/graphql?perPage=#{per_page}", Kameleoon::Query.query_feature_flags(site_code, environment)).map { |it| JSON.parse(it.response)['data']['featureFlags']['edges'] }.flatten.map do |feature_flag|
|
520
|
-
complete_campaign_graphql(feature_flag['node'])
|
521
|
-
end
|
522
|
-
log "Feature flags are fetched: " + feature_flags.inspect
|
523
|
-
feature_flags
|
524
|
-
end
|
525
|
-
|
526
|
-
def obtain_feature_flags(site_id, per_page = -1)
|
527
|
-
log "Fetching feature flags"
|
528
|
-
query_values = { 'perPage' => per_page }
|
529
|
-
filters = [
|
530
|
-
hash_filter('siteId', 'EQUAL', [site_id]),
|
531
|
-
hash_filter('status', 'IN', ['ACTIVE', 'PAUSED'])
|
532
|
-
]
|
533
|
-
feature_flags = fetch_all('feature-flags', query_values, filters).map { |it| JSON.parse(it.response) }.flatten.map do |ff|
|
534
|
-
complete_experiment(ff)
|
535
|
-
end
|
536
|
-
log "Feature flags are fetched: " + feature_flags.inspect
|
537
|
-
feature_flags
|
538
|
-
end
|
539
|
-
|
540
|
-
def fetch_all_graphql(path, query_graphql = {})
|
541
|
-
results = []
|
542
|
-
current_page = 1
|
543
|
-
loop do
|
544
|
-
http = fetch_one_graphql(path, query_graphql)
|
545
|
-
break if http == false
|
546
|
-
results.push(http)
|
547
|
-
break if http.response_header["X-Pagination-Page-Count"].to_i <= current_page
|
548
|
-
current_page += 1
|
549
|
-
end
|
550
|
-
results
|
551
|
-
end
|
552
|
-
|
553
|
-
def fetch_all(path, query_values = {}, filters = [])
|
554
|
-
results = []
|
555
|
-
current_page = 1
|
556
|
-
loop do
|
557
|
-
query_values['page'] = current_page
|
558
|
-
http = fetch_one(path, query_values, filters)
|
559
|
-
break if http == false
|
560
|
-
results.push(http)
|
561
|
-
break if http.response_header["X-Pagination-Page-Count"].to_i <= current_page
|
562
|
-
current_page += 1
|
563
|
-
end
|
564
|
-
results
|
565
|
-
end
|
566
|
-
|
567
|
-
def fetch_one_graphql(path, query_graphql = {})
|
568
|
-
request = EM::Synchrony.sync post({ :path => path, :body => query_graphql, :head => hash_headers })
|
569
|
-
unless is_successful(request)
|
570
|
-
log "Failed to fetch" + request.inspect
|
571
|
-
return false
|
572
|
-
end
|
573
|
-
request
|
574
|
-
end
|
575
|
-
|
576
|
-
def fetch_one(path, query_values = {}, filters = [])
|
577
|
-
unless filters.empty?
|
578
|
-
query_values['filter'] = filters.to_json
|
579
|
-
end
|
580
|
-
request = EM::Synchrony.sync get({ :path => path, :query => query_values, :head => hash_headers })
|
581
|
-
unless is_successful(request)
|
582
|
-
log "Failed to fetch" + request.inspect
|
504
|
+
# def hash_headers
|
505
|
+
# if @access_token.nil?
|
506
|
+
# CredentialsNotFound.new
|
507
|
+
# end
|
508
|
+
# {
|
509
|
+
# 'Authorization' => 'Bearer ' + @access_token.to_s,
|
510
|
+
# 'Content-Type' => 'application/json'
|
511
|
+
# }
|
512
|
+
# end
|
513
|
+
|
514
|
+
# def hash_filter(field, operator, parameters)
|
515
|
+
# { 'field' => field, 'operator' => operator, 'parameters' => parameters }
|
516
|
+
# end
|
517
|
+
|
518
|
+
def obtain_configuration(site_code, environment = @environment)
|
519
|
+
log "Fetching configuration from Client-Config service"
|
520
|
+
request_path = "mobile?siteCode=#{site_code}"
|
521
|
+
request_path += "&environment=#{environment}" unless environment.nil?
|
522
|
+
request = request_configuration(request_path)
|
523
|
+
return unless request
|
524
|
+
|
525
|
+
configuration = JSON.parse(request.response)
|
526
|
+
@experiments = Kameleoon::Configuration::Experiment.create_from_array(configuration['experiments']) ||
|
527
|
+
@experiments
|
528
|
+
@feature_flags = Kameleoon::Configuration::FeatureFlag.create_from_array(configuration['featureFlags']) ||
|
529
|
+
@feature_flags
|
530
|
+
feature_flags_v2_new = Kameleoon::Configuration::FeatureFlagV2.create_from_array(
|
531
|
+
configuration['featureFlagConfigurations']
|
532
|
+
)
|
533
|
+
@feature_flags_v2 = feature_flags_v2_new || @feature_flags_v2
|
534
|
+
log "Feature flags are fetched: #{request.inspect}"
|
535
|
+
end
|
536
|
+
|
537
|
+
def variation_for_experiment(visitor_code, experiment)
|
538
|
+
threshold = obtain_hash_double(visitor_code, experiment.respool_time, experiment.id)
|
539
|
+
experiment.deviations.each do |key, value|
|
540
|
+
threshold -= value
|
541
|
+
return key.to_s.to_i if threshold.negative?
|
542
|
+
end
|
543
|
+
nil
|
544
|
+
end
|
545
|
+
|
546
|
+
def request_configuration(path)
|
547
|
+
request = EM::Synchrony.sync get({ path: path }, CLIENT_CONFIG_URL)
|
548
|
+
unless successful?(request)
|
549
|
+
log "Failed to fetch #{request.inspect}"
|
583
550
|
return false
|
584
551
|
end
|
585
552
|
request
|
@@ -587,27 +554,27 @@ module Kameleoon
|
|
587
554
|
|
588
555
|
def get_common_ssx_parameters(visitor_code)
|
589
556
|
{
|
590
|
-
:
|
591
|
-
:
|
592
|
-
:
|
557
|
+
nonce: Kameleoon::Utils.generate_random_string(16),
|
558
|
+
siteCode: @site_code,
|
559
|
+
visitorCode: visitor_code
|
593
560
|
}
|
594
561
|
end
|
595
562
|
|
596
563
|
def get_experiment_register_url(visitor_code, experiment_id, variation_id = nil, none_variation = false)
|
597
|
-
url = "/experimentTracking
|
598
|
-
url += "&experimentId
|
564
|
+
url = "/experimentTracking?#{URI.encode_www_form(get_common_ssx_parameters(visitor_code))}"
|
565
|
+
url += "&experimentId=#{experiment_id}"
|
599
566
|
if variation_id.nil?
|
600
567
|
return url
|
601
568
|
end
|
602
|
-
url += "&variationId
|
569
|
+
url += "&variationId=#{variation_id}"
|
603
570
|
if none_variation
|
604
|
-
url +=
|
571
|
+
url += '&noneVariation=true'
|
605
572
|
end
|
606
573
|
url
|
607
574
|
end
|
608
575
|
|
609
576
|
def get_data_register_url(visitor_code)
|
610
|
-
"/dataTracking
|
577
|
+
"/dataTracking?#{URI.encode_www_form(get_common_ssx_parameters(visitor_code))}"
|
611
578
|
end
|
612
579
|
|
613
580
|
def get_api_data_request_url(key)
|
@@ -618,121 +585,210 @@ module Kameleoon
|
|
618
585
|
"/data?#{URI.encode_www_form(mapKey)}"
|
619
586
|
end
|
620
587
|
|
621
|
-
def
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
588
|
+
def find_feature_flag(feature_key)
|
589
|
+
case feature_key
|
590
|
+
when String
|
591
|
+
feature_flag = @feature_flags.select { |ff| ff.identification_key == feature_key }.first
|
592
|
+
when Integer
|
593
|
+
feature_flag = @feature_flags.select { |ff| ff.id.to_i == feature_key }.first
|
626
594
|
print "\nPassing `feature_key` with type of `int` to `activate_feature` or `obtain_feature_variable` "\
|
627
595
|
"is deprecated, it will be removed in next releases. This is necessary to support multi-environment feature\n"
|
628
596
|
else
|
629
|
-
raise TypeError.new(
|
630
|
-
|
631
|
-
if feature_flag.nil?
|
632
|
-
raise Exception::FeatureConfigurationNotFound.new(feature_key)
|
597
|
+
raise TypeError.new('Feature key should be a String or an Integer.'),
|
598
|
+
'Feature key should be a String or an Integer.'
|
633
599
|
end
|
600
|
+
error_message = "Feature #{feature_key} not found"
|
601
|
+
raise Exception::FeatureConfigurationNotFound.new(feature_key), error_message if feature_flag.nil?
|
602
|
+
|
634
603
|
feature_flag
|
635
604
|
end
|
636
605
|
|
637
|
-
def
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
if (schedule['dateStart'].nil? || Time.parse(schedule['dateStart']).to_i < date) &&
|
644
|
-
(schedule['dateEnd'].nil? || Time.parse(schedule['dateEnd']).to_i > date)
|
645
|
-
return true
|
646
|
-
end
|
606
|
+
def find_feature_flag_v2(feature_key)
|
607
|
+
if feature_key.is_a?(String)
|
608
|
+
feature_flag = @feature_flags_v2.select { |ff| ff.feature_key == feature_key }.first
|
609
|
+
else
|
610
|
+
raise TypeError.new('Feature key should be a String or an Integer.'),
|
611
|
+
'Feature key should be a String or an Integer.'
|
647
612
|
end
|
648
|
-
|
613
|
+
error_message = "Feature #{feature_key} not found"
|
614
|
+
raise Exception::FeatureConfigurationNotFound.new(feature_key), error_message if feature_flag.nil?
|
615
|
+
|
616
|
+
feature_flag
|
649
617
|
end
|
650
618
|
|
651
|
-
def track_experiment(visitor_code, experiment_id, variation_id = nil, none_variation
|
619
|
+
def track_experiment(visitor_code, experiment_id, variation_id = nil, none_variation: false)
|
652
620
|
data_not_sent = data_not_sent(visitor_code)
|
653
621
|
options = {
|
654
|
-
:
|
655
|
-
:
|
656
|
-
:
|
622
|
+
path: get_experiment_register_url(visitor_code, experiment_id, variation_id, none_variation),
|
623
|
+
body: (data_not_sent.map(&:obtain_full_post_text_line).join("\n") || '').encode('UTF-8'),
|
624
|
+
head: { 'Content-Type': 'text/plain' }
|
657
625
|
}
|
626
|
+
set_user_agent_to_headers(visitor_code, options[:head])
|
658
627
|
trial = 0
|
659
628
|
success = false
|
660
|
-
log "Start post tracking experiment:
|
629
|
+
log "Start post tracking experiment: #{data_not_sent.inspect}"
|
661
630
|
Thread.new do
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
break
|
671
|
-
end
|
672
|
-
trial += 1
|
631
|
+
while trial < 10
|
632
|
+
log "Send Experiment Tracking #{options.inspect}"
|
633
|
+
response = post_sync(options, @tracking_url)
|
634
|
+
log "Response #{response.inspect}"
|
635
|
+
if successful_sync?(response)
|
636
|
+
data_not_sent.each { |it| it.sent = true }
|
637
|
+
success = true
|
638
|
+
break
|
673
639
|
end
|
674
|
-
|
640
|
+
trial += 1
|
675
641
|
end
|
676
642
|
if success
|
677
|
-
log "Post to experiment tracking is done after
|
678
|
-
|
679
|
-
log "Post to experiment tracking is failed after
|
643
|
+
log "Post to experiment tracking is done after #{trial + 1} trials"
|
644
|
+
else
|
645
|
+
log "Post to experiment tracking is failed after #{trial} trials"
|
680
646
|
end
|
681
|
-
Thread.exit
|
682
647
|
end
|
683
648
|
end
|
684
649
|
|
685
|
-
def track_data(visitor_code
|
650
|
+
def track_data(visitor_code)
|
686
651
|
Thread.new do
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
end
|
705
|
-
iter.return(request)
|
706
|
-
}
|
707
|
-
request.errback { iter.return(request) }
|
708
|
-
end
|
709
|
-
data_not_sent = data_not_sent(visitor_code)
|
710
|
-
trials -= 1
|
652
|
+
trials = 0
|
653
|
+
data_not_sent = data_not_sent(visitor_code)
|
654
|
+
log "Start post tracking data: #{data_not_sent.inspect}"
|
655
|
+
while trials < 10 && !data_not_sent.empty?
|
656
|
+
options = {
|
657
|
+
path: get_data_register_url(visitor_code),
|
658
|
+
body: (data_not_sent.map(&:obtain_full_post_text_line).join("\n") || '').encode('UTF-8'),
|
659
|
+
head: { 'Content-Type': 'text/plain' }
|
660
|
+
}
|
661
|
+
set_user_agent_to_headers(visitor_code, options[:head])
|
662
|
+
log "Post tracking data for visitor_code: #{visitor_code} with options: #{options.inspect}"
|
663
|
+
response = post_sync(options, @tracking_url)
|
664
|
+
log "Response #{response.inspect}"
|
665
|
+
if successful_sync?(response)
|
666
|
+
data_not_sent.each { |it| it.sent = true }
|
667
|
+
success = true
|
668
|
+
break
|
711
669
|
end
|
712
|
-
|
713
|
-
EM.stop
|
670
|
+
trials += 1
|
714
671
|
end
|
715
|
-
|
672
|
+
if success
|
673
|
+
log "Post to data tracking is done after #{trials + 1} trials"
|
674
|
+
else
|
675
|
+
log "Post to data tracking is failed after #{trials} trials"
|
676
|
+
end
|
677
|
+
end
|
678
|
+
end
|
679
|
+
|
680
|
+
def check_site_code_enable(campaign)
|
681
|
+
raise Exception::SiteCodeDisabled.new(site_code), site_code unless campaign.site_enabled
|
682
|
+
end
|
683
|
+
|
684
|
+
def data_not_sent(visitor_code)
|
685
|
+
@data.key?(visitor_code) ? @data[visitor_code].reject(&:sent) : []
|
686
|
+
end
|
687
|
+
|
688
|
+
def log(text)
|
689
|
+
print "Kameleoon Log: #{text}\n" if @verbose_mode
|
690
|
+
end
|
691
|
+
|
692
|
+
def add_user_agent_data(visitor_code, user_agent)
|
693
|
+
@user_agents[visitor_code] = user_agent
|
694
|
+
end
|
695
|
+
|
696
|
+
def set_user_agent_to_headers(visitor_code, headers)
|
697
|
+
user_agent = @user_agents[visitor_code]
|
698
|
+
headers['User-Agent'] = user_agent.value unless user_agent.nil?
|
699
|
+
end
|
700
|
+
|
701
|
+
def get_valid_saved_variation(visitor_code, experiment)
|
702
|
+
variation_id = @variation_storage.get_variation_id(visitor_code, experiment.id.to_i)
|
703
|
+
unless variation_id.nil?
|
704
|
+
return @variation_storage.variation_valid?(visitor_code,
|
705
|
+
experiment.id.to_i,
|
706
|
+
experiment.respool_time[variation_id])
|
707
|
+
end
|
708
|
+
nil
|
709
|
+
end
|
710
|
+
|
711
|
+
def check_targeting(visitor_code, campaign_id, exp_ff_rule)
|
712
|
+
segment = exp_ff_rule.targeting_segment
|
713
|
+
segment.nil? || segment.check_tree(->(type) { get_condition_data(type, visitor_code, campaign_id) })
|
714
|
+
end
|
715
|
+
|
716
|
+
def get_condition_data(type, visitor_code, campaign_id)
|
717
|
+
condition_data = nil
|
718
|
+
case type
|
719
|
+
when Kameleoon::Targeting::ConditionType::CUSTOM_DATUM
|
720
|
+
condition_data = (@data[visitor_code] || []).flatten
|
721
|
+
when Kameleoon::Targeting::ConditionType::TARGET_EXPERIMENT
|
722
|
+
condition_data = @variation_storage.get_hash_saved_variation_id(visitor_code)
|
723
|
+
when Kameleoon::Targeting::ConditionType::EXCLUSIVE_EXPERIMENT
|
724
|
+
condition_data = OpenStruct.new
|
725
|
+
condition_data.experiment_id = campaign_id
|
726
|
+
condition_data.storage = @variation_storage.get_hash_saved_variation_id(visitor_code)
|
716
727
|
end
|
728
|
+
condition_data
|
717
729
|
end
|
718
730
|
|
719
|
-
|
720
|
-
|
721
|
-
|
731
|
+
##
|
732
|
+
# helper method for getting variation key for feature flag
|
733
|
+
def _get_feature_variation_key(visitor_code, feature_key)
|
734
|
+
check_visitor_code(visitor_code)
|
735
|
+
feature_flag = find_feature_flag_v2(feature_key)
|
736
|
+
variation_key, rule = _calculate_variation_key_for_feature(visitor_code, feature_flag)
|
737
|
+
if !rule.nil? && rule.type == Kameleoon::Configuration::RuleType::EXPERIMENTATION
|
738
|
+
_send_tracking_request(visitor_code, variation_key, rule)
|
722
739
|
end
|
740
|
+
[feature_flag, variation_key]
|
723
741
|
end
|
724
742
|
|
725
|
-
|
726
|
-
|
727
|
-
|
743
|
+
##
|
744
|
+
# helper method for calculate variation key for feature flag
|
745
|
+
def _calculate_variation_key_for_feature(visitor_code, feature_flag)
|
746
|
+
# no rules -> return default_variation_key
|
747
|
+
unless feature_flag.rules.empty?
|
748
|
+
# uses for rule exposition
|
749
|
+
hash_rule = obtain_hash_double_v2(visitor_code, feature_flag.id)
|
750
|
+
# uses for variation's expositions
|
751
|
+
hash_variation = obtain_hash_double_v2(visitor_code, feature_flag.id, 'variation')
|
752
|
+
feature_flag.rules.each do |rule|
|
753
|
+
# check if visitor is targeted for rule, else next rule
|
754
|
+
next unless check_targeting(visitor_code, feature_flag.id, rule)
|
755
|
+
|
756
|
+
# check main expostion for rule with hashRule
|
757
|
+
if hash_rule < rule.exposition
|
758
|
+
# get variation key with new hashVariation
|
759
|
+
variation_key = rule.get_variation_key(hash_variation)
|
760
|
+
# variation_key can be nil for experiment rules only, for targeted rule will be always exist
|
761
|
+
return [variation_key, rule] unless variation_key.nil?
|
762
|
+
# if visitor is targeted for targeted rule then break cycle -> return default
|
763
|
+
elsif rule.type == Kameleoon::Configuration::RuleType::TARGETED_DELIVERY
|
764
|
+
break
|
765
|
+
end
|
766
|
+
end
|
767
|
+
end
|
768
|
+
[feature_flag.default_variation_key, nil]
|
769
|
+
end
|
770
|
+
|
771
|
+
##
|
772
|
+
# helper method for sending tracking requests for new FF v2
|
773
|
+
def _send_tracking_request(visitor_code, variation_key, rule)
|
774
|
+
if !rule.experiment_id.nil?
|
775
|
+
track_experiment(visitor_code, rule.experiment_id, rule.get_variation_id_by_key(variation_key))
|
728
776
|
else
|
729
|
-
|
777
|
+
log 'An attempt to send a request with null experimentId was blocked'
|
730
778
|
end
|
731
779
|
end
|
732
780
|
|
733
|
-
|
734
|
-
|
735
|
-
|
781
|
+
##
|
782
|
+
# helper method for fetching values from a Variable
|
783
|
+
def _parse_feature_variable(variable)
|
784
|
+
case variable.type
|
785
|
+
when 'BOOLEAN', 'STRING', 'NUMBER'
|
786
|
+
variable.value
|
787
|
+
when 'JSON'
|
788
|
+
JSON.parse(variable.value)
|
789
|
+
else
|
790
|
+
raise TypeError.new('Unknown type for feature variable'),
|
791
|
+
'Unknown type for feature variable'
|
736
792
|
end
|
737
793
|
end
|
738
794
|
end
|