kameleoon-client-ruby 2.1.1 → 2.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1aef3f7b97d4a32024bec989ed4356cc1d10425c16b419b55f524cf1abfd4f52
4
- data.tar.gz: 5705b067a3effc12b7f38ccef23e01ee28e370b99a212fd371ce0c863184c3ec
3
+ metadata.gz: 7d51a71ea79cd23f422a5235fce6f2e3fb94cae538b01222dd4c23b770985e59
4
+ data.tar.gz: 29f2501a20ffedf1637ee6d441b637358982761955d6235317f49b5284f17b70
5
5
  SHA512:
6
- metadata.gz: 99cc273ca6010c9ce2c451e7a7cb3795e4b264b670fb8009436e5b9ddbaacf388e894f0922926bf92113ebf9f211636f4b4fc2a34fc01babcf848327bba798f8
7
- data.tar.gz: 3686a34890b5438e5d0ca878cbb0bfb89386f2150267c73ef27754d207c6f918aad3355a312045251d3dea15fc2faa6558433f172b907ae4e913c7b60cc204f4
6
+ metadata.gz: 7f4855238a2e8731458757336bc964a36d537e77e7e1b30eb73b5c7e270ee59ee021813b620c0997b9781a8e062b10f3a4797eaceffcd9ca731e5e86f11f1e12
7
+ data.tar.gz: e7d3659d4de54bc0be3aadf6ed96d213bf148ce481306ad8668b589dd892a26a74e78206c6d78dd9577c915e96bab1351c19f08c95ce8b4ae85aaef237b6bd6f
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'kameleoon/targeting/models'
4
- require 'kameleoon/request'
5
4
  require 'kameleoon/exceptions'
6
5
  require 'kameleoon/cookie'
6
+ require 'kameleoon/data/custom_data'
7
7
  require 'kameleoon/data/user_agent'
8
8
  require 'kameleoon/configuration/feature_flag'
9
9
  require 'kameleoon/configuration/variation'
@@ -11,6 +11,7 @@ require 'kameleoon/configuration/settings'
11
11
  require 'kameleoon/network/activity_event'
12
12
  require 'kameleoon/network/experiment_event'
13
13
  require 'kameleoon/network/url_provider'
14
+ require 'kameleoon/network/network_manager'
14
15
  require 'kameleoon/real_time/real_time_configuration_service'
15
16
  require 'kameleoon/storage/variation_storage'
16
17
  require 'kameleoon/hybrid/manager'
@@ -30,30 +31,19 @@ module Kameleoon
30
31
  # Client for Kameleoon
31
32
  #
32
33
  class Client
33
- include Request
34
34
  include Cookie
35
35
  include Exception
36
36
 
37
37
  ##
38
38
  # You should create Client with the Client Factory only.
39
39
  #
40
- def initialize(site_code, path_config_file, interval, default_timeout, client_id = nil, client_secret = nil)
41
- config = YAML.load_file(path_config_file)
40
+ def initialize(site_code, config)
42
41
  @site_code = site_code
43
- @default_timeout = config['default_timeout'] || default_timeout # in ms
44
- refresh_interval = config['actions_configuration_refresh_interval']
45
- @interval = refresh_interval.nil? ? interval : "#{refresh_interval}m"
46
- data_api_url = config['tracking_url'] || Network::UrlProvider::DEFAULT_DATA_API_URL
47
- @url_provider = Network::UrlProvider.new(@site_code, data_api_url)
42
+ read_config(config)
48
43
  @real_time_configuration_service = nil
49
44
  @update_configuration_handler = nil
50
45
  @fetch_configuration_update_job = nil
51
- @client_id = client_id || config['client_id']
52
- @client_secret = client_secret || config['client_secret']
53
- @data_maximum_size = config['visitor_data_maximum_size'] || 500 # mb
54
- @environment = config['environment'] || DEFAULT_ENVIRONMENT
55
46
  @settings = Kameleoon::Configuration::Settings.new
56
- @verbose_mode = config['verbose_mode'] || false
57
47
  @experiments = []
58
48
  @feature_flags = []
59
49
  @data = {}
@@ -65,6 +55,12 @@ module Kameleoon
65
55
  Kameleoon::Storage::CacheFactoryImpl.new,
66
56
  method(:log)
67
57
  )
58
+ @network_manager = Network::NetworkManager.new(
59
+ @environment,
60
+ @default_timeout,
61
+ Network::UrlProvider.new(@site_code, Network::UrlProvider::DEFAULT_DATA_API_URL),
62
+ method(:log)
63
+ )
68
64
  end
