kameleoon-client-ruby 2.0.0 → 2.1.1

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kameleoon/client.rb +277 -260
  3. data/lib/kameleoon/configuration/feature_flag.rb +13 -24
  4. data/lib/kameleoon/configuration/rule.rb +17 -5
  5. data/lib/kameleoon/configuration/settings.rb +20 -0
  6. data/lib/kameleoon/cookie.rb +3 -3
  7. data/lib/kameleoon/data/browser.rb +33 -0
  8. data/lib/kameleoon/data/conversion.rb +26 -0
  9. data/lib/kameleoon/data/custom_data.rb +53 -0
  10. data/lib/kameleoon/data/data.rb +35 -0
  11. data/lib/kameleoon/data/device.rb +26 -0
  12. data/lib/kameleoon/data/page_view.rb +31 -0
  13. data/lib/kameleoon/data/user_agent.rb +14 -0
  14. data/lib/kameleoon/hybrid/manager.rb +60 -0
  15. data/lib/kameleoon/network/activity_event.rb +31 -0
  16. data/lib/kameleoon/network/experiment_event.rb +35 -0
  17. data/lib/kameleoon/network/uri_helper.rb +36 -0
  18. data/lib/kameleoon/network/url_provider.rb +71 -0
  19. data/lib/kameleoon/real_time/real_time_configuration_service.rb +98 -0
  20. data/lib/kameleoon/real_time/real_time_event.rb +22 -0
  21. data/lib/kameleoon/real_time/sse_client.rb +111 -0
  22. data/lib/kameleoon/real_time/sse_message.rb +23 -0
  23. data/lib/kameleoon/real_time/sse_request.rb +59 -0
  24. data/lib/kameleoon/request.rb +5 -19
  25. data/lib/kameleoon/storage/cache.rb +84 -0
  26. data/lib/kameleoon/storage/cache_factory.rb +23 -0
  27. data/lib/kameleoon/targeting/condition.rb +41 -12
  28. data/lib/kameleoon/targeting/condition_factory.rb +35 -12
  29. data/lib/kameleoon/targeting/conditions/browser_condition.rb +71 -0
  30. data/lib/kameleoon/targeting/conditions/conversion_condition.rb +21 -0
  31. data/lib/kameleoon/targeting/conditions/custom_datum.rb +64 -34
  32. data/lib/kameleoon/targeting/conditions/device_condition.rb +21 -0
  33. data/lib/kameleoon/targeting/conditions/exclusive_experiment.rb +0 -12
  34. data/lib/kameleoon/targeting/conditions/page_title_condition.rb +21 -0
  35. data/lib/kameleoon/targeting/conditions/page_url_condition.rb +21 -0
  36. data/lib/kameleoon/targeting/conditions/sdk_language_condition.rb +65 -0
  37. data/lib/kameleoon/targeting/conditions/string_value_condition.rb +40 -0
  38. data/lib/kameleoon/targeting/conditions/target_experiment.rb +5 -9
  39. data/lib/kameleoon/targeting/conditions/unknown_condition.rb +15 -0
  40. data/lib/kameleoon/targeting/conditions/visitor_code_condition.rb +16 -0
  41. data/lib/kameleoon/targeting/models.rb +0 -24
  42. data/lib/kameleoon/utils.rb +1 -1
  43. data/lib/kameleoon/version.rb +28 -1
  44. metadata +45 -4
  45. data/lib/kameleoon/configuration/feature_flag_v2.rb +0 -30
  46. data/lib/kameleoon/data.rb +0 -169
@@ -4,10 +4,17 @@ require 'kameleoon/targeting/models'
4
4
  require 'kameleoon/request'
5
5
  require 'kameleoon/exceptions'
6
6
  require 'kameleoon/cookie'
7
+ require 'kameleoon/data/user_agent'
7
8
  require 'kameleoon/configuration/feature_flag'
8
9
  require 'kameleoon/configuration/variation'
9
- require 'kameleoon/configuration/feature_flag_v2'
10
+ require 'kameleoon/configuration/settings'
11
+ require 'kameleoon/network/activity_event'
12
+ require 'kameleoon/network/experiment_event'
13
+ require 'kameleoon/network/url_provider'
14
+ require 'kameleoon/real_time/real_time_configuration_service'
10
15
  require 'kameleoon/storage/variation_storage'
16
+ require 'kameleoon/hybrid/manager'
17
+ require 'kameleoon/storage/cache_factory'
11
18
  require 'rufus/scheduler'
12
19
  require 'yaml'
13
20
  require 'json'
@@ -36,19 +43,28 @@ module Kameleoon
36
43
  @default_timeout = config['default_timeout'] || default_timeout # in ms
37
44
  refresh_interval = config['actions_configuration_refresh_interval']
38
45
  @interval = refresh_interval.nil? ? interval : "#{refresh_interval}m"
