kameleoon-client-ruby 1.1.2 → 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 +456 -390
- 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 +14 -13
- 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,56 +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
|
-
request = EM::Synchrony.sync post(request_options, @tracking_url, connexion_options)
|
113
|
-
if is_successful(request)
|
114
|
-
variation_id = request.response
|
115
|
-
else
|
116
|
-
log "Failed to trigger experiment: " + request.inspect
|
117
|
-
raise Exception::ExperimentConfigurationNotFound.new(experiment_id) if variation_id.nil?
|
118
|
-
end
|
119
|
-
EM.stop
|
120
|
-
end
|
121
|
-
if variation_id.nil? || variation_id.to_s == "null" || variation_id.to_s == ""
|
122
|
-
raise Exception::NotTargeted.new(visitor_code)
|
123
|
-
elsif variation_id.to_s == "0"
|
124
|
-
raise Exception::NotActivated.new(visitor_code)
|
125
|
-
end
|
126
|
-
variation_id.to_i
|
127
|
-
else
|
128
|
-
check_site_code_enable(experiment)
|
129
|
-
visitor_data = @data.select { |key, value| key.to_s == visitor_code }.values.flatten! || []
|
130
|
-
if experiment['targetingSegment'].nil? || experiment['targetingSegment'].check_tree(visitor_data)
|
131
|
-
threshold = obtain_hash_double(visitor_code, experiment['respoolTime'], experiment['id'])
|
132
|
-
experiment['deviations'].each do |key, value|
|
133
|
-
threshold -= value
|
134
|
-
if threshold < 0
|
135
|
-
track_experiment(visitor_code, experiment_id, key)
|
136
|
-
return key.to_s.to_i
|
137
|
-
end
|
138
|
-
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
|
139
126
|
track_experiment(visitor_code, experiment_id, REFERENCE, true)
|
140
|
-
raise Exception::
|
127
|
+
raise Exception::NotAllocated.new(visitor_code),
|
128
|
+
"Experiment #{experiment_id} is not active for visitor #{visitor_code}"
|
141
129
|
end
|
142
|
-
|
130
|
+
else
|
131
|
+
raise Exception::NotTargeted.new(visitor_code),
|
132
|
+
"Experiment #{experiment_id} is not targeted for visitor #{visitor_code}"
|
143
133
|
end
|
144
134
|
end
|
145
135
|
|
@@ -161,12 +151,13 @@ module Kameleoon
|
|
161
151
|
while ObjectSpace.memsize_of(@data) > @data_maximum_size * (2**20) do
|
162
152
|
@data.shift
|
163
153
|
end
|
164
|
-
|
165
|
-
if
|
166
|
-
|
167
|
-
|
168
|
-
@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
|
169
158
|
end
|
159
|
+
@data[visitor_code] = [] unless @data.key?(visitor_code)
|
160
|
+
@data[visitor_code].push(data_element)
|
170
161
|
end
|
171
162
|
end
|
172
163
|
|
@@ -174,8 +165,10 @@ module Kameleoon
|
|
174
165
|
# Track conversions on a particular goal
|
175
166
|
#
|
176
167
|
# This method requires visitor_code and goal_id to track conversion on this particular goal.
|
177
|
-
# In addition, this method also accepts revenue as a third optional argument to track revenue.
|
178
|
-
# 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.
|
179
172
|
#
|
180
173
|
# @param [String] visitor_code Visitor code
|
181
174
|
# @param [Integer] goal_id Id of the goal
|
@@ -212,7 +205,7 @@ module Kameleoon
|
|
212
205
|
##
|
213
206
|
# Obtain variation associated data.
|
214
207
|
#
|
215
|
-
# 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.
|
216
209
|
# The JSON data usually represents some metadata of the variation, and can be configured on our web application
|
217
210
|
# interface or via our Automation API.
|
218
211
|
# This method takes the variationID as a parameter and will return the data as a json string.
|
@@ -224,74 +217,154 @@ module Kameleoon
|
|
224
217
|
#
|
225
218
|
# @raise [Kameleoon::Exception::VariationNotFound] Raise exception if the variation is not found.
|
226
219
|
#
|
227
|
-
def
|
228
|
-
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
|
229
222
|
if variation.nil?
|
230
|
-
raise Exception::VariationConfigurationNotFound.new(variation_id)
|
223
|
+
raise Exception::VariationConfigurationNotFound.new(variation_id),
|
224
|
+
"Variation key #{variation_id} not found"
|
231
225
|
else
|
232
226
|
JSON.parse(variation['customJson'])
|
233
227
|
end
|
234
228
|
end
|
235
229
|
|
236
|
-
#
|
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
|
+
# #
|
237
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
|
+
|
301
|
+
#
|
302
|
+
# get_feature_variation_key returns a variation key for visitor code
|
238
303
|
#
|
239
|
-
# This method takes a
|
240
|
-
#
|
241
|
-
#
|
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.
|
242
309
|
#
|
243
310
|
# @param [String] visitor_code
|
244
|
-
# @param [String
|
311
|
+
# @param [String] feature_key
|
245
312
|
#
|
246
|
-
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound]
|
247
|
-
# @raise [Kameleoon::Exception::NotTargeted]
|
313
|
+
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound] Feature Flag isn't found in this configuration
|
248
314
|
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
249
315
|
#
|
250
|
-
def
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
if @blocking
|
255
|
-
result = nil
|
256
|
-
EM.synchrony do
|
257
|
-
connexion_options = { :connect_timeout => (timeout.to_f / 1000.0) }
|
258
|
-
request_options = {
|
259
|
-
:path => get_experiment_register_url(visitor_code, id),
|
260
|
-
:body => (data_not_sent(visitor_code).map { |data| data.obtain_full_post_text_line }.join("\n") || "").encode("UTF-8")
|
261
|
-
}
|
262
|
-
log "Activate feature request: " + request_options.inspect
|
263
|
-
log "Activate feature connexion:" + connexion_options.inspect
|
264
|
-
request = EM::Synchrony.sync post(request_options, @tracking_url, connexion_options)
|
265
|
-
if is_successful(request)
|
266
|
-
result = request.response
|
267
|
-
else
|
268
|
-
log "Failed to get activation:" + result.inspect
|
269
|
-
end
|
270
|
-
EM.stop
|
271
|
-
end
|
272
|
-
raise Exception::FeatureConfigurationNotFound.new(id) if result.nil?
|
273
|
-
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
|
274
320
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
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
|
281
344
|
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
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"
|
294
364
|
end
|
365
|
+
variables = {}
|
366
|
+
variation.variables.each { |var| variables[var.key] = _parse_feature_variable(var) }
|
367
|
+
variables
|
295
368
|
end
|
296
369
|
|
297
370
|
##
|
@@ -302,32 +375,34 @@ module Kameleoon
|
|
302
375
|
# @param [String | Integer] feature_key
|
303
376
|
# @param [String] variable_key
|
304
377
|
#
|
305
|
-
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound]
|
378
|
+
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound] Feature Flag isn't found in this configuration
|
306
379
|
# @raise [Kameleoon::Exception::FeatureVariableNotFound]
|
307
380
|
#
|
381
|
+
# DEPRECATED. Please use `get_feature_variable` instead.
|
308
382
|
def obtain_feature_variable(feature_key, variable_key)
|
309
|
-
|
310
|
-
|
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]
|
311
386
|
if custom_json.nil?
|
312
|
-
raise Exception::FeatureVariableNotFound.new(
|
387
|
+
raise Exception::FeatureVariableNotFound.new('Feature variable not found')
|
313
388
|
end
|
314
389
|
case custom_json['type']
|
315
|
-
when
|
316
|
-
|
317
|
-
when
|
318
|
-
|
319
|
-
when
|
320
|
-
|
321
|
-
when
|
322
|
-
|
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'])
|
323
398
|
else
|
324
|
-
raise TypeError.new(
|
399
|
+
raise TypeError.new('Unknown type for feature variable')
|
325
400
|
end
|
326
401
|
end
|
327
402
|
|
328
403
|
##
|
329
|
-
# The retrieved_data_from_remote_source method allows you to retrieve data (according to a key passed as argument)
|
330
|
-
# 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.
|
331
406
|
# This method, along with the availability of our highly scalable servers for this purpose, provides a convenient way
|
332
407
|
# to quickly store massive amounts of data that can be later retrieved for each of your visitors / users.
|
333
408
|
#
|
@@ -342,247 +417,136 @@ module Kameleoon
|
|
342
417
|
path = get_api_data_request_url(key)
|
343
418
|
log "Retrieve API Data connexion: #{connexion_options.inspect}"
|
344
419
|
response = get_sync(@api_data_url + path, connexion_options)
|
345
|
-
if
|
420
|
+
if successful_sync?(response)
|
346
421
|
JSON.parse(response.body) unless response.nil?
|
347
422
|
else
|
348
423
|
return nil
|
349
424
|
end
|
350
425
|
end
|
351
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
|
+
|
352
477
|
private
|
353
478
|
|
354
479
|
API_SSX_URL = 'https://api-ssx.kameleoon.com'
|
355
480
|
REFERENCE = 0
|
356
|
-
STATUS_ACTIVE = 'ACTIVE'
|
357
|
-
FEATURE_STATUS_DEACTIVATED = 'DEACTIVATED'
|
358
481
|
DEFAULT_ENVIRONMENT = 'production'
|
359
|
-
attr :site_code, :client_id, :client_secret, :access_token, :experiments, :feature_flags, :scheduler, :data,
|
360
|
-
:
|
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
|
361
484
|
|
362
485
|
def fetch_configuration
|
363
486
|
@scheduler = Rufus::Scheduler.singleton
|
364
487
|
@scheduler.every @interval do
|
365
488
|
log('Scheduled job to fetch configuration is starting.')
|
366
|
-
|
489
|
+
fetch_configuration_job
|
367
490
|
end
|
368
491
|
@scheduler.schedule '0s' do
|
369
492
|
log('Start-up, fetching is starting')
|
370
|
-
|
371
|
-
end
|
372
|
-
end
|
373
|
-
|
374
|
-
def fetch_configuration_job_graphql
|
375
|
-
EM.synchrony do
|
376
|
-
obtain_access_token
|
377
|
-
@experiments = obtain_experiments_graphql(@site_code) || @experiments
|
378
|
-
@feature_flags = obtain_feature_flags_graphql(@site_code, @environment) || @feature_flags
|
379
|
-
EM.stop
|
493
|
+
fetch_configuration_job
|
380
494
|
end
|
381
495
|
end
|
382
496
|
|
383
497
|
def fetch_configuration_job
|
384
498
|
EM.synchrony do
|
385
|
-
|
386
|
-
site = obtain_site
|
387
|
-
if site.nil? || site.empty?
|
388
|
-
@experiments ||= []
|
389
|
-
@feature_flags ||= []
|
390
|
-
else
|
391
|
-
site_id = site.first['id']
|
392
|
-
@experiments = obtain_tests(site_id) || @experiments
|
393
|
-
@feature_flags = obtain_feature_flags(site_id) || @feature_flags
|
394
|
-
end
|
499
|
+
obtain_configuration(@site_code, @environment)
|
395
500
|
EM.stop
|
396
501
|
end
|
397
502
|
end
|
398
503
|
|
399
|
-
def hash_headers
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
end
|
408
|
-
|
409
|
-
def hash_filter(field, operator, parameters)
|
410
|
-
|
411
|
-
end
|
412
|
-
|
413
|
-
def
|
414
|
-
log "Fetching
|
415
|
-
|
416
|
-
|
417
|
-
request =
|
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
|
-
|
443
|
-
|
444
|
-
|
445
|
-
if request != false
|
446
|
-
JSON.parse(request.response)
|
447
|
-
end
|
448
|
-
end
|
449
|
-
|
450
|
-
def obtain_segment(segment_id)
|
451
|
-
request = fetch_one('segments/' + segment_id.to_s)
|
452
|
-
if request != false
|
453
|
-
JSON.parse(request.response)
|
454
|
-
end
|
455
|
-
end
|
456
|
-
|
457
|
-
def complete_experiment(experiment)
|
458
|
-
unless experiment['variations'].nil?
|
459
|
-
experiment['variations'] = experiment['variations'].map { |variationId| obtain_variation(variationId) }
|
460
|
-
end
|
461
|
-
unless experiment['targetingSegmentId'].nil?
|
462
|
-
experiment['targetingSegment'] = Kameleoon::Targeting::Segment.new(obtain_segment(experiment['targetingSegmentId']))
|
463
|
-
end
|
464
|
-
experiment
|
465
|
-
end
|
466
|
-
|
467
|
-
# fetching segment for both types: experiments and feature_flags (campaigns)
|
468
|
-
def complete_campaign_graphql(campaign)
|
469
|
-
campaign['id'] = campaign['id'].to_i
|
470
|
-
campaign['status'] = campaign['status']
|
471
|
-
unless campaign['deviations'].nil?
|
472
|
-
campaign['deviations'] = Hash[*campaign['deviations'].map { |it| [ it['variationId'] == '0' ? 'origin' : it['variationId'], it['value'] ] }.flatten]
|
473
|
-
end
|
474
|
-
unless campaign['respoolTime'].nil?
|
475
|
-
campaign['respoolTime'] = Hash[*campaign['respoolTime'].map { |it| [ it['variationId'] == '0' ? 'origin' : it['variationId'], it['value'] ] }.flatten]
|
476
|
-
end
|
477
|
-
unless campaign['variations'].nil?
|
478
|
-
campaign['variations'] = campaign['variations'].map { |it| {'id' => it['id'].to_i, 'customJson' => it['customJson']} }
|
479
|
-
end
|
480
|
-
unless campaign['segment'].nil?
|
481
|
-
campaign['targetingSegment'] = Kameleoon::Targeting::Segment.new((campaign['segment']))
|
482
|
-
end
|
483
|
-
campaign
|
484
|
-
end
|
485
|
-
|
486
|
-
def complete_experiment(experiment)
|
487
|
-
unless experiment['variations'].nil?
|
488
|
-
experiment['variations'] = experiment['variations'].map { |variationId| obtain_variation(variationId) }
|
489
|
-
end
|
490
|
-
unless experiment['targetingSegmentId'].nil?
|
491
|
-
experiment['targetingSegment'] = Kameleoon::Targeting::Segment.new(obtain_segment(experiment['targetingSegmentId']))
|
492
|
-
end
|
493
|
-
experiment
|
494
|
-
end
|
495
|
-
|
496
|
-
def obtain_experiments_graphql(site_code, per_page = -1)
|
497
|
-
log "Fetching experiments GraphQL"
|
498
|
-
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|
|
499
|
-
complete_campaign_graphql(experiment['node'])
|
500
|
-
end
|
501
|
-
log "Experiment are fetched: " + experiments.inspect
|
502
|
-
experiments
|
503
|
-
end
|
504
|
-
|
505
|
-
def obtain_tests(site_id, per_page = -1)
|
506
|
-
log "Fetching experiments"
|
507
|
-
query_values = { 'perPage' => per_page }
|
508
|
-
filters = [
|
509
|
-
hash_filter('siteId', 'EQUAL', [site_id]),
|
510
|
-
hash_filter('status', 'IN', ['ACTIVE', 'DEVIATED']),
|
511
|
-
hash_filter('type', 'IN', ['SERVER_SIDE', 'HYBRID'])
|
512
|
-
]
|
513
|
-
experiments = fetch_all('experiments', query_values, filters).map { |it| JSON.parse(it.response) }.flatten.map do |test|
|
514
|
-
complete_experiment(test)
|
515
|
-
end
|
516
|
-
log "Experiment are fetched: " + experiments.inspect
|
517
|
-
experiments
|
518
|
-
end
|
519
|
-
|
520
|
-
def obtain_feature_flags_graphql(site_id, environment = @environment, per_page = -1)
|
521
|
-
log "Fetching feature flags GraphQL"
|
522
|
-
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|
|
523
|
-
complete_campaign_graphql(feature_flag['node'])
|
524
|
-
end
|
525
|
-
log "Feature flags are fetched: " + feature_flags.inspect
|
526
|
-
feature_flags
|
527
|
-
end
|
528
|
-
|
529
|
-
def obtain_feature_flags(site_id, per_page = -1)
|
530
|
-
log "Fetching feature flags"
|
531
|
-
query_values = { 'perPage' => per_page }
|
532
|
-
filters = [
|
533
|
-
hash_filter('siteId', 'EQUAL', [site_id]),
|
534
|
-
hash_filter('status', 'IN', ['ACTIVE', 'PAUSED'])
|
535
|
-
]
|
536
|
-
feature_flags = fetch_all('feature-flags', query_values, filters).map { |it| JSON.parse(it.response) }.flatten.map do |ff|
|
537
|
-
complete_experiment(ff)
|
538
|
-
end
|
539
|
-
log "Feature flags are fetched: " + feature_flags.inspect
|
540
|
-
feature_flags
|
541
|
-
end
|
542
|
-
|
543
|
-
def fetch_all_graphql(path, query_graphql = {})
|
544
|
-
results = []
|
545
|
-
current_page = 1
|
546
|
-
loop do
|
547
|
-
http = fetch_one_graphql(path, query_graphql)
|
548
|
-
break if http == false
|
549
|
-
results.push(http)
|
550
|
-
break if http.response_header["X-Pagination-Page-Count"].to_i <= current_page
|
551
|
-
current_page += 1
|
552
|
-
end
|
553
|
-
results
|
554
|
-
end
|
555
|
-
|
556
|
-
def fetch_all(path, query_values = {}, filters = [])
|
557
|
-
results = []
|
558
|
-
current_page = 1
|
559
|
-
loop do
|
560
|
-
query_values['page'] = current_page
|
561
|
-
http = fetch_one(path, query_values, filters)
|
562
|
-
break if http == false
|
563
|
-
results.push(http)
|
564
|
-
break if http.response_header["X-Pagination-Page-Count"].to_i <= current_page
|
565
|
-
current_page += 1
|
566
|
-
end
|
567
|
-
results
|
568
|
-
end
|
569
|
-
|
570
|
-
def fetch_one_graphql(path, query_graphql = {})
|
571
|
-
request = EM::Synchrony.sync post({ :path => path, :body => query_graphql, :head => hash_headers })
|
572
|
-
unless is_successful(request)
|
573
|
-
log "Failed to fetch" + request.inspect
|
574
|
-
return false
|
575
|
-
end
|
576
|
-
request
|
577
|
-
end
|
578
|
-
|
579
|
-
def fetch_one(path, query_values = {}, filters = [])
|
580
|
-
unless filters.empty?
|
581
|
-
query_values['filter'] = filters.to_json
|
582
|
-
end
|
583
|
-
request = EM::Synchrony.sync get({ :path => path, :query => query_values, :head => hash_headers })
|
584
|
-
unless is_successful(request)
|
585
|
-
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}"
|
586
550
|
return false
|
587
551
|
end
|
588
552
|
request
|
@@ -590,27 +554,27 @@ module Kameleoon
|
|
590
554
|
|
591
555
|
def get_common_ssx_parameters(visitor_code)
|
592
556
|
{
|
593
|
-
:
|
594
|
-
:
|
595
|
-
:
|
557
|
+
nonce: Kameleoon::Utils.generate_random_string(16),
|
558
|
+
siteCode: @site_code,
|
559
|
+
visitorCode: visitor_code
|
596
560
|
}
|
597
561
|
end
|
598
562
|
|
599
563
|
def get_experiment_register_url(visitor_code, experiment_id, variation_id = nil, none_variation = false)
|
600
|
-
url = "/experimentTracking
|
601
|
-
url += "&experimentId
|
564
|
+
url = "/experimentTracking?#{URI.encode_www_form(get_common_ssx_parameters(visitor_code))}"
|
565
|
+
url += "&experimentId=#{experiment_id}"
|
602
566
|
if variation_id.nil?
|
603
567
|
return url
|
604
568
|
end
|
605
|
-
url += "&variationId
|
569
|
+
url += "&variationId=#{variation_id}"
|
606
570
|
if none_variation
|
607
|
-
url +=
|
571
|
+
url += '&noneVariation=true'
|
608
572
|
end
|
609
573
|
url
|
610
574
|
end
|
611
575
|
|
612
576
|
def get_data_register_url(visitor_code)
|
613
|
-
"/dataTracking
|
577
|
+
"/dataTracking?#{URI.encode_www_form(get_common_ssx_parameters(visitor_code))}"
|
614
578
|
end
|
615
579
|
|
616
580
|
def get_api_data_request_url(key)
|
@@ -621,52 +585,54 @@ module Kameleoon
|
|
621
585
|
"/data?#{URI.encode_www_form(mapKey)}"
|
622
586
|
end
|
623
587
|
|
624
|
-
def
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
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
|
629
594
|
print "\nPassing `feature_key` with type of `int` to `activate_feature` or `obtain_feature_variable` "\
|
630
595
|
"is deprecated, it will be removed in next releases. This is necessary to support multi-environment feature\n"
|
631
596
|
else
|
632
|
-
raise TypeError.new(
|
633
|
-
|
634
|
-
if feature_flag.nil?
|
635
|
-
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.'
|
636
599
|
end
|
600
|
+
error_message = "Feature #{feature_key} not found"
|
601
|
+
raise Exception::FeatureConfigurationNotFound.new(feature_key), error_message if feature_flag.nil?
|
602
|
+
|
637
603
|
feature_flag
|
638
604
|
end
|
639
605
|
|
640
|
-
def
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
if (schedule['dateStart'].nil? || Time.parse(schedule['dateStart']).to_i < date) &&
|
647
|
-
(schedule['dateEnd'].nil? || Time.parse(schedule['dateEnd']).to_i > date)
|
648
|
-
return true
|
649
|
-
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.'
|
650
612
|
end
|
651
|
-
|
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
|
652
617
|
end
|
653
618
|
|
654
|
-
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)
|
655
620
|
data_not_sent = data_not_sent(visitor_code)
|
656
621
|
options = {
|
657
|
-
:
|
658
|
-
:
|
659
|
-
:
|
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' }
|
660
625
|
}
|
626
|
+
set_user_agent_to_headers(visitor_code, options[:head])
|
661
627
|
trial = 0
|
662
628
|
success = false
|
663
|
-
log "Start post tracking experiment:
|
629
|
+
log "Start post tracking experiment: #{data_not_sent.inspect}"
|
664
630
|
Thread.new do
|
665
631
|
while trial < 10
|
666
|
-
log "Send Experiment Tracking
|
632
|
+
log "Send Experiment Tracking #{options.inspect}"
|
667
633
|
response = post_sync(options, @tracking_url)
|
668
|
-
log "Response
|
669
|
-
if
|
634
|
+
log "Response #{response.inspect}"
|
635
|
+
if successful_sync?(response)
|
670
636
|
data_not_sent.each { |it| it.sent = true }
|
671
637
|
success = true
|
672
638
|
break
|
@@ -674,9 +640,9 @@ module Kameleoon
|
|
674
640
|
trial += 1
|
675
641
|
end
|
676
642
|
if success
|
677
|
-
log "Post to experiment tracking is done after
|
678
|
-
else
|
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
647
|
end
|
682
648
|
end
|
@@ -685,17 +651,18 @@ module Kameleoon
|
|
685
651
|
Thread.new do
|
686
652
|
trials = 0
|
687
653
|
data_not_sent = data_not_sent(visitor_code)
|
688
|
-
log "Start post tracking data:
|
654
|
+
log "Start post tracking data: #{data_not_sent.inspect}"
|
689
655
|
while trials < 10 && !data_not_sent.empty?
|
690
656
|
options = {
|
691
|
-
:
|
692
|
-
:
|
693
|
-
:
|
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' }
|
694
660
|
}
|
695
|
-
|
661
|
+
set_user_agent_to_headers(visitor_code, options[:head])
|
662
|
+
log "Post tracking data for visitor_code: #{visitor_code} with options: #{options.inspect}"
|
696
663
|
response = post_sync(options, @tracking_url)
|
697
|
-
log "Response
|
698
|
-
if
|
664
|
+
log "Response #{response.inspect}"
|
665
|
+
if successful_sync?(response)
|
699
666
|
data_not_sent.each { |it| it.sent = true }
|
700
667
|
success = true
|
701
668
|
break
|
@@ -703,17 +670,15 @@ module Kameleoon
|
|
703
670
|
trials += 1
|
704
671
|
end
|
705
672
|
if success
|
706
|
-
log "Post to data tracking is done after
|
673
|
+
log "Post to data tracking is done after #{trials + 1} trials"
|
707
674
|
else
|
708
|
-
log "Post to data tracking is failed after
|
675
|
+
log "Post to data tracking is failed after #{trials} trials"
|
709
676
|
end
|
710
677
|
end
|
711
678
|
end
|
712
679
|
|
713
|
-
def check_site_code_enable(
|
714
|
-
|
715
|
-
raise Exception::SiteCodeDisabled.new(site_code)
|
716
|
-
end
|
680
|
+
def check_site_code_enable(campaign)
|
681
|
+
raise Exception::SiteCodeDisabled.new(site_code), site_code unless campaign.site_enabled
|
717
682
|
end
|
718
683
|
|
719
684
|
def data_not_sent(visitor_code)
|
@@ -721,8 +686,109 @@ module Kameleoon
|
|
721
686
|
end
|
722
687
|
|
723
688
|
def log(text)
|
724
|
-
if @verbose_mode
|
725
|
-
|
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)
|
727
|
+
end
|
728
|
+
condition_data
|
729
|
+
end
|
730
|
+
|
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)
|
739
|
+
end
|
740
|
+
[feature_flag, variation_key]
|
741
|
+
end
|
742
|
+
|
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))
|
776
|
+
else
|
777
|
+
log 'An attempt to send a request with null experimentId was blocked'
|
778
|
+
end
|
779
|
+
end
|
780
|
+
|
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'
|
726
792
|
end
|
727
793
|
end
|
728
794
|
end
|