69
65
 
70
66
  ##
@@ -268,7 +264,7 @@ module Kameleoon
268
264
  # associated targeting segment conditions were not fulfilled. He should see the reference variation
269
265
  # @raise [Kameleoon::Exception::VisitorCodeNotValid] If the visitor code is empty or longer than 255 chars
270
266
  #
271
- # DEPRECATED. Please use `is_feature_active` instead.
267
+ # DEPRECATED. Please use `feature_active?` instead.
272
268
  def activate_feature(visitor_code, feature_key)
273
269
  warn '[DEPRECATION] `activate_feature` is deprecated. Please use `feature_active?` instead.'
274
270
  feature_active?(visitor_code, feature_key)
@@ -399,13 +395,8 @@ module Kameleoon
399
395
  #
400
396
  # @return [Hash] Hash object of the json object.
401
397
  def get_remote_data(key, timeout = @default_timeout)
402
- connexion_options = { connect_timeout: (timeout.to_f / 1000.0) }
403
- url = @url_provider.make_api_data_get_request_url(key)
404
- log "Retrieve API Data connexion: #{connexion_options.inspect}"
405
- response = get_sync(url, connexion_options)
406
- return nil unless successful_sync?(response)
407
-
408
- JSON.parse(response.body) unless response.nil?
398
+ response = @network_manager.get_remote_data(key, timeout)
399
+ JSON.parse(response) if response
409
400
  end
410
401
 
411
402
  ##
@@ -415,6 +406,26 @@ module Kameleoon
415
406
  get_remote_data(key, timeout)
416
407
  end
417
408
 
409
+ ##
410
+ # The get_remote_visitor_data is a method for retrieving custom data for
411
+ # the latest visit of `visitor_code` from Kameleoon Data API and optionally adding it
412
+ # to the storage so that other methods could decide whether the current visitor is targeted or not.
413
+ #
414
+ # @param [String] visitor_code The visitor code for which you want to retrieve the assigned data.
415
+ # This field is mandatory.
416
+ # @param [Bool] add_data A boolean indicating whether the method should automatically add retrieved data
417
+ # for a visitor. If not specified, the default value is `True`. This field is optional.
418
+ # @param [Int] timeout Timeout for request (in milliseconds). Equals default_timeout in a config file.
419
+ # This field is optional.
420
+ #
421
+ # @return [Array] An array of data assigned to the given visitor.
422
+ def get_remote_visitor_data(visitor_code, timeout = nil, add_data: true)
423
+ response = @network_manager.get_remote_visitor_data(visitor_code, timeout)
424
+ data_array = parse_custom_data_array(visitor_code, response)
425
+ add_data(visitor_code, *data_array) if add_data && !data_array.empty?
426
+ data_array
427
+ end
428
+
418
429
  ##
419
430
  # Returns a list of all experiment ids
420
431
  #
@@ -494,11 +505,23 @@ module Kameleoon
494
505
  private
495
506
 
496
507
  REFERENCE = 0
497
- DEFAULT_ENVIRONMENT = 'production'
498
508
  CACHE_EXPIRATION_TIMEOUT = 5
499
509
  attr :site_code, :client_id, :client_secret, :access_token, :experiments, :feature_flags, :scheduler, :data,
500
510
  :tracking_url, :default_timeout, :interval, :memory_limit, :verbose_mode
501
511
 
512
+ def read_config(config)
513
+ @client_id = config.client_id
514
+ @client_secret = config.client_secret
515
+ @default_timeout = config.default_timeout # in ms
516
+ @interval = "#{config.configuration_refresh_interval}m"
517
+ @data_maximum_size = config.visitor_data_maximum_size
518
+ @environment = config.environment
519
+ @verbose_mode = config.verbose_mode
520
+ if @client_id.nil? || @client_secret.nil?
521
+ warn 'Kameleoon SDK: Credentials are invalid: client_id or client_secret (or both) are empty'
522
+ end
523
+ end
524
+
502
525
  def fetch_configuration
503
526
  Rufus::Scheduler.singleton.in '0s' do
504
527
  log('Initial configuration fetch is started.')
@@ -509,7 +532,7 @@ module Kameleoon
509
532
  def fetch_configuration_job(time_stamp = nil)
510
533
  EM.synchrony do
511
534
  begin
512
- ok = obtain_configuration(@environment, time_stamp)
535
+ ok = obtain_configuration(time_stamp)
513
536
  if !ok && @settings.real_time_update