39
- @tracking_url = config['tracking_url'] || "https://api-ssx.kameleoon.com"
40
- @api_data_url = "https://api-data.kameleoon.com"
46
+ data_api_url = config['tracking_url'] || Network::UrlProvider::DEFAULT_DATA_API_URL
47
+ @url_provider = Network::UrlProvider.new(@site_code, data_api_url)
48
+ @real_time_configuration_service = nil
49
+ @update_configuration_handler = nil
50
+ @fetch_configuration_update_job = nil
41
51
  @client_id = client_id || config['client_id']
42
52
  @client_secret = client_secret || config['client_secret']
43
53
  @data_maximum_size = config['visitor_data_maximum_size'] || 500 # mb
44
54
  @environment = config['environment'] || DEFAULT_ENVIRONMENT
55
+ @settings = Kameleoon::Configuration::Settings.new
45
56
  @verbose_mode = config['verbose_mode'] || false
46
57
  @experiments = []
47
58
  @feature_flags = []
48
- @feature_flags_v2 = []
49
59
  @data = {}
50
60
  @user_agents = {}
51
61
  @variation_storage = Kameleoon::Storage::VariationStorage.new
62
+ @hybrid_manager = Kameleoon::Hybrid::ManagerImpl.new(
63
+ CACHE_EXPIRATION_TIMEOUT,
64
+ CACHE_EXPIRATION_TIMEOUT * 3,
65
+ Kameleoon::Storage::CacheFactoryImpl.new,
66
+ method(:log)
67
+ )
52
68
  end
53
69
 
54
70
  ##
@@ -112,25 +128,26 @@ module Kameleoon
112
128
  check_visitor_code(visitor_code)
113
129
  experiment = @experiments.find { |exp| exp.id.to_s == experiment_id.to_s }
114
130
  if experiment.nil?
115
- raise Exception::ExperimentConfigurationNotFound.new(experiment_id)
131
+ raise Exception::ExperimentConfigurationNotFound.new(experiment_id),
132
+ "Experiment #{experiment_id} is not found"
116
133
  end
117
134
  check_site_code_enable(experiment)
118
- if check_targeting(visitor_code, experiment_id, experiment)
119
- saved_variation = get_valid_saved_variation(visitor_code, experiment)
120
- variation_key = saved_variation || variation_for_experiment(visitor_code, experiment)
121
- if !variation_key.nil?
122
- track_experiment(visitor_code, experiment_id, variation_key)
123
- @variation_storage.update_variation(visitor_code, experiment_id, variation_key) if saved_variation.nil?
124
- variation_key
125
- else
126
- track_experiment(visitor_code, experiment_id, REFERENCE, true)
127
- raise Exception::NotAllocated.new(visitor_code),
128
- "Experiment #{experiment_id} is not active for visitor #{visitor_code}"
129
- end
130
- else
135
+ targeted = check_targeting(visitor_code, experiment_id, experiment)
136
+ if targeted
137
+ # saved_variation = get_valid_saved_variation(visitor_code, experiment)
138
+ variation_id = calculate_variation_for_experiment(visitor_code, experiment)
139
+ save_variation(visitor_code, experiment_id, variation_id)
140
+ end
141
+ _send_tracking_request(visitor_code, experiment_id, variation_id)
142
+ unless targeted
131
143
  raise Exception::NotTargeted.new(visitor_code),
132
144
  "Experiment #{experiment_id} is not targeted for visitor #{visitor_code}"
133
145
  end
146
+ if variation_id.nil?
147
+ raise Exception::NotAllocated.new(visitor_code),
148
+ "Experiment #{experiment_id} is not active for visitor #{visitor_code}"
149
+ end
150
+ variation_id
134
151
  end
135
152
 
136
153
  ##
@@ -148,9 +165,7 @@ module Kameleoon
148
165
  #
149
166
  def add_data(visitor_code, *args)
150
167
  check_visitor_code(visitor_code)
151
- while ObjectSpace.memsize_of(@data) > @data_maximum_size * (2**20) do
152
- @data.shift
153
- end
168
+ @data.shift while ObjectSpace.memsize_of(@data) > @data_maximum_size * (2**20)
154
169
  args.each do |data_element|
155
170
  if data_element.is_a?(UserAgent)
156
171
  add_user_agent_data(visitor_code, data_element)
@@ -196,7 +211,7 @@ module Kameleoon
196
211
  def flush(visitor_code = nil)
197
212
  check_visitor_code(visitor_code) unless visitor_code.nil?
198
213
  if !visitor_code.nil?
199
- track_data(visitor_code)
214
+ _send_tracking_request(visitor_code)
200
215
  else
201
216
  @data.select { |_, values| values.any? { |data| !data.sent } }.each_key { |key| flush(key) }
202
217
  end
@@ -252,28 +267,11 @@ module Kameleoon
252
267
  # @raise [Kameleoon::Exception::NotTargeted] The visitor is not targeted by the experiment, as the
253
268
  # associated targeting segment conditions were not fulfilled. He should see the reference variation
254
269
  # @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
