kameleoon-client-ruby 1.1.2 → 2.1.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.
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