514
537
  @settings.real_time_update = false
515
538
  log('Switching to polling mode due to failed fetch')
@@ -542,7 +565,7 @@ module Kameleoon
542
565
  def start_real_time_configuration_service_if_needed
543
566
  return unless @real_time_configuration_service.nil?
544
567
 
545
- url = @url_provider.make_real_time_url
568
+ url = @network_manager.url_provider.make_real_time_url
546
569
  fetch_func = proc { |real_time_event| fetch_configuration_job(real_time_event.time_stamp) }
547
570
  @real_time_configuration_service =
548
571
  Kameleoon::RealTime::RealTimeConfigurationService.new(url, fetch_func, method(:log))
@@ -579,13 +602,12 @@ module Kameleoon
579
602
  # { 'field' => field, 'operator' => operator, 'parameters' => parameters }
580
603
  # end
581
604
 
582
- def obtain_configuration(environment = @environment, time_stamp = nil)
605
+ def obtain_configuration(time_stamp = nil)
583
606
  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
607
+ response = @network_manager.fetch_configuration(time_stamp)
608
+ return false unless response
587
609
 
588
- configuration = JSON.parse(request.response)
610
+ configuration = JSON.parse(response)
589
611
  @experiments = Kameleoon::Configuration::Experiment.create_from_array(configuration['experiments']) ||
590
612
  @experiments
591
613
  @feature_flags = Kameleoon::Configuration::FeatureFlag.create_from_array(
@@ -593,7 +615,7 @@ module Kameleoon
593
615
  )
594
616
  @settings.update(configuration['configuration'])
595
617
  call_update_handler_if_needed(!time_stamp.nil?)
596
- log "Feature flags are fetched: #{request.inspect}"
618
+ log "Feature flags are fetched: #{response.inspect}"
597
619
  true
598
620
  end
599
621
 
@@ -616,15 +638,6 @@ module Kameleoon
616
638
  nil
617
639
  end
618
640
 
619
- def request_configuration(url)
620
- request = EM::Synchrony.sync get({}, url)
621
- unless successful?(request)
622
- log "Failed to fetch #{request.inspect}"
623
- return false
624
- end
625
- request
626
- end
627
-
628
641
  def find_feature_flag(feature_key)
629
642
  if feature_key.is_a?(String)
630
643
  feature_flag = @feature_flags.select { |ff| ff.feature_key == feature_key }.first
@@ -760,39 +773,15 @@ module Kameleoon
760
773
  ##
761
774
  # helper method for sending tracking requests for new FF
762
775
  def _send_tracking_request(visitor_code, experiment_id = nil, variation_id = nil)
776
+ user_agent = @user_agents[visitor_code]&.value
763
777
  data_not_sent = data_not_sent(visitor_code)
764
778
  if experiment_id.nil? || variation_id.nil?
765
779
  data_not_sent.append(Network::ActivityEvent.new) if data_not_sent.empty?
766
780
  else
767
781
  data_not_sent.append(Network::ExperimentEvent.new(experiment_id, variation_id))
768
782
  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
783
  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
795
- end
784
+ @network_manager.send_tracking_data(visitor_code, data_not_sent, user_agent)
796
785
  end
797
786
 
798
787
  ##
@@ -808,5 +797,28 @@ module Kameleoon
808
797
  'Unknown type for feature variable'
809
798
  end
810
799
  end
800
+
801
+ ##
802
+ # helper method used by `get_remote_visitor_data`
803
+ def parse_custom_data_array(visitor_code, response)
804
+ return [] unless response
805
+
806
+ raw = JSON.parse(response)
807
+ latest_record = raw['currentVisit']
808
+ if latest_record.nil?
809
+ previous_visits = raw['previousVisits']
810
+ return [] unless previous_visits
811
+
812
+ latest_record = previous_visits.first
813
+ end
814
+ events = latest_record['customDataEvents']
815
+ return [] if events.nil?
816
+
817
+ events.map { |ev| ev['data'] }.compact
818
+ .map { |jcd| CustomData.new(jcd['index'] || -1, *(jcd['valuesCountMap'] || {}).keys) }
819
+ rescue StandardError => e
820
+ log("Parsing of visitor data of '#{visitor_code}' failed: #{e}")
821
+ []
822
+ end
811
823
  end