255
-
270
+ #
271
+ # DEPRECATED. Please use `is_feature_active` instead.
256
272
  def activate_feature(visitor_code, feature_key)
257
- check_visitor_code(visitor_code)
258
- feature_flag = find_feature_flag(feature_key)
259
- check_site_code_enable(feature_flag)
260
- unless check_targeting(visitor_code, feature_flag.id.to_i, feature_flag)
261
- raise Exception::NotTargeted.new(visitor_code),
262
- "Visitor '#{visitor_code}' is not targeted for FF with key '#{feature_key}'"
263
- end
264
-
265
- if feature_flag.is_scheduled_active(Time.now.to_i)
266
- threshold = obtain_hash_double(visitor_code, {}, feature_flag.id)
267
- if threshold >= 1 - feature_flag.exposition_rate
268
- track_experiment(visitor_code, feature_flag.id, feature_flag.variations.first['id'])
269
- true
270
- else
271
- track_experiment(visitor_code, feature_flag.id, REFERENCE, none_variation: true)
272
- false
273
- end
274
- else
275
- false
276
- end
273
+ warn '[DEPRECATION] `activate_feature` is deprecated. Please use `feature_active?` instead.'
274
+ feature_active?(visitor_code, feature_key)
277
275
  end
278
276
 
279
277
  ##
@@ -356,7 +354,7 @@ module Kameleoon
356
354
  # @raise [Kameleoon::Exception::VariationConfigurationNotFound]
357
355
  #
358
356
  def get_feature_all_variables(feature_key, variation_key)
359
- feature_flag = find_feature_flag_v2(feature_key)
357
+ feature_flag = find_feature_flag(feature_key)
360
358
  variation = feature_flag.get_variation_key(variation_key)
361
359
  if variation.nil?
362
360
  raise Exception::VariationConfigurationNotFound.new(variation_key),
@@ -381,54 +379,47 @@ module Kameleoon
381
379
  # DEPRECATED. Please use `get_feature_variable` instead.
382
380
  def obtain_feature_variable(feature_key, variable_key)
383
381
  warn '[DEPRECATION] `obtain_feature_variable` is deprecated. Please use `get_feature_variable` instead.'
384
- feature_flag = find_feature_flag(feature_key)
385
- custom_json = JSON.parse(feature_flag.variations.first['customJson'])[variable_key.to_s]
386
- if custom_json.nil?
387
- raise Exception::FeatureVariableNotFound.new('Feature variable not found')
388
- end
389
- case custom_json['type']
390
- when 'Boolean'
391
- custom_json['value']
392
- when 'String'
393
- custom_json['value']
394
- when 'Number'
395
- custom_json['value'].to_i
396
- when 'JSON'
397
- JSON.parse(custom_json['value'])
398
- else
399
- raise TypeError.new('Unknown type for feature variable')
400
- end
382
+ all_variables = get_feature_all_variables(
383
+ feature_key,
384
+ Configuration::VariationType::VARIATION_OFF
385
+ )
386
+ all_variables[variable_key]
401
387
  end
402
388
 
403
389
  ##
404
- # The retrieved_data_from_remote_source method allows you to retrieve data (according to a key passed as argument)
405
- # stored on a remote Kameleoon server. Usually data will be stored on our remote servers via the use of our Data API.
406
- # This method, along with the availability of our highly scalable servers for this purpose, provides a convenient way
407
- # to quickly store massive amounts of data that can be later retrieved for each of your visitors / users.
390
+ # The get_remote_data method allows you to retrieve data (according to a key passed as argument)
391
+ # stored on a remote Kameleoon server. Usually data will be stored on our remote
392
+ # servers via the use of our Data API.
393
+ # This method, along with the availability of our highly scalable servers for this purpose, provides a convenient
394
+ # way to quickly store massive amounts of data that can be later retrieved for each of your visitors / users.
408
395
  #
409
396
  # @param [String] key Key you want to retrieve data. This field is mandatory.
410
- # @param [Int] timeout Timeout for request. Equals default_timeout in a config file. This field is optional.
397
+ # @param [Int] timeout Timeout for request (in milliseconds). Equals default_timeout in a config file.
398
+ # This field is optional.
411
399
  #
412
400
  # @return [Hash] Hash object of the json object.
413
- #
414
- #
415
- def retrieve_data_from_remote_source(key, timeout = @default_timeout)
401
+ def get_remote_data(key, timeout = @default_timeout)
416
402
  connexion_options = { connect_timeout: (timeout.to_f / 1000.0) }
417
- path = get_api_data_request_url(key)
403
+ url = @url_provider.make_api_data_get_request_url(key)
418
404
  log "Retrieve API Data connexion: #{connexion_options.inspect}"
