kameleoon-client-ruby 1.1.1 → 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,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