kameleoon-client-ruby 1.1.2 → 2.1.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 +541 -404
- data/lib/kameleoon/configuration/experiment.rb +42 -0
- data/lib/kameleoon/configuration/feature_flag.rb +30 -0
- data/lib/kameleoon/configuration/rule.rb +57 -0
- data/lib/kameleoon/configuration/settings.rb +20 -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 +13 -6
- data/lib/kameleoon/data.rb +60 -40
- data/lib/kameleoon/exceptions.rb +46 -23
- data/lib/kameleoon/factory.rb +21 -18
- data/lib/kameleoon/hybrid/manager.rb +60 -0
- data/lib/kameleoon/real_time/real_time_configuration_service.rb +98 -0
- data/lib/kameleoon/real_time/real_time_event.rb +22 -0
- data/lib/kameleoon/real_time/sse_client.rb +111 -0
- data/lib/kameleoon/real_time/sse_message.rb +23 -0
- data/lib/kameleoon/real_time/sse_request.rb +59 -0
- data/lib/kameleoon/request.rb +14 -13
- data/lib/kameleoon/storage/cache.rb +84 -0
- data/lib/kameleoon/storage/cache_factory.rb +23 -0
- data/lib/kameleoon/storage/variation_storage.rb +42 -0
- data/lib/kameleoon/storage/visitor_variation.rb +20 -0
- data/lib/kameleoon/targeting/condition.rb +17 -5
- data/lib/kameleoon/targeting/condition_factory.rb +9 -2
- data/lib/kameleoon/targeting/conditions/custom_datum.rb +67 -48
- 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 +35 -3
- data/lib/kameleoon/query_graphql.rb +0 -76
data/lib/kameleoon/client.rb
CHANGED
@@ -4,7 +4,13 @@ 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/settings'
|
10
|
+
require 'kameleoon/real_time/real_time_configuration_service'
|
11
|
+
require 'kameleoon/storage/variation_storage'
|
12
|
+
require 'kameleoon/hybrid/manager'
|
13
|
+
require 'kameleoon/storage/cache_factory'
|
8
14
|
require 'rufus/scheduler'
|
9
15
|
require 'yaml'
|
10
16
|
require 'json'
|
@@ -13,6 +19,7 @@ require 'em-synchrony/em-http'
|
|
13
19
|
require 'em-synchrony/fiber_iterator'
|
14
20
|
require 'objspace'
|
15
21
|
require 'time'
|
22
|
+
require 'ostruct'
|
16
23
|
|
17
24
|
module Kameleoon
|
18
25
|
##
|
@@ -26,23 +33,35 @@ module Kameleoon
|
|
26
33
|
##
|
27
34
|
# You should create Client with the Client Factory only.
|
28
35
|
#
|
29
|
-
def initialize(site_code, path_config_file,
|
36
|
+
def initialize(site_code, path_config_file, interval, default_timeout, client_id = nil, client_secret = nil)
|
30
37
|
config = YAML.load_file(path_config_file)
|
31
38
|
@site_code = site_code
|
32
|
-
@blocking = blocking
|
33
39
|
@default_timeout = config['default_timeout'] || default_timeout # in ms
|
34
40
|
refresh_interval = config['actions_configuration_refresh_interval']
|
35
41
|
@interval = refresh_interval.nil? ? interval : "#{refresh_interval}m"
|
36
|
-
@tracking_url = config['tracking_url'] ||
|
37
|
-
@api_data_url =
|
42
|
+
@tracking_url = config['tracking_url'] || API_SSX_URL
|
43
|
+
@api_data_url = 'https://api-data.kameleoon.com'
|
44
|
+
@events_url = 'https://events.kameleoon.com:8110/'
|
45
|
+
@real_time_configuration_service = nil
|
46
|
+
@update_configuration_handler = nil
|
47
|
+
@fetch_configuration_update_job = nil
|
38
48
|
@client_id = client_id || config['client_id']
|
39
49
|
@client_secret = client_secret || config['client_secret']
|
40
50
|
@data_maximum_size = config['visitor_data_maximum_size'] || 500 # mb
|
41
51
|
@environment = config['environment'] || DEFAULT_ENVIRONMENT
|
52
|
+
@settings = Kameleoon::Configuration::Settings.new
|
42
53
|
@verbose_mode = config['verbose_mode'] || false
|
43
54
|
@experiments = []
|
44
55
|
@feature_flags = []
|
45
56
|
@data = {}
|
57
|
+
@user_agents = {}
|
58
|
+
@variation_storage = Kameleoon::Storage::VariationStorage.new
|
59
|
+
@hybrid_manager = Kameleoon::Hybrid::ManagerImpl.new(
|
60
|
+
CACHE_EXPIRATION_TIMEOUT,
|
61
|
+
CACHE_EXPIRATION_TIMEOUT * 3,
|
62
|
+
Kameleoon::Storage::CacheFactoryImpl.new,
|
63
|
+
method(:log)
|
64
|
+
)
|
46
65
|
end
|
47
66
|
|
48
67
|
##
|
@@ -69,16 +88,22 @@ module Kameleoon
|
|
69
88
|
#
|
70
89
|
# @example
|
71
90
|
# cookies = {'kameleoonVisitorCode' => '1234asdf4321fdsa'}
|
72
|
-
# visitor_code =
|
91
|
+
# visitor_code = get_visitor_code(cookies, 'my-domaine.com')
|
73
92
|
#
|
74
|
-
def
|
93
|
+
def get_visitor_code(cookies, top_level_domain, default_visitor_code = nil)
|
75
94
|
read_and_write(cookies, top_level_domain, cookies, default_visitor_code)
|
76
95
|
end
|
77
96
|
|
97
|
+
# DEPRECATED. Please use `get_visitor_code` instead.
|
98
|
+
def obtain_visitor_code(cookies, top_level_domain, default_visitor_code = nil)
|
99
|
+
warn '[DEPRECATION] `obtain_visitor_code` is deprecated. Please use `get_visitor_code` instead.'
|
100
|
+
get_visitor_code(cookies, top_level_domain, default_visitor_code)
|
101
|
+
end
|
102
|
+
|
78
103
|
##
|
79
104
|
# Trigger an experiment.
|
80
105
|
#
|
81
|
-
# If such a visitor_code has never been associated with any variation, the SDK returns a randomly selected variation
|
106
|
+
# If such a visitor_code has never been associated with any variation, the SDK returns a randomly selected variation
|
82
107
|
# If a user with a given visitor_code is already registered with a variation, it will detect the previously
|
83
108
|
# registered variation and return the variation_id.
|
84
109
|
# 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 +115,35 @@ module Kameleoon
|
|
90
115
|
# @return [Integer] Id of the variation
|
91
116
|
#
|
92
117
|
# @raise [Kameleoon::Exception::ExperimentConfigurationNotFound] Raise when experiment configuration is not found
|
93
|
-
# @raise [Kameleoon::Exception::
|
94
|
-
#
|
118
|
+
# @raise [Kameleoon::Exception::NotAllocated] The visitor triggered the experiment, but did not activate it.
|
119
|
+
# Usually, this happens because the user has been associated with excluded traffic
|
120
|
+
# @raise [Kameleoon::Exception::NotTargeted] The visitor is not targeted by the experiment, as the
|
121
|
+
# associated targeting segment conditions were not fulfilled. He should see the reference variation
|
95
122
|
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
96
123
|
#
|
97
|
-
def trigger_experiment(visitor_code, experiment_id
|
124
|
+
def trigger_experiment(visitor_code, experiment_id)
|
98
125
|
check_visitor_code(visitor_code)
|
99
|
-
experiment = @experiments.find { |
|
126
|
+
experiment = @experiments.find { |exp| exp.id.to_s == experiment_id.to_s }
|
100
127
|
if experiment.nil?
|
101
|
-
raise Exception::ExperimentConfigurationNotFound.new(experiment_id)
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
128
|
+
raise Exception::ExperimentConfigurationNotFound.new(experiment_id),
|
129
|
+
"Experiment #{experiment_id} is not found"
|
130
|
+
end
|
131
|
+
check_site_code_enable(experiment)
|
132
|
+
if check_targeting(visitor_code, experiment_id, experiment)
|
133
|
+
# saved_variation = get_valid_saved_variation(visitor_code, experiment)
|
134
|
+
variation_id = calculate_variation_for_experiment(visitor_code, experiment)
|
135
|
+
if !variation_id.nil?
|
136
|
+
track_experiment(visitor_code, experiment_id, variation_id)
|
137
|
+
save_variation(visitor_code, experiment_id, variation_id)
|
138
|
+
variation_id
|
139
|
+
else
|
139
140
|
track_experiment(visitor_code, experiment_id, REFERENCE, true)
|
140
|
-
raise Exception::
|
141
|
+
raise Exception::NotAllocated.new(visitor_code),
|
142
|
+
"Experiment #{experiment_id} is not active for visitor #{visitor_code}"
|
141
143
|
end
|
142
|
-
|
144
|
+
else
|
145
|
+
raise Exception::NotTargeted.new(visitor_code),
|
146
|
+
"Experiment #{experiment_id} is not targeted for visitor #{visitor_code}"
|
143
147
|
end
|
144
148
|
end
|
145
149
|
|
@@ -158,15 +162,14 @@ module Kameleoon
|
|
158
162
|
#
|
159
163
|
def add_data(visitor_code, *args)
|
160
164
|
check_visitor_code(visitor_code)
|
161
|
-
while ObjectSpace.memsize_of(@data) > @data_maximum_size * (2**20)
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
@data[visitor_code].push(*args)
|
167
|
-
else
|
168
|
-
@data[visitor_code] = args
|
165
|
+
@data.shift while ObjectSpace.memsize_of(@data) > @data_maximum_size * (2**20)
|
166
|
+
args.each do |data_element|
|
167
|
+
if data_element.is_a?(UserAgent)
|
168
|
+
add_user_agent_data(visitor_code, data_element)
|
169
|
+
next
|
169
170
|
end
|
171
|
+
@data[visitor_code] = [] unless @data.key?(visitor_code)
|
172
|
+
@data[visitor_code].push(data_element)
|
170
173
|
end
|
171
174
|
end
|
172
175
|
|
@@ -174,8 +177,10 @@ module Kameleoon
|
|
174
177
|
# Track conversions on a particular goal
|
175
178
|
#
|
176
179
|
# 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
|
180
|
+
# In addition, this method also accepts revenue as a third optional argument to track revenue.
|
181
|
+
# The visitor_code usually is identical to the one that was used when triggering the experiment.
|
182
|
+
# The track_conversion method doesn't return any value.
|
183
|
+
# This method is non-blocking as the server call is made asynchronously.
|
179
184
|
#
|
180
185
|
# @param [String] visitor_code Visitor code
|
181
186
|
# @param [Integer] goal_id Id of the goal
|
@@ -212,7 +217,7 @@ module Kameleoon
|
|
212
217
|
##
|
213
218
|
# Obtain variation associated data.
|
214
219
|
#
|
215
|
-
# To retrieve JSON data associated with a variation, call the
|
220
|
+
# To retrieve JSON data associated with a variation, call the get_variation_associated_data method of our SDK.
|
216
221
|
# The JSON data usually represents some metadata of the variation, and can be configured on our web application
|
217
222
|
# interface or via our Automation API.
|
218
223
|
# This method takes the variationID as a parameter and will return the data as a json string.
|
@@ -224,74 +229,137 @@ module Kameleoon
|
|
224
229
|
#
|
225
230
|
# @raise [Kameleoon::Exception::VariationNotFound] Raise exception if the variation is not found.
|
226
231
|
#
|
227
|
-
def
|
228
|
-
variation = @experiments.map
|
232
|
+
def get_variation_associated_data(variation_id)
|
233
|
+
variation = @experiments.map(&:variations).flatten.select { |var| var['id'].to_i == variation_id.to_i }.first
|
229
234
|
if variation.nil?
|
230
|
-
raise Exception::VariationConfigurationNotFound.new(variation_id)
|
235
|
+
raise Exception::VariationConfigurationNotFound.new(variation_id),
|
236
|
+
"Variation key #{variation_id} not found"
|
231
237
|
else
|
232
238
|
JSON.parse(variation['customJson'])
|
233
239
|
end
|
234
240
|
end
|
235
241
|
|
236
|
-
#
|
242
|
+
# DEPRECATED. Please use `get_variation_associated_data` instead.
|
243
|
+
def obtain_variation_associated_data(variation_id)
|
244
|
+
warn '[DEPRECATION] `obtain_variation_associated_data` is deprecated.
|
245
|
+
Please use `get_variation_associated_data` instead.'
|
246
|
+
get_variation_associated_data(variation_id)
|
247
|
+
end
|
248
|
+
|
249
|
+
# #
|
237
250
|
# Activate a feature toggle.
|
251
|
+
|
252
|
+
# This method takes a visitor_code and feature_key (or feature_id) as mandatory arguments to check if the specified
|
253
|
+
# feature will be active for a given user.
|
254
|
+
# If such a user has never been associated with this feature flag, the SDK returns a boolean value randomly
|
255
|
+
# (true if the user should have this feature or false if not). If a user with a given visitorCode is already
|
256
|
+
# registered with this feature flag, it will detect the previous featureFlag value.
|
257
|
+
# You have to make sure that proper error handling is set up in your code as shown in the example
|
258
|
+
# to the right to catch potential exceptions.
|
259
|
+
|
260
|
+
# @param [String] visitor_code
|
261
|
+
# @param [String | Integer] feature_key
|
262
|
+
|
263
|
+
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound] Feature Flag isn't found in this configuration
|
264
|
+
# @raise [Kameleoon::Exception::NotTargeted] The visitor is not targeted by the experiment, as the
|
265
|
+
# associated targeting segment conditions were not fulfilled. He should see the reference variation
|
266
|
+
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
238
267
|
#
|
239
|
-
#
|
240
|
-
|
241
|
-
|
268
|
+
# DEPRECATED. Please use `is_feature_active` instead.
|
269
|
+
def activate_feature(visitor_code, feature_key)
|
270
|
+
warn '[DEPRECATION] `activate_feature` is deprecated. Please use `feature_active?` instead.'
|
271
|
+
feature_active?(visitor_code, feature_key)
|
272
|
+
end
|
273
|
+
|
274
|
+
##
|
275
|
+
# Check if feature is active for a given visitor code
|
276
|
+
#
|
277
|
+
# This method takes a visitor_code and feature_key as mandatory arguments to check if the specified
|
278
|
+
# feature will be active for a given user.
|
279
|
+
# If such a user has never been associated with this feature flag, the SDK returns a boolean value randomly
|
280
|
+
# (true if the user should have this feature or false if not). If a user with a given visitorCode is already
|
281
|
+
# registered with this feature flag, it will detect the previous feature flag value.
|
282
|
+
# You have to make sure that proper error handling is set up in your code as shown in the example
|
283
|
+
# to the right to catch potential exceptions.
|
284
|
+
#
|
285
|
+
# @param [String] visitor_code Unique identifier of the user. This field is mandatory.
|
286
|
+
# @param [String] feature_key Key of the feature flag you want to expose to a user. This field is mandatory.
|
287
|
+
#
|
288
|
+
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound] Feature Flag isn't found in this configuration
|
289
|
+
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
290
|
+
#
|
291
|
+
def feature_active?(visitor_code, feature_key)
|
292
|
+
_, variation_key = _get_feature_variation_key(visitor_code, feature_key)
|
293
|
+
variation_key != Kameleoon::Configuration::VariationType::VARIATION_OFF
|
294
|
+
end
|
295
|
+
|
296
|
+
#
|
297
|
+
# get_feature_variation_key returns a variation key for visitor code
|
298
|
+
#
|
299
|
+
# This method takes a visitorCode and featureKey as mandatory arguments and
|
300
|
+
# returns a variation assigned for a given visitor
|
301
|
+
# If such a user has never been associated with any feature flag rules, the SDK returns a default variation key
|
302
|
+
# You have to make sure that proper error handling is set up in your code as shown in the example to the right
|
303
|
+
# to catch potential exceptions.
|
242
304
|
#
|
243
305
|
# @param [String] visitor_code
|
244
|
-
# @param [String
|
306
|
+
# @param [String] feature_key
|
245
307
|
#
|
246
|
-
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound]
|
247
|
-
# @raise [Kameleoon::Exception::NotTargeted]
|
308
|
+
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound] Feature Flag isn't found in this configuration
|
248
309
|
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
249
310
|
#
|
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"
|
311
|
+
def get_feature_variation_key(visitor_code, feature_key)
|
312
|
+
_, variation_key = _get_feature_variation_key(visitor_code, feature_key)
|
313
|
+
variation_key
|
314
|
+
end
|
274
315
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
316
|
+
##
|
317
|
+
# Retrieves a feature variable value from assigned for visitor variation
|
318
|
+
#
|
319
|
+
# A feature variable can be changed easily via our web application.
|
320
|
+
#
|
321
|
+
# @param [String] visitor_code
|
322
|
+
# @param [String] feature_key
|
323
|
+
# @param [String] variable_name
|
324
|
+
#
|
325
|
+
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound] Feature Flag isn't found in this configuration
|
326
|
+
# @raise [Kameleoon::Exception::FeatureVariableNotFound]
|
327
|
+
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
328
|
+
#
|
329
|
+
def get_feature_variable(visitor_code, feature_key, variable_name)
|
330
|
+
feature_flag, variation_key = _get_feature_variation_key(visitor_code, feature_key)
|
331
|
+
variation = feature_flag.get_variation_key(variation_key)
|
332
|
+
variable = variation&.get_variable_by_key(variable_name)
|
333
|
+
if variable.nil?
|
334
|
+
raise Exception::FeatureVariableNotFound.new(variable_name),
|
335
|
+
"Feature variable #{variable_name} not found"
|
336
|
+
end
|
337
|
+
_parse_feature_variable(variable)
|
338
|
+
end
|
281
339
|
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
340
|
+
##
|
341
|
+
# Retrieves all feature variable values for a given variation
|
342
|
+
#
|
343
|
+
# This method takes a feature_key and variation_key as mandatory arguments and
|
344
|
+
# returns a list of variables for a given variation key
|
345
|
+
# A feature variable can be changed easily via our web application.
|
346
|
+
#
|
347
|
+
# @param [String] feature_key
|
348
|
+
# @param [String] variation_key
|
349
|
+
#
|
350
|
+
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound] Feature Flag isn't found in this configuration
|
351
|
+
# @raise [Kameleoon::Exception::VariationConfigurationNotFound]
|
352
|
+
#
|
353
|
+
def get_feature_all_variables(feature_key, variation_key)
|
354
|
+
feature_flag = find_feature_flag(feature_key)
|
355
|
+
variation = feature_flag.get_variation_key(variation_key)
|
356
|
+
if variation.nil?
|
357
|
+
raise Exception::VariationConfigurationNotFound.new(variation_key),
|
358
|
+
"Variation key #{variation_key} not found"
|
294
359
|
end
|
360
|
+
variables = {}
|
361
|
+
variation.variables.each { |var| variables[var.key] = _parse_feature_variable(var) }
|
362
|
+
variables
|
295
363
|
end
|
296
364
|
|
297
365
|
##
|
@@ -302,287 +370,256 @@ module Kameleoon
|
|
302
370
|
# @param [String | Integer] feature_key
|
303
371
|
# @param [String] variable_key
|
304
372
|
#
|
305
|
-
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound]
|
373
|
+
# @raise [Kameleoon::Exception::FeatureConfigurationNotFound] Feature Flag isn't found in this configuration
|
306
374
|
# @raise [Kameleoon::Exception::FeatureVariableNotFound]
|
307
375
|
#
|
376
|
+
# DEPRECATED. Please use `get_feature_variable` instead.
|
308
377
|
def obtain_feature_variable(feature_key, variable_key)
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
when "Boolean"
|
316
|
-
return custom_json['value'].downcase == "true"
|
317
|
-
when "String"
|
318
|
-
return custom_json['value']
|
319
|
-
when "Number"
|
320
|
-
return custom_json['value'].to_i
|
321
|
-
when "JSON"
|
322
|
-
return JSON.parse(custom_json['value'])
|
323
|
-
else
|
324
|
-
raise TypeError.new("Unknown type for feature variable")
|
325
|
-
end
|
378
|
+
warn '[DEPRECATION] `obtain_feature_variable` is deprecated. Please use `get_feature_variable` instead.'
|
379
|
+
all_variables = get_feature_all_variables(
|
380
|
+
feature_key,
|
381
|
+
Configuration::VariationType::VARIATION_OFF
|
382
|
+
)
|
383
|
+
all_variables[variable_key]
|
326
384
|
end
|
327
385
|
|
328
386
|
##
|
329
|
-
# The
|
330
|
-
# stored on a remote Kameleoon server. Usually data will be stored on our remote
|
331
|
-
#
|
332
|
-
#
|
387
|
+
# The get_remote_date method allows you to retrieve data (according to a key passed as argument)
|
388
|
+
# stored on a remote Kameleoon server. Usually data will be stored on our remote
|
389
|
+
# servers via the use of our Data API.
|
390
|
+
# This method, along with the availability of our highly scalable servers for this purpose, provides a convenient
|
391
|
+
# way to quickly store massive amounts of data that can be later retrieved for each of your visitors / users.
|
333
392
|
#
|
334
393
|
# @param [String] key Key you want to retrieve data. This field is mandatory.
|
335
|
-
# @param [Int] timeout Timeout for request. Equals default_timeout in a config file.
|
394
|
+
# @param [Int] timeout Timeout for request (in milliseconds). Equals default_timeout in a config file.
|
395
|
+
# This field is optional.
|
336
396
|
#
|
337
397
|
# @return [Hash] Hash object of the json object.
|
338
|
-
|
339
|
-
#
|
340
|
-
def retrieve_data_from_remote_source(key, timeout = @default_timeout)
|
398
|
+
def get_remote_date(key, timeout = @default_timeout)
|
341
399
|
connexion_options = { connect_timeout: (timeout.to_f / 1000.0) }
|
342
400
|
path = get_api_data_request_url(key)
|
343
401
|
log "Retrieve API Data connexion: #{connexion_options.inspect}"
|
344
402
|
response = get_sync(@api_data_url + path, connexion_options)
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
return nil
|
349
|
-
end
|
403
|
+
return nil unless successful_sync?(response)
|
404
|
+
|
405
|
+
JSON.parse(response.body) unless response.nil?
|
350
406
|
end
|
351
407
|
|
352
|
-
|
408
|
+
##
|
409
|
+
# DEPRECATED. Please use `get_feature_variable` instead.
|
410
|
+
def retrieve_data_from_remote_source(key, timeout = @default_timeout)
|
411
|
+
warn '[DEPRECATION] `retrieve_data_from_remote_source` is deprecated. Please use `get_remote_date` instead.'
|
412
|
+
get_remote_date(key, timeout)
|
413
|
+
end
|
353
414
|
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
415
|
+
##
|
416
|
+
# Returns a list of all experiment ids
|
417
|
+
#
|
418
|
+
# @return [Array] array of all experiment ids
|
419
|
+
def get_experiment_list # rubocop:disable Naming/AccessorMethodName
|
420
|
+
@experiments.map { |it| it.id.to_i }
|
421
|
+
end
|
361
422
|
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
423
|
+
##
|
424
|
+
# Returns a list of all experiment ids targeted for a visitor
|
425
|
+
# if only_allocated is `true` returns a list of allocated experiments for a visitor
|
426
|
+
#
|
427
|
+
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
428
|
+
#
|
429
|
+
# @return [Array] array of all experiment ids accorging to a only_allocated parameter
|
430
|
+
def get_experiment_list_for_visitor(visitor_code, only_allocated: true)
|
431
|
+
list_ids = []
|
432
|
+
@experiments.each do |experiment|
|
433
|
+
next unless check_targeting(visitor_code, experiment.id.to_i, experiment)
|
434
|
+
next if only_allocated && calculate_variation_for_experiment(visitor_code, experiment).nil?
|
435
|
+
|
436
|
+
list_ids.push(experiment.id.to_i)
|
371
437
|
end
|
438
|
+
list_ids
|
372
439
|
end
|
373
440
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
end
|
441
|
+
##
|
442
|
+
# Returns a list of all feature flag keys
|
443
|
+
#
|
444
|
+
# @return [Array] array of all feature flag keys
|
445
|
+
def get_feature_list # rubocop:disable Naming/AccessorMethodName
|
446
|
+
@feature_flags.map(&:feature_key)
|
381
447
|
end
|
382
448
|
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
449
|
+
##
|
450
|
+
# Returns a list of active feature flag keys for a visitor
|
451
|
+
#
|
452
|
+
# @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
|
453
|
+
#
|
454
|
+
# @return [Array] array of active feature flag keys for a visitor
|
455
|
+
def get_active_feature_list_for_visitor(visitor_code)
|
456
|
+
check_visitor_code(visitor_code)
|
457
|
+
list_keys = []
|
458
|
+
@feature_flags.each do |feature_flag|
|
459
|
+
variation, rule, = _calculate_variation_key_for_feature(visitor_code, feature_flag)
|
460
|
+
variation_key = _get_variation_key(variation, rule, feature_flag)
|
461
|
+
if variation_key != Kameleoon::Configuration::VariationType::VARIATION_OFF
|
462
|
+
list_keys.push(feature_flag.feature_key)
|
394
463
|
end
|
395
|
-
EM.stop
|
396
464
|
end
|
465
|
+
list_keys
|
397
466
|
end
|
398
467
|
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
468
|
+
##
|
469
|
+
# The `on_update_configuration()` method allows you to handle the event when configuration
|
470
|
+
# has updated data. It takes one input parameter: callable **handler**. The handler
|
471
|
+
# that will be called when the configuration is updated using a real-time configuration event.
|
472
|
+
#
|
473
|
+
# @param handler [Callable | NilClass] The handler that will be called when the configuration
|
474
|
+
# is updated using a real-time configuration event.
|
475
|
+
def on_update_configuration(handler)
|
476
|
+
@update_configuration_handler = handler
|
407
477
|
end
|
408
478
|
|
409
|
-
|
410
|
-
|
479
|
+
##
|
480
|
+
# The `get_engine_tracking_code` returns the JavasScript code to be inserted in your page
|
481
|
+
# to send automatically the exposure events to the analytics solution you are using.
|
482
|
+
#
|
483
|
+
# @param [String] visitor_code The user's unique identifier. This field is mandatory.
|
484
|
+
#
|
485
|
+
# @return [String] JavasScript code to be inserted in your page to send automatically
|
486
|
+
# the exposure events to the analytics solution you are using.
|
487
|
+
def get_engine_tracking_code(visitor_code)
|
488
|
+
@hybrid_manager.get_engine_tracking_code(visitor_code)
|
411
489
|
end
|
412
490
|
|
413
|
-
|
414
|
-
log "Fetching site"
|
415
|
-
query_params = { 'perPage' => 1 }
|
416
|
-
filters = [hash_filter('code', 'EQUAL', [@site_code])]
|
417
|
-
request = fetch_one('sites', query_params, filters)
|
418
|
-
if request != false
|
419
|
-
sites = JSON.parse(request.response)
|
420
|
-
log "Sites are fetched: " + sites.inspect
|
421
|
-
sites
|
422
|
-
end
|
423
|
-
end
|
491
|
+
private
|
424
492
|
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
}
|
432
|
-
header = { 'Content-Type' => 'application/x-www-form-urlencoded' }
|
493
|
+
API_SSX_URL = 'https://api-ssx.kameleoon.com'
|
494
|
+
REFERENCE = 0
|
495
|
+
DEFAULT_ENVIRONMENT = 'production'
|
496
|
+
CACHE_EXPIRATION_TIMEOUT = 5
|
497
|
+
attr :site_code, :client_id, :client_secret, :access_token, :experiments, :feature_flags, :scheduler, :data,
|
498
|
+
:tracking_url, :default_timeout, :interval, :memory_limit, :verbose_mode
|
433
499
|
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
else
|
439
|
-
log "Failed to fetch bearer token: " + request.inspect
|
500
|
+
def fetch_configuration
|
501
|
+
Rufus::Scheduler.singleton.in '0s' do
|
502
|
+
log('Initial configuration fetch is started.')
|
503
|
+
fetch_configuration_job
|
440
504
|
end
|
441
505
|
end
|
442
506
|
|
443
|
-
def
|
444
|
-
|
445
|
-
|
446
|
-
|
507
|
+
def fetch_configuration_job(time_stamp = nil)
|
508
|
+
EM.synchrony do
|
509
|
+
begin
|
510
|
+
ok = obtain_configuration(@site_code, @environment, time_stamp)
|
511
|
+
if !ok && @settings.real_time_update
|
512
|
+
@settings.real_time_update = false
|
513
|
+
log('Switching to polling mode due to failed fetch')
|
514
|
+
end
|
515
|
+
rescue StandardError => e
|
516
|
+
log("Error occurred during configuration fetching: #{e}")
|
517
|
+
end
|
518
|
+
manage_configuration_update(@settings.real_time_update)
|
519
|
+
EM.stop
|
447
520
|
end
|
448
521
|
end
|
449
522
|
|
450
|
-
def
|
451
|
-
|
452
|
-
|
453
|
-
|
523
|
+
def start_configuration_update_job_if_needed
|
524
|
+
return unless @fetch_configuration_update_job.nil?
|
525
|
+
|
526
|
+
@fetch_configuration_update_job = Rufus::Scheduler.singleton.schedule_every @interval do
|
527
|
+
log('Scheduled job to fetch configuration is started.')
|
528
|
+
fetch_configuration_job
|
454
529
|
end
|
455
530
|
end
|
456
531
|
|
457
|
-
def
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
end
|
464
|
-
experiment
|
532
|
+
def stop_configuration_update_job_if_needed
|
533
|
+
return if @fetch_configuration_update_job.nil?
|
534
|
+
|
535
|
+
@fetch_configuration_update_job.unschedule
|
536
|
+
@fetch_configuration_update_job = nil
|
537
|
+
log('Scheduled job to fetch configuration is stopped.')
|
465
538
|
end
|
466
539
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
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
|
540
|
+
def start_real_time_configuration_service_if_needed
|
541
|
+
return unless @real_time_configuration_service.nil?
|
542
|
+
|
543
|
+
events_url = "#{@events_url}sse?siteCode=#{@site_code}"
|
544
|
+
fetch_func = proc { |real_time_event| fetch_configuration_job(real_time_event.time_stamp) }
|
545
|
+
@real_time_configuration_service =
|
546
|
+
Kameleoon::RealTime::RealTimeConfigurationService.new(events_url, fetch_func, method(:log))
|
484
547
|
end
|
485
548
|
|
486
|
-
def
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
experiment['targetingSegment'] = Kameleoon::Targeting::Segment.new(obtain_segment(experiment['targetingSegmentId']))
|
492
|
-
end
|
493
|
-
experiment
|
549
|
+
def stop_real_time_configuration_service_if_needed
|
550
|
+
return if @real_time_configuration_service.nil?
|
551
|
+
|
552
|
+
@real_time_configuration_service.close
|
553
|
+
@real_time_configuration_service = nil
|
494
554
|
end
|
495
555
|
|
496
|
-
def
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
556
|
+
def manage_configuration_update(is_real_time_update)
|
557
|
+
if is_real_time_update
|
558
|
+
stop_configuration_update_job_if_needed
|
559
|
+
start_real_time_configuration_service_if_needed
|
560
|
+
else
|
561
|
+
stop_real_time_configuration_service_if_needed
|
562
|
+
start_configuration_update_job_if_needed
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
# def hash_headers
|
567
|
+
# if @access_token.nil?
|
568
|
+
# CredentialsNotFound.new
|
569
|
+
# end
|
570
|
+
# {
|
571
|
+
# 'Authorization' => 'Bearer ' + @access_token.to_s,
|
572
|
+
# 'Content-Type' => 'application/json'
|
573
|
+
# }
|
574
|
+
# end
|
575
|
+
|
576
|
+
# def hash_filter(field, operator, parameters)
|
577
|
+
# { 'field' => field, 'operator' => operator, 'parameters' => parameters }
|
578
|
+
# end
|
579
|
+
|
580
|
+
def obtain_configuration(site_code, environment = @environment, time_stamp = nil)
|
581
|
+
log 'Fetching configuration from Client-Config service'
|
582
|
+
request_path = "mobile?siteCode=#{site_code}"
|
583
|
+
request_path += "&environment=#{environment}" unless environment.nil?
|
584
|
+
request_path += "&ts=#{time_stamp}" unless time_stamp.nil?
|
585
|
+
request = request_configuration(request_path)
|
586
|
+
return false unless request
|
587
|
+
|
588
|
+
configuration = JSON.parse(request.response)
|
589
|
+
@experiments = Kameleoon::Configuration::Experiment.create_from_array(configuration['experiments']) ||
|
590
|
+
@experiments
|
591
|
+
@feature_flags = Kameleoon::Configuration::FeatureFlag.create_from_array(
|
592
|
+
configuration['featureFlagConfigurations']
|
593
|
+
)
|
594
|
+
@settings.update(configuration['configuration'])
|
595
|
+
call_update_handler_if_needed(!time_stamp.nil?)
|
596
|
+
log "Feature flags are fetched: #{request.inspect}"
|
597
|
+
true
|
518
598
|
end
|
519
599
|
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
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
|
600
|
+
##
|
601
|
+
# Call the handler when configuraiton was updated with new time stamp.
|
602
|
+
#
|
603
|
+
# @param need_call [Bool] Indicates if we need to call handler or not.
|
604
|
+
def call_update_handler_if_needed(need_call)
|
605
|
+
return if !need_call || @update_configuration_handler.nil?
|
606
|
+
|
607
|
+
@update_configuration_handler.call
|
568
608
|
end
|
569
609
|
|
570
|
-
def
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
return
|
610
|
+
def calculate_variation_for_experiment(visitor_code, experiment)
|
611
|
+
threshold = obtain_hash_double(visitor_code, experiment.respool_time, experiment.id)
|
612
|
+
experiment.deviations.each do |key, value|
|
613
|
+
threshold -= value
|
614
|
+
return key.to_s.to_i if threshold.negative?
|
575
615
|
end
|
576
|
-
|
616
|
+
nil
|
577
617
|
end
|
578
618
|
|
579
|
-
def
|
580
|
-
|
581
|
-
|
582
|
-
|
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
|
619
|
+
def request_configuration(path)
|
620
|
+
request = EM::Synchrony.sync get({ path: path }, CLIENT_CONFIG_URL)
|
621
|
+
unless successful?(request)
|
622
|
+
log "Failed to fetch #{request.inspect}"
|
586
623
|
return false
|
587
624
|
end
|
588
625
|
request
|
@@ -590,27 +627,25 @@ module Kameleoon
|
|
590
627
|
|
591
628
|
def get_common_ssx_parameters(visitor_code)
|
592
629
|
{
|
593
|
-
:
|
594
|
-
:
|
595
|
-
:
|
630
|
+
nonce: Kameleoon::Utils.generate_random_string(16),
|
631
|
+
siteCode: @site_code,
|
632
|
+
visitorCode: visitor_code
|
596
633
|
}
|
597
634
|
end
|
598
635
|
|
599
636
|
def get_experiment_register_url(visitor_code, experiment_id, variation_id = nil, none_variation = false)
|
600
|
-
url = "/experimentTracking
|
601
|
-
url += "&experimentId
|
602
|
-
if variation_id.nil?
|
603
|
-
|
604
|
-
|
605
|
-
url +=
|
606
|
-
|
607
|
-
url += "&noneVariation=true"
|
608
|
-
end
|
637
|
+
url = "/experimentTracking?#{URI.encode_www_form(get_common_ssx_parameters(visitor_code))}"
|
638
|
+
url += "&experimentId=#{experiment_id}"
|
639
|
+
return url if variation_id.nil?
|
640
|
+
|
641
|
+
url += "&variationId=#{variation_id}"
|
642
|
+
url += '&noneVariation=true' if none_variation
|
643
|
+
|
609
644
|
url
|
610
645
|
end
|
611
646
|
|
612
647
|
def get_data_register_url(visitor_code)
|
613
|
-
"/dataTracking
|
648
|
+
"/dataTracking?#{URI.encode_www_form(get_common_ssx_parameters(visitor_code))}"
|
614
649
|
end
|
615
650
|
|
616
651
|
def get_api_data_request_url(key)
|
@@ -621,52 +656,36 @@ module Kameleoon
|
|
621
656
|
"/data?#{URI.encode_www_form(mapKey)}"
|
622
657
|
end
|
623
658
|
|
624
|
-
def
|
659
|
+
def find_feature_flag(feature_key)
|
625
660
|
if feature_key.is_a?(String)
|
626
|
-
feature_flag = @feature_flags.select { |ff| ff
|
627
|
-
elsif feature_key.is_a?(Integer)
|
628
|
-
feature_flag = @feature_flags.select { |ff| ff['id'].to_i == feature_key}.first
|
629
|
-
print "\nPassing `feature_key` with type of `int` to `activate_feature` or `obtain_feature_variable` "\
|
630
|
-
"is deprecated, it will be removed in next releases. This is necessary to support multi-environment feature\n"
|
661
|
+
feature_flag = @feature_flags.select { |ff| ff.feature_key == feature_key }.first
|
631
662
|
else
|
632
|
-
raise TypeError.new(
|
633
|
-
|
634
|
-
if feature_flag.nil?
|
635
|
-
raise Exception::FeatureConfigurationNotFound.new(feature_key)
|
663
|
+
raise TypeError.new('Feature key should be a String or an Integer.'),
|
664
|
+
'Feature key should be a String or an Integer.'
|
636
665
|
end
|
637
|
-
|
638
|
-
|
666
|
+
error_message = "Feature #{feature_key} not found"
|
667
|
+
raise Exception::FeatureConfigurationNotFound.new(feature_key), error_message if feature_flag.nil?
|
639
668
|
|
640
|
-
|
641
|
-
current_status = feature_flag['status'] == STATUS_ACTIVE
|
642
|
-
if feature_flag['featureStatus'] == FEATURE_STATUS_DEACTIVATED || feature_flag['schedules'].empty?
|
643
|
-
return current_status
|
644
|
-
end
|
645
|
-
feature_flag['schedules'].each do |schedule|
|
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
|
650
|
-
end
|
651
|
-
false
|
669
|
+
feature_flag
|
652
670
|
end
|
653
671
|
|
654
|
-
def track_experiment(visitor_code, experiment_id, variation_id = nil, none_variation
|
672
|
+
def track_experiment(visitor_code, experiment_id, variation_id = nil, none_variation: false)
|
655
673
|
data_not_sent = data_not_sent(visitor_code)
|
656
674
|
options = {
|
657
|
-
:
|
658
|
-
:
|
659
|
-
:
|
675
|
+
path: get_experiment_register_url(visitor_code, experiment_id, variation_id, none_variation),
|
676
|
+
body: (data_not_sent.map(&:obtain_full_post_text_line).join("\n") || '').encode('UTF-8'),
|
677
|
+
head: { 'Content-Type': 'text/plain' }
|
660
678
|
}
|
679
|
+
set_user_agent_to_headers(visitor_code, options[:head])
|
661
680
|
trial = 0
|
662
681
|
success = false
|
663
|
-
log "Start post tracking experiment:
|
682
|
+
log "Start post tracking experiment: #{data_not_sent.inspect}"
|
664
683
|
Thread.new do
|
665
684
|
while trial < 10
|
666
|
-
log "Send Experiment Tracking
|
685
|
+
log "Send Experiment Tracking #{options.inspect}"
|
667
686
|
response = post_sync(options, @tracking_url)
|
668
|
-
log "Response
|
669
|
-
if
|
687
|
+
log "Response #{response.inspect}"
|
688
|
+
if successful_sync?(response)
|
670
689
|
data_not_sent.each { |it| it.sent = true }
|
671
690
|
success = true
|
672
691
|
break
|
@@ -674,9 +693,9 @@ module Kameleoon
|
|
674
693
|
trial += 1
|
675
694
|
end
|
676
695
|
if success
|
677
|
-
log "Post to experiment tracking is done after
|
678
|
-
else
|
679
|
-
log "Post to experiment tracking is failed after
|
696
|
+
log "Post to experiment tracking is done after #{trial + 1} trials"
|
697
|
+
else
|
698
|
+
log "Post to experiment tracking is failed after #{trial} trials"
|
680
699
|
end
|
681
700
|
end
|
682
701
|
end
|
@@ -685,17 +704,18 @@ module Kameleoon
|
|
685
704
|
Thread.new do
|
686
705
|
trials = 0
|
687
706
|
data_not_sent = data_not_sent(visitor_code)
|
688
|
-
log "Start post tracking data:
|
707
|
+
log "Start post tracking data: #{data_not_sent.inspect}"
|
689
708
|
while trials < 10 && !data_not_sent.empty?
|
690
709
|
options = {
|
691
|
-
:
|
692
|
-
:
|
693
|
-
:
|
710
|
+
path: get_data_register_url(visitor_code),
|
711
|
+
body: (data_not_sent.map(&:obtain_full_post_text_line).join("\n") || '').encode('UTF-8'),
|
712
|
+
head: { 'Content-Type': 'text/plain' }
|
694
713
|
}
|
695
|
-
|
714
|
+
set_user_agent_to_headers(visitor_code, options[:head])
|
715
|
+
log "Post tracking data for visitor_code: #{visitor_code} with options: #{options.inspect}"
|
696
716
|
response = post_sync(options, @tracking_url)
|
697
|
-
log "Response
|
698
|
-
if
|
717
|
+
log "Response #{response.inspect}"
|
718
|
+
if successful_sync?(response)
|
699
719
|
data_not_sent.each { |it| it.sent = true }
|
700
720
|
success = true
|
701
721
|
break
|
@@ -703,17 +723,15 @@ module Kameleoon
|
|
703
723
|
trials += 1
|
704
724
|
end
|
705
725
|
if success
|
706
|
-
log "Post to data tracking is done after
|
726
|
+
log "Post to data tracking is done after #{trials + 1} trials"
|
707
727
|
else
|
708
|
-
log "Post to data tracking is failed after
|
728
|
+
log "Post to data tracking is failed after #{trials} trials"
|
709
729
|
end
|
710
730
|
end
|
711
731
|
end
|
712
732
|
|
713
|
-
def check_site_code_enable(
|
714
|
-
|
715
|
-
raise Exception::SiteCodeDisabled.new(site_code)
|
716
|
-
end
|
733
|
+
def check_site_code_enable(campaign)
|
734
|
+
raise Exception::SiteCodeDisabled.new(site_code), site_code unless campaign.site_enabled
|
717
735
|
end
|
718
736
|
|
719
737
|
def data_not_sent(visitor_code)
|
@@ -721,8 +739,127 @@ module Kameleoon
|
|
721
739
|
end
|
722
740
|
|
723
741
|
def log(text)
|
724
|
-
if @verbose_mode
|
725
|
-
|
742
|
+
print "Kameleoon SDK Log: #{text}\n" if @verbose_mode
|
743
|
+
end
|
744
|
+
|
745
|
+
def add_user_agent_data(visitor_code, user_agent)
|
746
|
+
@user_agents[visitor_code] = user_agent
|
747
|
+
end
|
748
|
+
|
749
|
+
def set_user_agent_to_headers(visitor_code, headers)
|
750
|
+
user_agent = @user_agents[visitor_code]
|
751
|
+
headers['User-Agent'] = user_agent.value unless user_agent.nil?
|
752
|
+
end
|
753
|
+
|
754
|
+
# Uncomment when using storage
|
755
|
+
# def get_valid_saved_variation(visitor_code, experiment)
|
756
|
+
# variation_id = @variation_storage.get_variation_id(visitor_code, experiment.id.to_i)
|
757
|
+
# unless variation_id.nil?
|
758
|
+
# return @variation_storage.variation_valid?(visitor_code,
|
759
|
+
# experiment.id.to_i,
|
760
|
+
# experiment.respool_time[variation_id])
|
761
|
+
# end
|
762
|
+
# nil
|
763
|
+
# end
|
764
|
+
|
765
|
+
def check_targeting(visitor_code, campaign_id, exp_ff_rule)
|
766
|
+
segment = exp_ff_rule.targeting_segment
|
767
|
+
segment.nil? || segment.check_tree(->(type) { get_condition_data(type, visitor_code, campaign_id) })
|
768
|
+
end
|
769
|
+
|
770
|
+
def get_condition_data(type, visitor_code, campaign_id)
|
771
|
+
condition_data = nil
|
772
|
+
case type
|
773
|
+
when Kameleoon::Targeting::ConditionType::CUSTOM_DATUM
|
774
|
+
condition_data = (@data[visitor_code] || []).flatten
|
775
|
+
when Kameleoon::Targeting::ConditionType::TARGET_EXPERIMENT
|
776
|
+
condition_data = @variation_storage.get_hash_saved_variation_id(visitor_code)
|
777
|
+
when Kameleoon::Targeting::ConditionType::EXCLUSIVE_EXPERIMENT
|
778
|
+
condition_data = OpenStruct.new
|
779
|
+
condition_data.experiment_id = campaign_id
|
780
|
+
condition_data.storage = @variation_storage.get_hash_saved_variation_id(visitor_code)
|
781
|
+
end
|
782
|
+
condition_data
|
783
|
+
end
|
784
|
+
|
785
|
+
##
|
786
|
+
# helper method for getting variation key for feature flag
|
787
|
+
def _get_feature_variation_key(visitor_code, feature_key)
|
788
|
+
check_visitor_code(visitor_code)
|
789
|
+
feature_flag = find_feature_flag(feature_key)
|
790
|
+
variation, rule = _calculate_variation_key_for_feature(visitor_code, feature_flag)
|
791
|
+
variation_key = _get_variation_key(variation, rule, feature_flag)
|
792
|
+
unless rule.nil?
|
793
|
+
save_variation(visitor_code, rule.experiment_id, variation.variation_id) unless variation.nil?
|
794
|
+
variation_id = variation.variation_id unless variation.nil?
|
795
|
+
_send_tracking_request(visitor_code, rule.experiment_id, variation_id)
|
796
|
+
end
|
797
|
+
[feature_flag, variation_key]
|
798
|
+
end
|
799
|
+
|
800
|
+
##
|
801
|
+
# helper method for calculate variation key for feature flag
|
802
|
+
def _calculate_variation_key_for_feature(visitor_code, feature_flag)
|
803
|
+
# no rules -> return default_variation_key
|
804
|
+
feature_flag.rules.each do |rule|
|
805
|
+
# check if visitor is targeted for rule, else next rule
|
806
|
+
next unless check_targeting(visitor_code, feature_flag.id, rule)
|
807
|
+
|
808
|
+
# uses for rule exposition
|
809
|
+
hash_rule = obtain_hash_double_rule(visitor_code, rule.id, rule.respool_time)
|
810
|
+
# check main expostion for rule with hashRule
|
811
|
+
if hash_rule <= rule.exposition
|
812
|
+
# uses for variation's expositions
|
813
|
+
hash_variation = obtain_hash_double_rule(visitor_code, rule.experiment_id, rule.respool_time)
|
814
|
+
# get variation key with new hashVariation
|
815
|
+
variation = rule.get_variation(hash_variation)
|
816
|
+
# variation_key can be nil for experiment rules only, for targeted rule will be always exist
|
817
|
+
return [variation, rule] unless variation.nil?
|
818
|
+
# if visitor is targeted for targeted rule then break cycle -> return default
|
819
|
+
elsif rule.targeted_delivery_type?
|
820
|
+
break
|
821
|
+
end
|
822
|
+
end
|
823
|
+
[nil, nil]
|
824
|
+
end
|
825
|
+
|
826
|
+
def save_variation(visitor_code, experiment_id, variation_id)
|
827
|
+
return if experiment_id.nil? || variation_id.nil?
|
828
|
+
|
829
|
+
@variation_storage.update_variation(visitor_code, experiment_id, variation_id)
|
830
|
+
@hybrid_manager.add_variation(visitor_code, experiment_id, variation_id)
|
831
|
+
end
|
832
|
+
|
833
|
+
def _get_variation_key(var_by_exp, rule, feature_flag)
|
834
|
+
return var_by_exp.variation_key unless var_by_exp.nil?
|
835
|
+
|
836
|
+
return Kameleoon::Configuration::VariationType::VARIATION_OFF if !rule.nil? && rule.experiment_type?
|
837
|
+
|
838
|
+
feature_flag.default_variation_key
|
839
|
+
end
|
840
|
+
|
841
|
+
##
|
842
|
+
# helper method for sending tracking requests for new FF
|
843
|
+
def _send_tracking_request(visitor_code, experiment_id, variation_id)
|
844
|
+
if !experiment_id.nil?
|
845
|
+
variation_reference_id = variation_id || 0
|
846
|
+
track_experiment(visitor_code, experiment_id, variation_reference_id)
|
847
|
+
else
|
848
|
+
log 'An attempt to send a request with null experimentId was blocked'
|
849
|
+
end
|
850
|
+
end
|
851
|
+
|
852
|
+
##
|
853
|
+
# helper method for fetching values from a Variable
|
854
|
+
def _parse_feature_variable(variable)
|
855
|
+
case variable.type
|
856
|
+
when 'BOOLEAN', 'STRING', 'NUMBER'
|
857
|
+
variable.value
|
858
|
+
when 'JSON'
|
859
|
+
JSON.parse(variable.value)
|
860
|
+
else
|
861
|
+
raise TypeError.new('Unknown type for feature variable'),
|
862
|
+
'Unknown type for feature variable'
|
726
863
|
end
|
727
864
|
end
|
728
865
|
end
|