419
- response = get_sync(@api_data_url + path, connexion_options)
420
- if successful_sync?(response)
421
- JSON.parse(response.body) unless response.nil?
422
- else
423
- return nil
424
- end
405
+ response = get_sync(url, connexion_options)
406
+ return nil unless successful_sync?(response)
407
+
408
+ JSON.parse(response.body) unless response.nil?
409
+ end
410
+
411
+ ##
412
+ # DEPRECATED. Please use `get_feature_variable` instead.
413
+ def retrieve_data_from_remote_source(key, timeout = @default_timeout)
414
+ warn '[DEPRECATION] `retrieve_data_from_remote_source` is deprecated. Please use `get_remote_date` instead.'
415
+ get_remote_data(key, timeout)
425
416
  end
426
417
 
427
418
  ##
428
419
  # Returns a list of all experiment ids
429
420
  #
430
421
  # @return [Array] array of all experiment ids
431
- def get_experiment_list
422
+ def get_experiment_list # rubocop:disable Naming/AccessorMethodName
432
423
  @experiments.map { |it| it.id.to_i }
433
424
  end
434
425
 
@@ -443,7 +434,7 @@ module Kameleoon
443
434
  list_ids = []
444
435
  @experiments.each do |experiment|
445
436
  next unless check_targeting(visitor_code, experiment.id.to_i, experiment)
446
- next if only_allocated && variation_for_experiment(visitor_code, experiment).nil?
437
+ next if only_allocated && calculate_variation_for_experiment(visitor_code, experiment).nil?
447
438
 
448
439
  list_ids.push(experiment.id.to_i)
449
440
  end
@@ -454,8 +445,8 @@ module Kameleoon
454
445
  # Returns a list of all feature flag keys
455
446
  #
456
447
  # @return [Array] array of all feature flag keys
457
- def get_feature_list
458
- @feature_flags_v2.map(&:feature_key)
448
+ def get_feature_list # rubocop:disable Naming/AccessorMethodName
449
+ @feature_flags.map(&:feature_key)
459
450
  end
460
451
 
461
452
  ##
@@ -467,40 +458,113 @@ module Kameleoon
467
458
  def get_active_feature_list_for_visitor(visitor_code)
468
459
  check_visitor_code(visitor_code)
469
460
  list_keys = []
470
- @feature_flags_v2.each do |feature_flag|
471
- variation_key, = _calculate_variation_key_for_feature(visitor_code, feature_flag)
472
- list_keys.push(feature_flag.feature_key) if variation_key != Kameleoon::Configuration::VariationType::VARIATION_OFF
461
+ @feature_flags.each do |feature_flag|
462
+ variation, rule, = _calculate_variation_key_for_feature(visitor_code, feature_flag)
463
+ variation_key = _get_variation_key(variation, rule, feature_flag)
464
+ if variation_key != Kameleoon::Configuration::VariationType::VARIATION_OFF
465
+ list_keys.push(feature_flag.feature_key)
466
+ end
473
467
  end
474
468
  list_keys
475
469
  end
476
470
 
471
+ ##
472
+ # The `on_update_configuration()` method allows you to handle the event when configuration
473
+ # has updated data. It takes one input parameter: callable **handler**. The handler
474
+ # that will be called when the configuration is updated using a real-time configuration event.
475
+ #
476
+ # @param handler [Callable | NilClass] The handler that will be called when the configuration
477
+ # is updated using a real-time configuration event.
478
+ def on_update_configuration(handler)
479
+ @update_configuration_handler = handler
480
+ end
481
+
482
+ ##
483
+ # The `get_engine_tracking_code` returns the JavasScript code to be inserted in your page
484
+ # to send automatically the exposure events to the analytics solution you are using.
485
+ #
486
+ # @param [String] visitor_code The user's unique identifier. This field is mandatory.
487
+ #
488
+ # @return [String] JavasScript code to be inserted in your page to send automatically
489
+ # the exposure events to the analytics solution you are using.
490
+ def get_engine_tracking_code(visitor_code)
491
+ @hybrid_manager.get_engine_tracking_code(visitor_code)
492
+ end
493
+
477
494
  private
478
495
 
479
- API_SSX_URL = 'https://api-ssx.kameleoon.com'
480
496
  REFERENCE = 0
481
497
  DEFAULT_ENVIRONMENT = 'production'
482
- attr :site_code, :client_id, :client_secret, :access_token, :experiments, :feature_flags, :feature_flags_v2, :scheduler, :data,
498
+ CACHE_EXPIRATION_TIMEOUT = 5
499
+ attr :site_code, :client_id, :client_secret, :access_token, :experiments, :feature_flags, :scheduler, :data,
483
500
  :tracking_url, :default_timeout, :interval, :memory_limit, :verbose_mode
484
501
 
485
502
  def fetch_configuration
486
- @scheduler = Rufus::Scheduler.singleton
487
- @scheduler.every @interval do
488
- log('Scheduled job to fetch configuration is starting.')
489
- fetch_configuration_job
490
- end
491
- @scheduler.schedule '0s' do
492
- log('Start-up, fetching is starting')
503
+ Rufus::Scheduler.singleton.in '0s' do
504
+ log('Initial configuration fetch is started.')
493
505
  fetch_configuration_job