812
824
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ # Client configuration which can be used instead of an external configuration file
5
+ class ClientConfig
6
+ CONFIGURATION_UPDATE_INTERVAL = 60
7
+ VISITOR_DATA_MAXIMUM_SIZE = 500
8
+ DEFAULT_TIMEOUT = 2000 # milli-seconds
9
+
10
+ attr_reader :client_id, :client_secret, :data_refresh_interval, :default_timeout, :configuration_refresh_interval,
11
+ :visitor_data_maximum_size, :environment, :verbose_mode
12
+
13
+ def initialize(
14
+ client_id: nil,
15
+ client_secret: nil,
16
+ configuration_refresh_interval: CONFIGURATION_UPDATE_INTERVAL,
17
+ default_timeout: DEFAULT_TIMEOUT,
18
+ visitor_data_maximum_size: VISITOR_DATA_MAXIMUM_SIZE,
19
+ environment: nil,
20
+ verbose_mode: false
21
+ )
22
+ @client_id = client_id
23
+ @client_secret = client_secret
24
+ @configuration_refresh_interval = configuration_refresh_interval || CONFIGURATION_UPDATE_INTERVAL
25
+ @default_timeout = default_timeout || DEFAULT_TIMEOUT
26
+ @visitor_data_maximum_size = visitor_data_maximum_size || VISITOR_DATA_MAXIMUM_SIZE
27
+ @environment = environment
28
+ @verbose_mode = verbose_mode || false
29
+ end
30
+
31
+ def self.make_from_yaml(yaml)
32
+ yaml ||= {}
33
+ ClientConfig.new(
34
+ client_id: yaml['client_id'],
35
+ client_secret: yaml['client_secret'],
36
+ configuration_refresh_interval: yaml['actions_configuration_refresh_interval'],
37
+ default_timeout: yaml['default_timeout'],
38
+ visitor_data_maximum_size: yaml['visitor_data_maximum_size'],
39
+ environment: yaml['environment'],
40
+ verbose_mode: yaml['verbose_mode']
41
+ )
42
+ end
43
+ end
44
+ end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
4
+ require 'kameleoon/network/uri_helper'
2
5
  require_relative 'data'
3
6
 
4
7
  module Kameleoon
@@ -24,10 +27,13 @@ module Kameleoon
24
27
  end
25
28
 
26
29
  def obtain_full_post_text_line
27
- nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
28
- url = "eventType=staticData&browserIndex=#{@type}&nonce=#{nonce}"
29
- url.concat("&browserVersion=#{@version}") if @version.is_a?(Integer) || (@version.is_a?(Float) && !@version.nan?)
30
- url
30
+ params = {
31
+ eventType: 'staticData',
32
+ browserIndex: @type,
33
+ nonce: nonce
34
+ }
35
+ params[:browserVersion] = @version if @version.is_a?(Integer) || (@version.is_a?(Float) && !@version.nan?)
36
+ Kameleoon::Network::UriHelper.encode_query(params)
31
37
  end
32
38
  end
33
39
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'kameleoon/network/uri_helper'
4
5
  require_relative 'data'
5
6
 
6
7
  module Kameleoon
@@ -19,8 +20,14 @@ module Kameleoon
19
20
  end
20
21
 
21
22
  def obtain_full_post_text_line
22
- nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
23
- "eventType=conversion&goalId=#{@goal_id}&revenue=#{@revenue}&negative=#{@negative}&nonce=#{nonce}"
23
+ params = {
24
+ eventType: 'conversion',
25
+ goalId: @goal_id,
26
+ revenue: @revenue,
27
+ negative: @negative,
28
+ nonce: nonce
29
+ }
30
+ Kameleoon::Network::UriHelper.encode_query(params)
24
31
  end
25
32
  end
26
33
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'kameleoon/network/uri_helper'
4
5
  require_relative 'data'
5
6
 
6
7
  module Kameleoon
@@ -46,8 +47,14 @@ module Kameleoon
46
47
  return '' if @values.empty?
47
48
 
48
49
  str_values = JSON.generate(Hash[@values.collect { |k| [k, 1] }])
49
- nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
50
- "eventType=customData&index=#{@id}&valuesCountMap=#{self.class.encode(str_values)}&overwrite=true&nonce=#{nonce}"
50
+ params = {
51
+ eventType: 'customData',
52
+ index: @id,
53
+ valuesCountMap: str_values,
54
+ overwrite: 'true',
55
+ nonce: nonce
56
+ }
57
+ Kameleoon::Network::UriHelper.encode_query(params)
51
58
  end
52
59
  end
53
60
  end
@@ -28,8 +28,11 @@ module Kameleoon
28
28
  raise KameleoonError.new('ToDo: implement this method.'), 'ToDo: implement this method.'
