kameleoon-client-ruby 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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