494
506
  end
495
507
  end
496
508
 
497
- def fetch_configuration_job
509
+ def fetch_configuration_job(time_stamp = nil)
498
510
  EM.synchrony do
499
- obtain_configuration(@site_code, @environment)
511
+ begin
512
+ ok = obtain_configuration(@environment, time_stamp)
513
+ if !ok && @settings.real_time_update
514
+ @settings.real_time_update = false
515
+ log('Switching to polling mode due to failed fetch')
516
+ end
517
+ rescue StandardError => e
518
+ log("Error occurred during configuration fetching: #{e}")
519
+ end
520
+ manage_configuration_update(@settings.real_time_update)
500
521
  EM.stop
501
522
  end
502
523
  end
503
524
 
525
+ def start_configuration_update_job_if_needed
526
+ return unless @fetch_configuration_update_job.nil?
527
+
528
+ @fetch_configuration_update_job = Rufus::Scheduler.singleton.schedule_every @interval do
529
+ log('Scheduled job to fetch configuration is started.')
530
+ fetch_configuration_job
531
+ end
532
+ end
533
+
534
+ def stop_configuration_update_job_if_needed
535
+ return if @fetch_configuration_update_job.nil?
536
+
537
+ @fetch_configuration_update_job.unschedule
538
+ @fetch_configuration_update_job = nil
539
+ log('Scheduled job to fetch configuration is stopped.')
540
+ end
541
+
542
+ def start_real_time_configuration_service_if_needed
543
+ return unless @real_time_configuration_service.nil?
544
+
545
+ url = @url_provider.make_real_time_url
546
+ fetch_func = proc { |real_time_event| fetch_configuration_job(real_time_event.time_stamp) }
547
+ @real_time_configuration_service =
548
+ Kameleoon::RealTime::RealTimeConfigurationService.new(url, fetch_func, method(:log))
549
+ end
550
+
551
+ def stop_real_time_configuration_service_if_needed
552
+ return if @real_time_configuration_service.nil?
553
+
554
+ @real_time_configuration_service.close
555
+ @real_time_configuration_service = nil
556
+ end
557
+
558
+ def manage_configuration_update(is_real_time_update)
559
+ if is_real_time_update
560
+ stop_configuration_update_job_if_needed
561
+ start_real_time_configuration_service_if_needed
562
+ else
563
+ stop_real_time_configuration_service_if_needed
564
+ start_configuration_update_job_if_needed
565
+ end
566
+ end
567
+
504
568
  # def hash_headers
505
569
  # if @access_token.nil?
506
570
  # CredentialsNotFound.new
@@ -515,26 +579,35 @@ module Kameleoon
515
579
  # { 'field' => field, 'operator' => operator, 'parameters' => parameters }
516
580
  # end
517
581
 
518
- def obtain_configuration(site_code, environment = @environment)
519
- log "Fetching configuration from Client-Config service"
520
- request_path = "mobile?siteCode=#{site_code}"
521
- request_path += "&environment=#{environment}" unless environment.nil?
522
- request = request_configuration(request_path)
523
- return unless request
582
+ def obtain_configuration(environment = @environment, time_stamp = nil)
583
+ log 'Fetching configuration from Client-Config service'
584
+ url = @url_provider.make_configuration_url(environment, time_stamp)
585
+ request = request_configuration(url)
586
+ return false unless request
524
587
 
525
588
  configuration = JSON.parse(request.response)
526
589
  @experiments = Kameleoon::Configuration::Experiment.create_from_array(configuration['experiments']) ||
527
590
  @experiments
