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