kameleoon-client-ruby 1.1.2 → 2.1.0

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