29
29
  end
30
30
 
31
- def self.encode(url)
32
- Network::UriHelper.encode_uri(url)
31
+ private
32
+
33
+ def nonce
34
+ @nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH) if @nonce.nil?
35
+ @nonce
33
36
  end
34
37
  end
35
38
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'kameleoon/network/uri_helper'
4
5
  require_relative 'data'
5
6
 
6
7
  module Kameleoon
@@ -19,8 +20,12 @@ module Kameleoon
19
20
  end
20
21
 
21
22
  def obtain_full_post_text_line
22
- nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
23
- "eventType=staticData&deviceType=#{@device_type}&nonce=#{nonce}"
23
+ params = {
24
+ eventType: 'staticData',
25
+ deviceType: @device_type,
26
+ nonce: nonce
27
+ }
28
+ Kameleoon::Network::UriHelper.encode_query(params)
24
29
  end
25
30
  end
26
31
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'kameleoon/network/uri_helper'
4
5
  require_relative 'data'
5
6
 
6
7
  module Kameleoon
@@ -19,13 +20,15 @@ module Kameleoon
19
20
  end
20
21
 
21
22
  def obtain_full_post_text_line
22
- nonce = Kameleoon::Utils.generate_random_string(NONCE_LENGTH)
23
- referrer_text = if !@referrers.nil? && !@referrers.empty?
24
- "&referrersIndices=%5B#{@referrers.each(&:to_s).join('%2C')}%5D"
25
- else
26
- ''
27
- end
28
- "eventType=page&href=#{self.class.encode(@url)}&title=#{self.class.encode(@title)}#{referrer_text}&nonce=#{nonce}"
23
+ params = {
24
+ eventType: 'page',
25
+ href: @url,
26
+ title: @title,
27
+ nonce: nonce
28
+ }
29
+ params[:referrersIndices] = "[#{@referrers.each(&:to_s).join(',')}]" if
30
+ !@referrers.nil? && !@referrers.empty?
31
+ Kameleoon::Network::UriHelper.encode_query(params)
29
32
  end
30
33
  end
31
34
  end
@@ -1,29 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'kameleoon/client'
4
+ require 'kameleoon/client_config'
4
5
 
5
6
  module Kameleoon
6
7
  # A Factory class for creating kameleoon clients
7
8
  module ClientFactory
8
- CONFIGURATION_UPDATE_INTERVAL = '60m'
9
9
  CONFIG_PATH = '/etc/kameleoon/client-ruby.yaml'
10
- DEFAULT_TIMEOUT = 2000 # milli-seconds
11
10
 
12
11
  @clients = {}
13
12
 
14
- def self.create(site_code, config_path = CONFIG_PATH, client_id = nil, client_secret = nil)
15
- if @clients[site_code].nil?
16
- client = Client.new(site_code, config_path, CONFIGURATION_UPDATE_INTERVAL,
17
- DEFAULT_TIMEOUT, client_id, client_secret)
13
+ def self.create(site_code, config_path = CONFIG_PATH, config: nil)
14
+ if config.nil?
15
+ config_yaml = YAML.load_file(config_path) if File.exist?(config_path)
16
+ if config_yaml.nil?
17
+ warn "Kameleoon SDK: Configuration file with path #{config_path} does not exist" if config_yaml.nil?
18
+ config_yaml = {}
19
+ end
20
+ end
21
+ environment = config&.environment || (config_yaml || {})['environment']
22
+ client = @clients[get_client_key(site_code, environment)]
23
+ if client.nil?
24
+ config = ClientConfig.make_from_yaml(config_yaml) if config.nil?
25
+ client = Client.new(site_code, config)
18
26
  client.send(:log, "Client created with site code: #{site_code}")
19
27
  client.send(:fetch_configuration)
20
28
  @clients.store(site_code, client)
21
29
  end
22
- @clients[site_code]
30
+ client
31
+ end
32
+
33
+ def self.forget(site_code, environment = '')
34
+ @clients.delete(get_client_key(site_code, environment))
23
35
  end
24
36
 
25
- def self.forget(site_code)
26
- @clients.delete(site_code)
37
+ private_class_method
38
+
39
+ def self.get_client_key(site_code, environment)
40
+ site_code + (environment || '')
27
41
  end
28
42
  end
