kameleoon-client-ruby 1.1.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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/query_graphql'
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, blocking, interval, default_timeout, client_id = nil, client_secret = nil)
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 = obtain_visitor_code(cookies, 'my-domaine.com')
78
+ # visitor_code = get_visitor_code(cookies, 'my-domaine.com')
73
79
  #
74
- def obtain_visitor_code(cookies, top_level_domain, default_visitor_code = nil)
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::NotActivated] The visitor triggered the experiment, but did not activate it. Usually, this happens because the user has been associated with excluded traffic
94
- # @raise [Kameleoon::Exception::NotTargeted] The visitor is not targeted by the experiment, as the associated targeting segment conditions were not fulfilled. He should see the reference variation
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, timeout = @default_timeout)
111
+ def trigger_experiment(visitor_code, experiment_id)
98
112
  check_visitor_code(visitor_code)
99
- experiment = @experiments.find { |experiment| experiment['id'].to_s == experiment_id.to_s }
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
- if @blocking
104
- variation_id = nil
105
- EM.synchrony do
106
- connexion_options = { :connect_timeout => (timeout.to_f / 1000.0) }
107
- body = (data_not_sent(visitor_code).map { |data| data.obtain_full_post_text_line }.join("\n") || "").encode("UTF-8")
108
- path = get_experiment_register_url(visitor_code, experiment_id)
109
- request_options = { :path => path, :body => body }
110
- log "Trigger experiment request: " + request_options.inspect
111
- log "Trigger experiment connexion:" + connexion_options.inspect
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::NotActivated.new(visitor_code)
127
+ raise Exception::NotAllocated.new(visitor_code),
128
+ "Experiment #{experiment_id} is not active for visitor #{visitor_code}"
141
129
  end
142
- raise Exception::NotTargeted.new(visitor_code)
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
- unless args.empty?
165
- if @data.key?(visitor_code)
166
- @data[visitor_code].push(*args)
167
- else
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. The visitor_code usually is identical to the one that was used when triggering the experiment.
178
- # The track_conversion method doesn't return any value. This method is non-blocking as the server call is made asynchronously.
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 obtain_variation_associated_data method of our SDK.
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 obtain_variation_associated_data(variation_id)
228
- variation = @experiments.map { |experiment| experiment['variations'] }.flatten.select { |variation| variation['id'].to_i == variation_id.to_i }.first
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 visitor_code and feature_key (or feature_id) as mandatory arguments to check if the specified feature will be active for a given user.
240
- # If such a user has never been associated with this feature flag, the SDK returns a boolean value randomly (true if the user should have this feature or false if not). If a user with a given visitorCode is already registered with this feature flag, it will detect the previous featureFlag value.
241
- # You have to make sure that proper error handling is set up in your code as shown in the example to the right to catch potential exceptions.
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 | Integer] feature_key
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 activate_feature(visitor_code, feature_key, timeout = @default_timeout)
251
- check_visitor_code(visitor_code)
252
- feature_flag = get_feature_flag(feature_key)
253
- id = feature_flag['id']
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
- else
276
- check_site_code_enable(feature_flag)
277
- visitor_data = @data.select { |key, value| key.to_s == visitor_code }.values.flatten! || []
278
- unless feature_flag['targetingSegment'].nil? || feature_flag['targetingSegment'].check_tree(visitor_data)
279
- raise Exception::NotTargeted.new(visitor_code)
280
- end
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
- if is_feature_flag_scheduled(feature_flag, Time.now.to_i)
283
- threshold = obtain_hash_double(visitor_code, {}, id)
284
- if threshold >= 1 - feature_flag['expositionRate']
285
- track_experiment(visitor_code, id, feature_flag["variations"].first['id'])
286
- true
287
- else
288
- track_experiment(visitor_code, id, REFERENCE, true)
289
- false
290
- end
291
- else
292
- false
293
- end
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
- feature_flag = get_feature_flag(feature_key)
310
- custom_json = JSON.parse(feature_flag["variations"].first['customJson'])[variable_key.to_s]
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("Feature variable not found")
387
+ raise Exception::FeatureVariableNotFound.new('Feature variable not found')
313
388
  end
314
389
  case custom_json['type']
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'])
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("Unknown type for feature variable")
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 is_successful_sync(response)
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
- :blocking, :tracking_url, :default_timeout, :interval, :memory_limit, :verbose_mode
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
- fetch_configuration_job_graphql
489
+ fetch_configuration_job
367
490
  end