528
- @feature_flags = Kameleoon::Configuration::FeatureFlag.create_from_array(configuration['featureFlags']) ||
529
- @feature_flags
530
- feature_flags_v2_new = Kameleoon::Configuration::FeatureFlagV2.create_from_array(
591
+ @feature_flags = Kameleoon::Configuration::FeatureFlag.create_from_array(
531
592
  configuration['featureFlagConfigurations']
532
593
  )
533
- @feature_flags_v2 = feature_flags_v2_new || @feature_flags_v2
594
+ @settings.update(configuration['configuration'])
595
+ call_update_handler_if_needed(!time_stamp.nil?)
534
596
  log "Feature flags are fetched: #{request.inspect}"
597
+ true
535
598
  end
536
599
 
537
- def variation_for_experiment(visitor_code, experiment)
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
608
+ end
609
+
610
+ def calculate_variation_for_experiment(visitor_code, experiment)
538
611
  threshold = obtain_hash_double(visitor_code, experiment.respool_time, experiment.id)
539
612
  experiment.deviations.each do |key, value|
540
613
  threshold -= value
@@ -543,8 +616,8 @@ module Kameleoon
543
616
  nil
544
617
  end
545
618
 
546
- def request_configuration(path)
547
- request = EM::Synchrony.sync get({ path: path }, CLIENT_CONFIG_URL)
619
+ def request_configuration(url)
620
+ request = EM::Synchrony.sync get({}, url)
548
621
  unless successful?(request)
549
622
  log "Failed to fetch #{request.inspect}"
550
623
  return false
@@ -552,60 +625,9 @@ module Kameleoon
552
625
  request
553
626
  end
554
627
 
555
- def get_common_ssx_parameters(visitor_code)
556
- {
557
- nonce: Kameleoon::Utils.generate_random_string(16),
558
- siteCode: @site_code,
559
- visitorCode: visitor_code
560
- }
561
- end
562
-
563
- def get_experiment_register_url(visitor_code, experiment_id, variation_id = nil, none_variation = false)
564
- url = "/experimentTracking?#{URI.encode_www_form(get_common_ssx_parameters(visitor_code))}"
565
- url += "&experimentId=#{experiment_id}"
566
- if variation_id.nil?
567
- return url
568
- end
569
- url += "&variationId=#{variation_id}"
570
- if none_variation
571
- url += '&noneVariation=true'
572
- end
573
- url
574
- end
575
-
576
- def get_data_register_url(visitor_code)
577
- "/dataTracking?#{URI.encode_www_form(get_common_ssx_parameters(visitor_code))}"
578
- end
579
-
580
- def get_api_data_request_url(key)
581
- mapKey = {
582
- siteCode: site_code,
583
- key: key
584
- }
585
- "/data?#{URI.encode_www_form(mapKey)}"
586
- end
587
-
588
628
  def find_feature_flag(feature_key)
589
- case feature_key
590
- when String
591
- feature_flag = @feature_flags.select { |ff| ff.identification_key == feature_key }.first
592
- when Integer
593
- feature_flag = @feature_flags.select { |ff| ff.id.to_i == feature_key }.first
594
- print "\nPassing `feature_key` with type of `int` to `activate_feature` or `obtain_feature_variable` "\
595
- "is deprecated, it will be removed in next releases. This is necessary to support multi-environment feature\n"
596
- else
597
- raise TypeError.new('Feature key should be a String or an Integer.'),
598
- 'Feature key should be a String or an Integer.'
599
- end
600
- error_message = "Feature #{feature_key} not found"
601
- raise Exception::FeatureConfigurationNotFound.new(feature_key), error_message if feature_flag.nil?
602
-
603
- feature_flag
604
- end
605
-
606
- def find_feature_flag_v2(feature_key)
607
629
  if feature_key.is_a?(String)
608
- feature_flag = @feature_flags_v2.select { |ff| ff.feature_key == feature_key }.first
630
+ feature_flag = @feature_flags.select { |ff| ff.feature_key == feature_key }.first
609
631
  else
610
632
  raise TypeError.new('Feature key should be a String or an Integer.'),
611
633
  'Feature key should be a String or an Integer.'
@@ -616,67 +638,6 @@ module Kameleoon
616
638
  feature_flag
617
639
  end
618
640
 
619
- def track_experiment(visitor_code, experiment_id, variation_id = nil, none_variation: false)
620
- data_not_sent = data_not_sent(visitor_code)
621
- options = {
622
- path: get_experiment_register_url(visitor_code, experiment_id, variation_id, none_variation),
623
- body: (data_not_sent.map(&:obtain_full_post_text_line).join("\n") || '').encode('UTF-8'),
624
- head: { 'Content-Type': 'text/plain' }
625
- }
626
- set_user_agent_to_headers(visitor_code, options[:head])
627
- trial = 0
628
- success = false
629
- log "Start post tracking experiment: #{data_not_sent.inspect}"
630
- Thread.new do
631
- while trial < 10
632
- log "Send Experiment Tracking #{options.inspect}"
633
- response = post_sync(options, @tracking_url)
634
- log "Response #{response.inspect}"
635
- if successful_sync?(response)
636
- data_not_sent.each { |it| it.sent = true }
637
- success = true
638
- break
639
- end
640
- trial += 1
641
- end
642
- if success
643
- log "Post to experiment tracking is done after #{trial + 1} trials"
644
- else
645
- log "Post to experiment tracking is failed after #{trial} trials"
646
- end
647
- end
648
- end
649
-
650
- def track_data(visitor_code)
651
- Thread.new do
652
- trials = 0
653
- data_not_sent = data_not_sent(visitor_code)
654
- log "Start post tracking data: #{data_not_sent.inspect}"
655
- while trials < 10 && !data_not_sent.empty?
656
- options = {
657
- path: get_data_register_url(visitor_code),
658
- body: (data_not_sent.map(&:obtain_full_post_text_line).join("\n") || '').encode('UTF-8'),
659
- head: { 'Content-Type': 'text/plain' }
660
- }
661
- set_user_agent_to_headers(visitor_code, options[:head])
662
- log "Post tracking data for visitor_code: #{visitor_code} with options: #{options.inspect}"
663
- response = post_sync(options, @tracking_url)
664
- log "Response #{response.inspect}"
665
- if successful_sync?(response)
666
- data_not_sent.each { |it| it.sent = true }
667
- success = true
668
- break
669
- end
670
- trials += 1
671
- end
672
- if success
673
- log "Post to data tracking is done after #{trials + 1} trials"
674
- else
675
- log "Post to data tracking is failed after #{trials} trials"
676
- end
677
- end
678
- end
679
-
680
641
  def check_site_code_enable(campaign)
681
642
  raise Exception::SiteCodeDisabled.new(site_code), site_code unless campaign.site_enabled
682
643
  end
@@ -686,7 +647,7 @@ module Kameleoon
686
647
  end
687
648
 
688
649
  def log(text)
689
- print "Kameleoon Log: #{text}\n" if @verbose_mode
650
+ print "Kameleoon SDK Log: #{text}\n" if @verbose_mode
690
651
  end
691
652
 
692
653
  def add_user_agent_data(visitor_code, user_agent)
@@ -698,15 +659,16 @@ module Kameleoon
698
659
  headers['User-Agent'] = user_agent.value unless user_agent.nil?
699
660
  end
700
661
 
701
- def get_valid_saved_variation(visitor_code, experiment)
702
- variation_id = @variation_storage.get_variation_id(visitor_code, experiment.id.to_i)
703
- unless variation_id.nil?
704
- return @variation_storage.variation_valid?(visitor_code,
705
- experiment.id.to_i,
706
- experiment.respool_time[variation_id])
707
- end
708
- nil
709
- end
662
+ # Uncomment when using storage
663
+ # def get_valid_saved_variation(visitor_code, experiment)
664
+ # variation_id = @variation_storage.get_variation_id(visitor_code, experiment.id.to_i)
665
+ # unless variation_id.nil?
666
+ # return @variation_storage.variation_valid?(visitor_code,
667
+ # experiment.id.to_i,
668
+ # experiment.respool_time[variation_id])
669
+ # end
670
+ # nil
671
+ # end
710
672
 
711
673
  def check_targeting(visitor_code, campaign_id, exp_ff_rule)
712
674
  segment = exp_ff_rule.targeting_segment
@@ -716,8 +678,17 @@ module Kameleoon
716
678
  def get_condition_data(type, visitor_code, campaign_id)
717
679
  condition_data = nil
718
680
  case type
719
- when Kameleoon::Targeting::ConditionType::CUSTOM_DATUM
681
+ when Kameleoon::Targeting::ConditionType::CUSTOM_DATUM,
682
+ Kameleoon::Targeting::ConditionType::PAGE_TITLE,
683
+ Kameleoon::Targeting::ConditionType::PAGE_URL,
684
+ Kameleoon::Targeting::ConditionType::DEVICE_TYPE,
685
+ Kameleoon::Targeting::ConditionType::BROWSER,
686
+ Kameleoon::Targeting::ConditionType::CONVERSIONS
720
687
  condition_data = (@data[visitor_code] || []).flatten
688
+ when Kameleoon::Targeting::ConditionType::SDK_LANGUAGE
689
+ condition_data = Kameleoon::Targeting::SdkInfo.new(Kameleoon::SDK_NAME, Kameleoon::SDK_VERSION)
690
+ when Kameleoon::Targeting::ConditionType::VISITOR_CODE
691
+ condition_data = visitor_code
721
692
  when Kameleoon::Targeting::ConditionType::TARGET_EXPERIMENT
722
693
  condition_data = @variation_storage.get_hash_saved_variation_id(visitor_code)
723
694
  when Kameleoon::Targeting::ConditionType::EXCLUSIVE_EXPERIMENT
@@ -732,11 +703,15 @@ module Kameleoon
732
703
  # helper method for getting variation key for feature flag
733
704
  def _get_feature_variation_key(visitor_code, feature_key)
734
705
  check_visitor_code(visitor_code)
735
- feature_flag = find_feature_flag_v2(feature_key)
736
- variation_key, rule = _calculate_variation_key_for_feature(visitor_code, feature_flag)
737
- if !rule.nil? && rule.type == Kameleoon::Configuration::RuleType::EXPERIMENTATION
738
- _send_tracking_request(visitor_code, variation_key, rule)
739
- end
706
+ feature_flag = find_feature_flag(feature_key)
707
+ variation, rule = _calculate_variation_key_for_feature(visitor_code, feature_flag)
708
+ variation_key = _get_variation_key(variation, rule, feature_flag)
709
+ unless rule.nil?
710
+ experiment_id = rule.experiment_id
711
+ variation_id = variation.variation_id unless variation.nil?
712
+ save_variation(visitor_code, experiment_id, variation_id)
713
+ end
714
+ _send_tracking_request(visitor_code, experiment_id, variation_id)
740
715
  [feature_flag, variation_key]
741
716
  end
742
717
 
@@ -744,37 +719,79 @@ module Kameleoon
744
719
  # helper method for calculate variation key for feature flag
745
720
  def _calculate_variation_key_for_feature(visitor_code, feature_flag)
746
721
  # no rules -> return default_variation_key
747
- unless feature_flag.rules.empty?
722
+ feature_flag.rules.each do |rule|
723
+ # check if visitor is targeted for rule, else next rule
724
+ next unless check_targeting(visitor_code, feature_flag.id, rule)
725
+
748
726
  # uses for rule exposition
749
- hash_rule = obtain_hash_double_v2(visitor_code, feature_flag.id)
750
- # uses for variation's expositions
751
- hash_variation = obtain_hash_double_v2(visitor_code, feature_flag.id, 'variation')
752
- feature_flag.rules.each do |rule|
753
- # check if visitor is targeted for rule, else next rule
754
- next unless check_targeting(visitor_code, feature_flag.id, rule)
755
-
756
- # check main expostion for rule with hashRule
757
- if hash_rule < rule.exposition
758
- # get variation key with new hashVariation
759
- variation_key = rule.get_variation_key(hash_variation)
760
- # variation_key can be nil for experiment rules only, for targeted rule will be always exist
761
- return [variation_key, rule] unless variation_key.nil?
762
- # if visitor is targeted for targeted rule then break cycle -> return default
763
- elsif rule.type == Kameleoon::Configuration::RuleType::TARGETED_DELIVERY
764
- break
765
- end
727
+ hash_rule = obtain_hash_double_rule(visitor_code, rule.id, rule.respool_time)
728
+ # check main expostion for rule with hashRule
729
+ if hash_rule <= rule.exposition
730
+ return [rule.variation_by_exposition[0], rule] if rule.targeted_delivery_type?
731
+
732
+ # uses for variation's expositions
733
+ hash_variation = obtain_hash_double_rule(visitor_code, rule.experiment_id, rule.respool_time)
734
+ # get variation key with new hashVariation
735
+ variation = rule.get_variation(hash_variation)
736
+ return [variation, rule] unless variation.nil?
737
+ # if visitor is targeted for targeted rule then break cycle -> return default
738
+ elsif rule.targeted_delivery_type?
739
+ break
766
740
  end
767
741
  end
768
- [feature_flag.default_variation_key, nil]
742
+ [nil, nil]
743
+ end
744
+
745
+ def save_variation(visitor_code, experiment_id, variation_id)
746
+ return if experiment_id.nil? || variation_id.nil?
747
+
748
+ @variation_storage.update_variation(visitor_code, experiment_id, variation_id)
749
+ @hybrid_manager.add_variation(visitor_code, experiment_id, variation_id)
750
+ end
751
+
752
+ def _get_variation_key(var_by_exp, rule, feature_flag)
753
+ return var_by_exp.variation_key unless var_by_exp.nil?
754
+
755
+ return Kameleoon::Configuration::VariationType::VARIATION_OFF if !rule.nil? && rule.experimentation_type?
756
+
757
+ feature_flag.default_variation_key
769
758
  end
770
759
 
771
760
  ##
772
- # helper method for sending tracking requests for new FF v2
773
- def _send_tracking_request(visitor_code, variation_key, rule)
774
- if !rule.experiment_id.nil?
775
- track_experiment(visitor_code, rule.experiment_id, rule.get_variation_id_by_key(variation_key))
761
+ # helper method for sending tracking requests for new FF
762
+ def _send_tracking_request(visitor_code, experiment_id = nil, variation_id = nil)
763
+ data_not_sent = data_not_sent(visitor_code)
764
+ if experiment_id.nil? || variation_id.nil?
765
+ data_not_sent.append(Network::ActivityEvent.new) if data_not_sent.empty?
776
766
  else
777
- log 'An attempt to send a request with null experimentId was blocked'
767
+ data_not_sent.append(Network::ExperimentEvent.new(experiment_id, variation_id))
768
+ end
769
+ options = {
770
+ body: (data_not_sent.map(&:obtain_full_post_text_line).join("\n") || '').encode('UTF-8'),
771
+ head: { 'Content-Type': 'text/plain' }
772
+ }
773
+ set_user_agent_to_headers(visitor_code, options[:head])
774
+ url = @url_provider.make_tracking_url(visitor_code)
775
+ trial = 0
776
+ success = false
777
+ log "Start post tracking: #{data_not_sent.inspect}"
778
+ Thread.new do
779
+ while trial < 10
780
+ log "Send tracking #{options.inspect}"
781
+ response = post_sync(options, url)
782
+ log "Response #{response.inspect}"
783
+ if successful_sync?(response)
784
+ data_not_sent.each { |it| it.sent = true }
785
+ success = true
786
+ break
787
+ end
788
+ trial += 1
789
+ end
790
+ if success
791
+ log "Post to tracking is done after #{trial + 1} trials"
792
+ else
793
+ log "Post to tracking is failed after #{trial} trials"
794
+ end
778
795
  end
779
796
  end
780
797