29
43
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Network
5
+ module ContentType
6
+ TEXT = 'text/plain'
7
+ JSON = 'application/json'
8
+ FORM = 'application/x-www-form-urlencoded'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Network
5
+ module Method
6
+ GET = 'GET'
7
+ POST = 'POST'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'em-synchrony/em-http'
4
+ require 'net/http'
5
+ require 'kameleoon/version'
6
+ require 'kameleoon/network/response'
7
+ require 'kameleoon/exceptions'
8
+
9
+ module Kameleoon
10
+ module Network
11
+ class NetProvider
12
+ def make_request(_request)
13
+ raise KameleoonError, 'Call of not implemented method!'
14
+ end
15
+
16
+ private
17
+
18
+ def collect_headers(request)
19
+ headers = { 'Content-Type': request.content_type }
20
+ headers['User-Agent'] = request.user_agent unless request.user_agent.nil?
21
+ headers
22
+ end
23
+
24
+ def unknown_method_response(method, request)
25
+ Response.new("Unknown request method '#{method}'", nil, nil, request)
26
+ end
27
+ end
28
+
29
+ class EMNetProvider < NetProvider
30
+ def make_request(request)
31
+ connetion_options = {
32
+ tls: { verify_peer: false },
33
+ connect_timeout: request.timeout,
34
+ inactivity_timeout: request.timeout
35
+ }
36
+ headers = collect_headers(request)
37
+ request_options = { head: headers, body: request.data }
38
+ begin
39
+ case request.method
40
+ when Method::POST
41
+ EventMachine::HttpRequest.new(request.url, connetion_options).apost(request_options)
42
+ when Method::GET
43
+ EventMachine::HttpRequest.new(request.url, connetion_options).aget(request_options)
44
+ else
45
+ dfr = DeferrableResponse.new
46
+ dfr.response = unknown_method_response(request.method, request)
47
+ dfr
48
+ end
49
+ rescue => e
50
+ dfr = DeferrableResponse.new
51
+ dfr.response = Response.new(e, nil, nil, request)
52
+ dfr
53
+ end
54
+ end
55
+
56
+ def self.em_resp_to_response(request, resp)
57
+ return resp if resp.is_a?(Response)
58
+
59
+ Response.new(nil, resp.response_header.status, resp.response, request)
60
+ end
61
+ end
62
+
63
+ class SyncNetProvider < NetProvider
64
+ def make_request(request)
65
+ resp = nil
66
+ begin
67
+ case request.method
68
+ when Method::GET
69
+ req = Net::HTTP::Get.new(request.url)
70
+ when Method::POST
71
+ req = Net::HTTP::Post.new(request.url)
72
+ req.body = request.data
73
+ else
74
+ return unknown_method_response(request.method, request)
75
+ end
76
+ timeout = request.timeout.to_f / 1000.0
77
+ headers = collect_headers(request)
78
+ headers.each { |k, v| req[k] = v }
79
+ uri = URI(request.url)
80
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: timeout,
81
+ read_timeout: timeout, ssl_timeout: timeout) do |http|
82
+ resp = http.request(req)
83
+ end
84
+ rescue => e
85
+ return Response.new(e, nil, nil, request)
86
+ end
87
+ Response.new(nil, resp.code.to_i, resp.body, request)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'em-synchrony'
4
+ require 'kameleoon/network/content_type'
5
+ require 'kameleoon/network/method'
6
+ require 'kameleoon/network/request'
7
+ require 'kameleoon/network/net_provider'
8
+
9
+ module Kameleoon
10
+ module Network
11
+ ##
12
+ # NetworkManager is used to make API calls.
13
+ class NetworkManager
14
+ FETCH_CONFIGURATION_ATTEMPT_NUMBER = 4
15
+ TRACKING_CALL_ATTEMPT_NUMBER = 4
16
+ TRACKING_CALL_RETRY_DELAY = 5.0 # in seconds
17
+
18
+ attr_reader :environment, :default_timeout, :url_provider
19
+
20
+ def initialize(environment, default_timeout, url_provider, log_func = nil)
21
+ @environment = environment
22
+ @default_timeout = default_timeout
23
+ @url_provider = url_provider
24
+ @em_net_provider = EMNetProvider.new
25
+ @sync_net_provider = SyncNetProvider.new
26
+ @log_func = log_func
27
+ end
28
+
29
+ def fetch_configuration(timestamp = nil, timeout = nil)
30
+ url = @url_provider.make_configuration_url(@environment, timestamp)
31
+ timeout = ensure_timeout(timeout)
32
+ request = Request.new(Method::GET, url, ContentType::JSON, timeout)
33
+ attempts_left = FETCH_CONFIGURATION_ATTEMPT_NUMBER
34
+ while attempts_left.positive?
35
+ em_resp = EM::Synchrony.sync(@em_net_provider.make_request(request))
36
+ response = EMNetProvider.em_resp_to_response(request, em_resp)
37
+ result = handle_response(response)
38
+ return result if result
39
+
40
+ attempts_left -= 1
41
+ end
42
+ false
43
+ end
44
+
45
+ def get_remote_data(key, timeout = nil)
46
+ url = @url_provider.make_api_data_get_request_url(key)
47
+ timeout = ensure_timeout(timeout)
48
+ request = Request.new(Method::GET, url, ContentType::JSON, timeout)
49
+ response = @sync_net_provider.make_request(request)
50
+ handle_response(response)
51
+ end
52
+
53
+ def get_remote_visitor_data(visitor_code, timeout = nil)
54
+ url = @url_provider.make_visitor_data_get_url(visitor_code)
55
+ timeout = ensure_timeout(timeout)
56
+ request = Request.new(Method::GET, url, ContentType::JSON, timeout)
57
+ response = @sync_net_provider.make_request(request)
58
+ handle_response(response)
59
+ end
60
+
61
+ def send_tracking_data(visitor_code, lines, user_agent, timeout = nil)
62
+ return if lines.nil? || lines.empty?
63
+
64
+ url = @url_provider.make_tracking_url(visitor_code)
65
+ timeout = ensure_timeout(timeout)
66
+ data = (lines.map(&:obtain_full_post_text_line).join("\n") || '').encode('UTF-8')
67
+ request = Request.new(Method::POST, url, ContentType::TEXT, timeout, user_agent, data)
68
+ Thread.new do
69
+ attempts_left = TRACKING_CALL_ATTEMPT_NUMBER
70
+ loop do
71
+ response = @sync_net_provider.make_request(request)
72
+ if handle_response(response) != false
73
+ lines.each { |line| line.sent = true }
74
+ break
75
+ end
76
+ attempts_left -= 1
77
+ break unless attempts_left.positive?
78
+
79
+ delay(TRACKING_CALL_RETRY_DELAY)
80
+ end
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def log_failure(request, message)
87
+ return if @log_func.nil?
88
+
89
+ premsg = "#{request.method} call '#{request.url}' "
90
+ premsg += request.data.nil? ? 'failed' : "(data '#{request.data}') failed"
91
+ @log_func.call("#{premsg}: #{message}")
92
+ end
93
+
94
+ def delay(period)
95
+ sleep(period)
96
+ end
97
+
98
+ def ensure_timeout(timeout)
99
+ timeout.nil? ? @default_timeout : timeout
100
+ end
101
+
102
+ def handle_response(response)
103
+ if !response.error.nil?
104
+ log_failure(response.request, "Error occurred during request: #{response.error}")
105
+ elsif response.code / 100 != 2
106
+ log_failure(response.request, "Received unexpected status code '#{response.code}'")
107
+ else
108
+ return response.body
109
+ end
110
+ false
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Network
5
+ ##
6
+ # Request represents HTTP request.
7
+ class Request
8
+ attr_reader :method, :url, :content_type, :timeout, :user_agent, :data
9
+
10
+ def initialize(method, url, content_type, timeout, user_agent = nil, data = nil)
11
+ @method = method
12
+ @url = url
13
+ @content_type = content_type
14
+ @timeout = timeout
15
+ @user_agent = user_agent
16
+ @data = !data.nil? && data.is_a?(String) ? data.encode('UTF-8') : data
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ module Network
5
+ ##
6
+ # Response represents HTTP response.
7
+ class Response
8
+ attr_reader :error, :code, :body, :request
9
+
10
+ def initialize(error, code, body, request)
11
+ @error = error
12
+ @code = code
13
+ @body = body
14
+ @request = request
15
+ end
16
+
17
+ def success?
18
+ @error.nil? && (@code / 100 == 2)
19
+ end
20
+ end
21
+
22
+ class DeferrableResponse < EventMachine::DefaultDeferrable
23
+ attr_writer :response
24
+
25
+ def callback(&blk)
26
+ blk.call(@response)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -21,6 +21,7 @@ module Kameleoon
21
21
  end