368
491
  @scheduler.schedule '0s' do
369
492
  log('Start-up, fetching is starting')
370
- fetch_configuration_job_graphql
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
- obtain_access_token
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
- if @access_token.nil?
401
- CredentialsNotFound.new
402
- end
403
- {
404
- 'Authorization' => 'Bearer ' + @access_token.to_s,
405
- 'Content-Type' => 'application/json'
406
- }
407
- end
408
-
409
- def hash_filter(field, operator, parameters)
410
- { 'field' => field, 'operator' => operator, 'parameters' => parameters }
411
- end
412
-
413
- def obtain_site
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
424
-
425
- def obtain_access_token
426
- log "Fetching bearer token"
427
- body = {
428
- 'grant_type' => 'client_credentials',
429
- 'client_id' => @client_id,
430
- 'client_secret' => @client_secret
431
- }
432
- header = { 'Content-Type' => 'application/x-www-form-urlencoded' }
433
-
434
- request = EM::Synchrony.sync post({ :path => '/oauth/token', :body => body, :head => header })
435
- if is_successful(request)
436
- @access_token = JSON.parse(request.response)['access_token']
437
- log "Bearer Token is fetched: " + @access_token.to_s
438
- else
439
- log "Failed to fetch bearer token: " + request.inspect
440
- end
441
- end
442
-
443
- def obtain_variation(variation_id)
444
- request = fetch_one('variations/' + variation_id.to_s)
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
- :nonce => Kameleoon::Utils.generate_random_string(16),
594
- :siteCode => @site_code,
595
- :visitorCode => visitor_code
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?" + URI.encode_www_form(get_common_ssx_parameters(visitor_code))
601
- url += "&experimentId=" + experiment_id.to_s
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=" + variation_id.to_s
569
+ url += "&variationId=#{variation_id}"
606
570
  if none_variation
607
- url += "&noneVariation=true"
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?" + URI.encode_www_form(get_common_ssx_parameters(visitor_code))
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 get_feature_flag(feature_key)
625
- if feature_key.is_a?(String)
626
- feature_flag = @feature_flags.select { |ff| ff['identificationKey'] == feature_key}.first
627
- elsif feature_key.is_a?(Integer)
628
- feature_flag = @feature_flags.select { |ff| ff['id'].to_i == feature_key}.first
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("Feature key should be a String or an Integer.")
633
- end
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 is_feature_flag_scheduled(feature_flag, date)
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
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
- false
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 = false)
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
- :path => get_experiment_register_url(visitor_code, experiment_id, variation_id, none_variation),
658
- :body => (data_not_sent.map{ |it| it.obtain_full_post_text_line }.join("\n") || "").encode("UTF-8"),
659
- :head => { "Content-Type" => "text/plain"}
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: " + data_not_sent.inspect
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 " + options.inspect
632
+ log "Send Experiment Tracking #{options.inspect}"
667
633
  response = post_sync(options, @tracking_url)
668
- log "Response " + response.inspect
669
- if is_successful_sync(response)
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 " + (trial + 1).to_s + " trials"
678
- else
679
- log "Post to experiment tracking is failed after " + trial.to_s + " trials"
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: " + data_not_sent.inspect
654
+ log "Start post tracking data: #{data_not_sent.inspect}"
689
655
  while trials < 10 && !data_not_sent.empty?
690
656
  options = {
691
- :path => get_data_register_url(visitor_code),
692
- :body => (data_not_sent.map{ |it| it.obtain_full_post_text_line }.join("\n") || "").encode("UTF-8"),
693
- :head => { "Content-Type" => "text/plain" }
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
- log "Post tracking data for visitor_code: " + visitor_code + " with options: " + options.inspect
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 " + response.inspect
698
- if is_successful_sync(response)
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 " + (trials + 1).to_s + " trials"
673
+ log "Post to data tracking is done after #{trials + 1} trials"
707
674
  else
708
- log "Post to data tracking is failed after " + trials.to_s + " trials"
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(exp_or_ff)
714
- unless exp_or_ff['site'].nil? || exp_or_ff['site']['isKameleoonEnabled']
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
- print "Kameleoon Log: " + text.to_s + "\n"
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