22
22
 
23
23
  def self.encode_query(params)
24
+ params.delete_if { |_k, v| v.nil? || (v == '') }
24
25
  encoded = URI.encode_www_form(params)
25
26
  encoded.gsub!(/%21|%27|%28|%29|%7E|\+/,
26
27
  '%21' => '!',
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Kameleoon
4
4
  SDK_NAME = 'RUBY'
5
- SDK_VERSION = '2.1.1'
5
+ SDK_VERSION = '2.3.0'
6
6
 
7
7
  # SdkManager is a helper method for fetching / obtaining version of SDK from string
8
8
  class SdkVersion
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kameleoon-client-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kameleoon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-28 00:00:00.000000000 Z
11
+ date: 2023-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: em-http-request
@@ -76,6 +76,7 @@ files:
76
76
  - README.md
77
77
  - lib/kameleoon.rb
78
78
  - lib/kameleoon/client.rb
79
+ - lib/kameleoon/client_config.rb
79
80
  - lib/kameleoon/configuration/experiment.rb
80
81
  - lib/kameleoon/configuration/feature_flag.rb
81
82
  - lib/kameleoon/configuration/rule.rb
@@ -95,7 +96,13 @@ files:
95
96
  - lib/kameleoon/factory.rb
96
97
  - lib/kameleoon/hybrid/manager.rb
97
98
  - lib/kameleoon/network/activity_event.rb
99
+ - lib/kameleoon/network/content_type.rb
98
100
  - lib/kameleoon/network/experiment_event.rb
101
+ - lib/kameleoon/network/method.rb
102
+ - lib/kameleoon/network/net_provider.rb
103
+ - lib/kameleoon/network/network_manager.rb
104
+ - lib/kameleoon/network/request.rb
105
+ - lib/kameleoon/network/response.rb
99
106
  - lib/kameleoon/network/uri_helper.rb
100
107
  - lib/kameleoon/network/url_provider.rb
101
108
  - lib/kameleoon/real_time/real_time_configuration_service.rb
@@ -103,7 +110,6 @@ files:
103
110
  - lib/kameleoon/real_time/sse_client.rb
104
111
  - lib/kameleoon/real_time/sse_message.rb
105
112
  - lib/kameleoon/real_time/sse_request.rb
106
- - lib/kameleoon/request.rb
107
113
  - lib/kameleoon/storage/cache.rb
108
114
  - lib/kameleoon/storage/cache_factory.rb
109
115
  - lib/kameleoon/storage/variation_storage.rb
@@ -1,76 +0,0 @@
1
- require 'em-synchrony/em-http'
2
- require 'kameleoon/version'
3
- require 'net/http'
4
-
5
- module Kameleoon
6
- # @api private
7
- module Request
8
- protected
9
-
10
- module Method
11
- GET = 'get'.freeze
12
- POST = 'post'.freeze
13
- end
14
-
15
- def get(request_options, url, connexion_options = {})
16
- request(Method::GET, request_options, url, connexion_options)
17
- end
18
-
19
- def post(request_options, url, connexion_options = {})
20
- request(Method::POST, request_options, url, connexion_options)
21
- end
22
-
23
- def get_sync(url, connexion_options = {})
24
- request_sync(Method::GET, url, connexion_options, {})
25
- end
26
-
27
- def post_sync(request_options, url, connexion_options = {})
28
- request_sync(Method::POST, url, connexion_options, request_options)
29
- end
30
-
31
- private
32
-
33
- def request(method, request_options, url, connexion_options)
34
- connexion_options[:tls] = { verify_peer: false }
35
- case method
36
- when Method::POST then
37
- return EventMachine::HttpRequest.new(url, connexion_options).apost request_options
38
- when Method::GET then
39
- return EventMachine::HttpRequest.new(url, connexion_options).get request_options
40
- else
41
- print 'Unknown request type'
42
- return false
43
- end
44
- end
45
-
46
- def request_sync(method, url, connexion_options, request_options)
47
- request_options = {} if request_options.nil?
48
- case method
49
- when Method::GET then
50
- uri = URI(url)
51
- request = Net::HTTP.new(url)
52
- response = Net::HTTP.get_response(uri)
53
- response
54
- when Method::POST then
55
- uri = URI.parse(url)
56
- http = Net::HTTP.new(uri.host, uri.port)
57
- http.use_ssl = true
58
- request = Net::HTTP::Post.new(uri, initheader = request_options[:head])
59
- request.body = request_options[:body]
60
- response = http.request(request)
61
- response
62
- else
63
- print "Unknown request type"
64
- return false
65
- end
66
- end
67
-
68
- def successful?(request)
69
- !request.nil? && request != false && /20\d/.match(request.response_header.status.to_s)
70
- end
71
-
72
- def successful_sync?(response)
73
- !response.nil? && response != false && response.is_a?(Net::HTTPSuccess)
74
- end
75
- end
76
- end