kameleoon-client-ruby 2.3.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kameleoon/client_readiness.rb +40 -0
  3. data/lib/kameleoon/configuration/data_file.rb +41 -0
  4. data/lib/kameleoon/configuration/feature_flag.rb +3 -2
  5. data/lib/kameleoon/configuration/rule.rb +18 -3
  6. data/lib/kameleoon/configuration/settings.rb +5 -0
  7. data/lib/kameleoon/configuration/variable.rb +0 -2
  8. data/lib/kameleoon/configuration/variation_exposition.rb +0 -2
  9. data/lib/kameleoon/data/browser.rb +1 -1
  10. data/lib/kameleoon/data/conversion.rb +1 -1
  11. data/lib/kameleoon/data/custom_data.rb +10 -14
  12. data/lib/kameleoon/data/data.rb +22 -3
  13. data/lib/kameleoon/data/device.rb +2 -1
  14. data/lib/kameleoon/data/manager/assigned_variation.rb +38 -0
  15. data/lib/kameleoon/data/manager/data_array_storage.rb +43 -0
  16. data/lib/kameleoon/data/manager/data_map_storage.rb +43 -0
  17. data/lib/kameleoon/data/manager/page_view_visit.rb +19 -0
  18. data/lib/kameleoon/data/manager/visitor.rb +142 -0
  19. data/lib/kameleoon/data/manager/visitor_manager.rb +71 -0
  20. data/lib/kameleoon/data/page_view.rb +2 -1
  21. data/lib/kameleoon/exceptions.rb +30 -35
  22. data/lib/kameleoon/hybrid/manager.rb +13 -31
  23. data/lib/kameleoon/{client.rb → kameleoon_client.rb} +194 -334
  24. data/lib/kameleoon/kameleoon_client_config.rb +91 -0
  25. data/lib/kameleoon/kameleoon_client_factory.rb +42 -0
  26. data/lib/kameleoon/managers/warehouse/warehouse_manager.rb +33 -0
  27. data/lib/kameleoon/network/access_token_source.rb +109 -0
  28. data/lib/kameleoon/network/activity_event.rb +6 -3
  29. data/lib/kameleoon/network/cookie/cookie_manager.rb +84 -0
  30. data/lib/kameleoon/network/net_provider.rb +25 -58
  31. data/lib/kameleoon/network/network_manager.rb +65 -43
  32. data/lib/kameleoon/network/request.rb +7 -2
  33. data/lib/kameleoon/network/response.rb +0 -8
  34. data/lib/kameleoon/network/url_provider.rb +30 -12
  35. data/lib/kameleoon/real_time/sse_client.rb +2 -0
  36. data/lib/kameleoon/targeting/conditions/browser_condition.rb +2 -3
  37. data/lib/kameleoon/targeting/conditions/conversion_condition.rb +12 -4
  38. data/lib/kameleoon/targeting/conditions/custom_datum.rb +19 -13
  39. data/lib/kameleoon/targeting/conditions/device_condition.rb +3 -4
  40. data/lib/kameleoon/targeting/conditions/exclusive_experiment.rb +2 -1
  41. data/lib/kameleoon/targeting/conditions/page_title_condition.rb +11 -4
  42. data/lib/kameleoon/targeting/conditions/page_url_condition.rb +18 -4
  43. data/lib/kameleoon/targeting/conditions/string_value_condition.rb +2 -0
  44. data/lib/kameleoon/targeting/conditions/target_experiment.rb +11 -6
  45. data/lib/kameleoon/utils.rb +41 -4
  46. data/lib/kameleoon/version.rb +1 -1
  47. data/lib/kameleoon.rb +4 -2
  48. metadata +16 -10
  49. data/lib/kameleoon/client_config.rb +0 -44
  50. data/lib/kameleoon/configuration/experiment.rb +0 -42
  51. data/lib/kameleoon/cookie.rb +0 -88
  52. data/lib/kameleoon/factory.rb +0 -43
  53. data/lib/kameleoon/network/experiment_event.rb +0 -35
  54. data/lib/kameleoon/storage/variation_storage.rb +0 -42
  55. data/lib/kameleoon/storage/visitor_variation.rb +0 -20
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kameleoon
4
+ # KameleoonClient configuration which can be used instead of an external configuration file
5
+ class KameleoonClientConfig
6
+ DEFAULT_REFRESH_INTERVAL_MINUTES = 60
7
+ DEFAULT_SESSION_DURATION_MINUTES = 30
8
+ DEFAULT_TIMEOUT_MILLISECONDS = 10_000
9
+
10
+ attr_reader :client_id, :client_secret, :refresh_interval_second, :session_duration_second,
11
+ :default_timeout_millisecond, :environment, :top_level_domain, :verbose_mode
12
+
13
+ def initialize(
14
+ client_id,
15
+ client_secret,
16
+ refresh_interval_minute: DEFAULT_REFRESH_INTERVAL_MINUTES,
17
+ session_duration_minute: DEFAULT_SESSION_DURATION_MINUTES,
18
+ default_timeout_millisecond: DEFAULT_TIMEOUT_MILLISECONDS,
19
+ environment: nil,
20
+ top_level_domain: nil,
21
+ verbose_mode: false
22
+ )
23
+ raise Exception::ConfigCredentialsInvalid, 'Client ID is not specified' if client_id&.empty? != false
24
+ raise Exception::ConfigCredentialsInvalid, 'Client secret is not specified' if client_secret&.empty? != false
25
+
26
+ @verbose_mode = verbose_mode || false
27
+
28
+ @client_id = client_id
29
+ @client_secret = client_secret
30
+
31
+ if refresh_interval_minute.nil?
32
+ refresh_interval_minute = DEFAULT_REFRESH_INTERVAL_MINUTES
33
+ elsif refresh_interval_minute <= 0
34
+ log('Configuration refresh interval must have positive value. ' \
35
+ "Default refresh interval (#{DEFAULT_REFRESH_INTERVAL_MINUTES} minutes) is applied.")
36
+ refresh_interval_minute = DEFAULT_REFRESH_INTERVAL_MINUTES
37
+ end
38
+ @refresh_interval_second = refresh_interval_minute * 60
39
+
40
+ if session_duration_minute.nil?
41
+ session_duration_minute = DEFAULT_SESSION_DURATION_MINUTES
42
+ elsif session_duration_minute <= 0
43
+ log('Session duration must have positive value. ' \
44
+ "Default session duration (#{DEFAULT_SESSION_DURATION_MINUTES} minutes) is applied.")
45
+ session_duration_minute = DEFAULT_SESSION_DURATION_MINUTES
46
+ end
47
+ @session_duration_second = session_duration_minute * 60
48
+
49
+ if default_timeout_millisecond.nil?
50
+ @default_timeout_millisecond = DEFAULT_TIMEOUT_MILLISECONDS
51
+ elsif default_timeout_millisecond <= 0
52
+ log('Default timeout must have positive value. ' \
53
+ "Default timeout (#{DEFAULT_TIMEOUT_MILLISECONDS} ms) is applied.")
54
+ @default_timeout_millisecond = DEFAULT_TIMEOUT_MILLISECONDS
55
+ else
56
+ @default_timeout_millisecond = default_timeout_millisecond
57
+ end
58
+
59
+ @environment = environment
60
+
61
+ if top_level_domain.nil?
62
+ log('Setting top level domain is strictly recommended, otherwise you may have problems when using subdomains.')
63
+ end
64
+ @top_level_domain = top_level_domain
65
+ end
66
+
67
+ def self.read_from_yaml(path)
68
+ yaml = YAML.load_file(path) if File.exist?(path)
69
+ if yaml.nil?
70
+ warn "Kameleoon SDK: Configuration file with path #{path} does not exist"
71
+ yaml = {}
72
+ end
73
+ KameleoonClientConfig.new(
74
+ yaml['client_id'],
75
+ yaml['client_secret'],
76
+ refresh_interval_minute: yaml['refresh_interval_minute'],
77
+ session_duration_minute: yaml['session_duration_minute'],
78
+ default_timeout_millisecond: yaml['default_timeout_millisecond'],
79
+ environment: yaml['environment'],
80
+ top_level_domain: yaml['top_level_domain'],
81
+ verbose_mode: yaml['verbose_mode']
82
+ )
83
+ end
84
+
85
+ private
86
+
87
+ def log(text)
88
+ print "Kameleoon SDK Log: #{text}\n" if @verbose_mode
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+ require 'kameleoon/kameleoon_client'
5
+ require 'kameleoon/kameleoon_client_config'
6
+
7
+ module Kameleoon
8
+ # A Factory class for creating kameleoon clients
9
+ module KameleoonClientFactory
10
+ CONFIG_PATH = '/etc/kameleoon/client-ruby.yaml'
11
+
12
+ @clients = Concurrent::Map.new
13
+
14
+ def self.create(site_code, config: nil, config_path: CONFIG_PATH)
15
+ unless config.is_a?(KameleoonClientConfig)
16
+ config_path = CONFIG_PATH unless config_path.is_a?(String)
17
+ config = KameleoonClientConfig.read_from_yaml(config_path)
18
+ end
19
+ key = get_client_key(site_code, config.environment)
20
+ @clients.compute_if_absent(key) do
21
+ client = KameleoonClient.new(site_code, config)
22
+ client.send(:log, "Client created with site code: #{site_code}")
23
+ client.send(:fetch_configuration_initially)
24
+ client
25
+ end
26
+ end
27
+
28
+ def self.forget(site_code, environment = nil)
29
+ key = get_client_key(site_code, environment)
30
+ @clients.compute_if_present(key) do |client|
31
+ client.send(:dispose)
32
+ nil
33
+ end
34
+ end
35
+
36
+ private_class_method
37
+
38
+ def self.get_client_key(site_code, environment)
39
+ environment.nil? ? site_code : "#{site_code}/#{environment}"
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/data/custom_data'
4
+ require 'kameleoon/utils'
5
+
6
+ module Kameleoon
7
+ module Managers
8
+ module Warehouse
9
+ class WarehouseManager
10
+ WAREHOUSE_AUDIENCES_FIELD_NAME = 'warehouseAudiences'
11
+
12
+ def initialize(network_manager, visitor_manager, log_func = nil)
13
+ @network_manager = network_manager
14
+ @visitor_manager = visitor_manager
15
+ @log_func = log_func
16
+ end
17
+
18
+ def get_visitor_warehouse_audience(visitor_code, custom_data_index, warehouse_key = nil, timeout = nil)
19
+ Utils::VisitorCode.validate(visitor_code)
20
+ remote_data_key = warehouse_key.nil? || warehouse_key.empty? ? visitor_code : warehouse_key
21
+ response = @network_manager.get_remote_data(remote_data_key, timeout)
22
+ remote_data = response.is_a?(String) ? JSON.parse(response) : nil
23
+ warehouse_audiences = remote_data.is_a?(Hash) ? remote_data[WAREHOUSE_AUDIENCES_FIELD_NAME] : nil
24
+ data_values = warehouse_audiences.is_a?(Hash) ? warehouse_audiences.keys : []
25
+ warehouse_audiences_data = CustomData.new(custom_data_index, *data_values)
26
+ visitor = @visitor_manager.get_or_create_visitor(visitor_code)
27
+ visitor.add_data(@log_func, warehouse_audiences_data)
28
+ warehouse_audiences_data
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'time'
6
+
7
+ module Kameleoon
8
+ module Network
9
+ class AccessTokenSource
10
+ TOKEN_EXPIRATION_GAP = 60 # in seconds
11
+ TOKEN_OBSOLESCENCE_GAP = 1800 # in seconds
12
+ JWT_ACCESS_TOKEN_FIELD = 'access_token'
13
+ JWT_EXPIRES_IN_FIELD = 'expires_in'
14
+
15
+ def initialize(network_manager, client_id, client_secret, log_func)
16
+ @network_manager = network_manager
17
+ @client_id = client_id
18
+ @client_secret = client_secret
19
+ @fetching = false
20
+ @log_func = log_func
21
+ end
22
+
23
+ def get_token
24
+ now = Time.new.to_i
25
+ token = @cached_token
26
+ return fetch_token if token.nil? || token.expired?(now)
27
+
28
+ Thread.new { fetch_token } if !@fetching && token.obsolete?(now)
29
+ token.value
30
+ end
31
+
32
+ def discard_token(token)
33
+ @cached_token = nil if @cached_token&.value == token
34
+ end
35
+
36
+ private
37
+
38
+ def fetch_token
39
+ @fetching = true
40
+ response_content = @network_manager.fetch_access_jwtoken(@client_id, @client_secret)
41
+ unless response_content
42
+ @log_func&.call('Failed to fetch access JWT')
43
+ return nil
44
+ end
45
+ begin
46
+ jwt = JSON.parse(response_content)
47
+ token = jwt[JWT_ACCESS_TOKEN_FIELD]
48
+ expires_in = jwt[JWT_EXPIRES_IN_FIELD]
49
+ rescue JSON::ParserError => e
50
+ @log_func&.call("Failed to parse access JWT: #{e}")
51
+ return nil
52
+ end
53
+ unless token.is_a?(String) && !token.empty? && expires_in.is_a?(Integer) && expires_in.positive?
54
+ @log_func&.call('Failed to read access JWT')
55
+ return nil
56
+ end
57
+ handle_fetched_token(token, expires_in)
58
+ ensure
59
+ @fetching = false
60
+ end
61
+
62
+ def handle_fetched_token(token, expires_in)
63
+ now = Time.new.to_i
64
+ exp_time = now + expires_in - TOKEN_EXPIRATION_GAP
65
+ if expires_in > TOKEN_OBSOLESCENCE_GAP
66
+ obs_time = now + expires_in - TOKEN_OBSOLESCENCE_GAP
67
+ else
68
+ obs_time = exp_time
69
+ unless @log_func.nil?
70
+ issue = expires_in <= TOKEN_EXPIRATION_GAP ? 'cache the token' : 'refresh cached token in background'
71
+ @log_func.call("Access token life time (#{expires_in}s) is not long enough to #{issue}")
72
+ end
73
+ end
74
+ @cached_token = ExpiringToken.new(token, exp_time, obs_time)
75
+ token
76
+ end
77
+ end
78
+
79
+ class ExpiringToken
80
+ attr_reader :value, :expiration_time, :obsolescence_time
81
+
82
+ def initialize(value, expiration_time, obsolescence_time)
83
+ @value = value
84
+ @expiration_time = expiration_time
85
+ @obsolescence_time = obsolescence_time
86
+ end
87
+
88
+ def expired?(now)
89
+ now >= @expiration_time
90
+ end
91
+
92
+ def obsolete?(now)
93
+ now >= @obsolescence_time
94
+ end
95
+ end
96
+
97
+ class AccessTokenSourceFactory
98
+ def initialize(client_id, client_secret, log_func)
99
+ @client_id = client_id
100
+ @client_secret = client_secret
101
+ @log_func = log_func
102
+ end
103
+
104
+ def create(network_manager)
105
+ AccessTokenSource.new(network_manager, @client_id, @client_secret, @log_func)
106
+ end
107
+ end
108
+ end
109
+ end
@@ -12,20 +12,23 @@ module Kameleoon
12
12
  class ActivityEvent
13
13
  EVENT_TYPE = 'activity'
14
14
 
15
- attr_accessor :sent
15
+ attr_reader :sent
16
16
 
17
17
  def initialize
18
18
  @sent = false
19
- @nonce = Kameleoon::Utils.generate_random_string(Kameleoon::NONCE_LENGTH)
20
19
  end
21
20
 
22
21
  def obtain_full_post_text_line
23
22
  params = {
24
23
  eventType: EVENT_TYPE,
25
- nonce: @nonce
24
+ nonce: Kameleoon::Utils.generate_random_string(Kameleoon::NONCE_LENGTH)
26
25
  }
27
26
  UriHelper.encode_query(params)
28
27
  end
28
+
29
+ def mark_as_sent
30
+ @sent = true
31
+ end
29
32
  end
30
33
  end
31
34
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kameleoon/utils'
4
+
5
+ module Kameleoon
6
+ module Network
7
+ module Cookie
8
+ COOKIE_KEY_JS = '_js_'
9
+ VISITOR_CODE_COOKIE = 'kameleoonVisitorCode'
10
+ COOKIE_TTL_SECONDS = 380 * 86_400 # 380 days in seconds
11
+
12
+ class CookieManager
13
+ attr_accessor :consent_required
14
+
15
+ def initialize(top_level_domain)
16
+ @consent_required = false
17
+ @top_level_domain = top_level_domain
18
+ end
19
+
20
+ def get_or_add(cookies, default_visitor_code = nil)
21
+ return if cookies.nil?
22
+
23
+ visitor_code = get_visitor_code_from_cookies(cookies)
24
+ unless visitor_code.nil?
25
+ Utils::VisitorCode.validate(visitor_code)
26
+ # Remove adding cookies when we will be sure that it doesn't break anything
27
+ add(visitor_code, cookies) unless @consent_required
28
+ return visitor_code
29
+ end
30
+
31
+ if default_visitor_code.nil?
32
+ visitor_code = Utils::VisitorCode.generate
33
+ add(visitor_code, cookies) unless @consent_required
34
+ return visitor_code
35
+ end
36
+
37
+ visitor_code = default_visitor_code
38
+ Utils::VisitorCode.validate(visitor_code)
39
+ add(visitor_code, cookies)
40
+ visitor_code
41
+ end
42
+
43
+ def update(visitor_code, consent, cookies)
44
+ return if cookies.nil?
45
+
46
+ if consent
47
+ add(visitor_code, cookies)
48
+ else
49
+ remove(cookies)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def add(visitor_code, cookies)
56
+ cookie = {
57
+ value: visitor_code,
58
+ expires: Time.now + COOKIE_TTL_SECONDS,
59
+ path: '/',
60
+ domain: @top_level_domain
61
+ }
62
+ cookies[VISITOR_CODE_COOKIE] = cookie
63
+ end
64
+
65
+ def remove(cookies)
66
+ cookies[VISITOR_CODE_COOKIE] = nil if @consent_required
67
+ end
68
+
69
+ def get_visitor_code_from_cookies(cookies)
70
+ cookie = cookies[VISITOR_CODE_COOKIE]
71
+ case cookie
72
+ when String
73
+ visitor_code = cookie
74
+ when Hash
75
+ visitor_code = cookie[:value]
76
+ end
77
+ visitor_code = visitor_code[COOKIE_KEY_JS.size..] if visitor_code&.start_with?(COOKIE_KEY_JS)
78
+ visitor_code = nil if visitor_code&.empty?
79
+ visitor_code
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'em-synchrony/em-http'
4
3
  require 'net/http'
5
4
  require 'kameleoon/version'
6
5
  require 'kameleoon/network/response'
@@ -9,14 +8,16 @@ require 'kameleoon/exceptions'
9
8
  module Kameleoon
10
9
  module Network
11
10
  class NetProvider
12
- def make_request(_request)
11
+ def make_request(request)
13
12
  raise KameleoonError, 'Call of not implemented method!'
14
13
  end
15
14
 
16
15
  private
17
16
 
18
17
  def collect_headers(request)
19
- headers = { 'Content-Type': request.content_type }
18
+ headers = request.extra_headers || {}
19
+ headers['Authorization'] = "Bearer #{request.access_token}" unless request.access_token.nil?
20
+ headers['Content-Type'] = request.content_type
20
21
  headers['User-Agent'] = request.user_agent unless request.user_agent.nil?
21
22
  headers
22
23
  end
@@ -26,65 +27,31 @@ module Kameleoon
26
27
  end
27
28
  end
28
29
 
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
30
  class SyncNetProvider < NetProvider
64
31
  def make_request(request)
65
32
  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)
33
+ case request.method
34
+ when Method::GET
35
+ req = Net::HTTP::Get.new(request.url)
36
+ when Method::POST
37
+ req = Net::HTTP::Post.new(request.url)
38
+ req.body = request.data
39
+ else
40
+ return unknown_method_response(request.method, request)
41
+ end
42
+ timeout = request.timeout.to_f / 1000.0
43
+ headers = collect_headers(request)
44
+ headers.each { |k, v| req[k] = v }
45
+ uri = URI(request.url)
46
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: timeout,
47
+ read_timeout: timeout, ssl_timeout: timeout) do |http|
48
+ resp = http.request(req)
86
49
  end
87
- Response.new(nil, resp.code.to_i, resp.body, request)
50
+ body = resp.body
51
+ body = nil if body&.empty?
52
+ Response.new(nil, resp.code.to_i, body, request)
53
+ rescue => e
54
+ Response.new(e, nil, nil, request)
88
55
  end
89
56
  end
90
57
  end
@@ -1,27 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'em-synchrony'
4
3
  require 'kameleoon/network/content_type'
5
4
  require 'kameleoon/network/method'
6
5
  require 'kameleoon/network/request'
7
6
  require 'kameleoon/network/net_provider'
7
+ require 'kameleoon/network/uri_helper'
8
+ require 'kameleoon/version'
8
9
 
9
10
  module Kameleoon
10
11
  module Network
11
12
  ##
12
13
  # NetworkManager is used to make API calls.
13
14
  class NetworkManager
14
- FETCH_CONFIGURATION_ATTEMPT_NUMBER = 4
15
- TRACKING_CALL_ATTEMPT_NUMBER = 4
15
+ FETCH_CONFIGURATION_ATTEMPT_NUMBER = 3
16
+ TRACKING_CALL_ATTEMPT_NUMBER = 3
16
17
  TRACKING_CALL_RETRY_DELAY = 5.0 # in seconds
18
+ SDK_TYPE_HEADER = 'X-Kameleoon-SDK-Type'
19
+ SDK_VERSION_HEADER = 'X-Kameleoon-SDK-Version'
20
+ ACCESS_TOKEN_GRANT_TYPE = 'client_credentials'
17
21
 
18
- attr_reader :environment, :default_timeout, :url_provider
22
+ attr_reader :environment, :default_timeout, :access_token_source, :url_provider
19
23
 
20
- def initialize(environment, default_timeout, url_provider, log_func = nil)
24
+ def initialize(environment, default_timeout, access_token_source_factory, url_provider, log_func = nil)
21
25
  @environment = environment
22
26
  @default_timeout = default_timeout
27
+ @access_token_source = access_token_source_factory.create(self)
23
28
  @url_provider = url_provider
24
- @em_net_provider = EMNetProvider.new
25
29
  @sync_net_provider = SyncNetProvider.new
26
30
  @log_func = log_func
27
31
  end
@@ -29,33 +33,23 @@ module Kameleoon
29
33
  def fetch_configuration(timestamp = nil, timeout = nil)
30
34
  url = @url_provider.make_configuration_url(@environment, timestamp)
31
35
  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
36
+ sdk_headers = { SDK_TYPE_HEADER => SDK_NAME, SDK_VERSION_HEADER => SDK_VERSION }
37
+ request = Request.new(Method::GET, url, ContentType::JSON, timeout, extra_headers: sdk_headers)
38
+ make_call(request, false, FETCH_CONFIGURATION_ATTEMPT_NUMBER - 1)
43
39
  end
44
40
 
45
41
  def get_remote_data(key, timeout = nil)
46
42
  url = @url_provider.make_api_data_get_request_url(key)
47
43
  timeout = ensure_timeout(timeout)
48
44
  request = Request.new(Method::GET, url, ContentType::JSON, timeout)
49
- response = @sync_net_provider.make_request(request)
50
- handle_response(response)
45
+ make_call(request, true)
51
46
  end
52
47
 
53
48
  def get_remote_visitor_data(visitor_code, timeout = nil)
54
49
  url = @url_provider.make_visitor_data_get_url(visitor_code)
55
50
  timeout = ensure_timeout(timeout)
56
51
  request = Request.new(Method::GET, url, ContentType::JSON, timeout)
57
- response = @sync_net_provider.make_request(request)
58
- handle_response(response)
52
+ make_call(request, true)
59
53
  end
60
54
 
61
55
  def send_tracking_data(visitor_code, lines, user_agent, timeout = nil)
@@ -64,25 +58,57 @@ module Kameleoon
64
58
  url = @url_provider.make_tracking_url(visitor_code)
65
59
  timeout = ensure_timeout(timeout)
66
60
  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)
61
+ request = Request.new(Method::POST, url, ContentType::TEXT, timeout, user_agent: user_agent, data: data)
68
62
  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
63
+ result = make_call(request, true, TRACKING_CALL_ATTEMPT_NUMBER - 1, TRACKING_CALL_RETRY_DELAY)
64
+ lines.each(&:mark_as_sent) if result != false
81
65
  end
82
66
  end
83
67
 
68
+ def fetch_access_jwtoken(client_id, client_secret, timeout = nil)
69
+ url = @url_provider.make_access_token_url
70
+ timeout = ensure_timeout(timeout)
71
+ data_map = {
72
+ grant_type: ACCESS_TOKEN_GRANT_TYPE,
73
+ client_id: client_id,
74
+ client_secret: client_secret
75
+ }
76
+ data = UriHelper.encode_query(data_map).encode('UTF-8')
77
+ request = Request.new(Method::POST, url, ContentType::FORM, timeout, data: data)
78
+ make_call(request, false)
79
+ end
80
+
84
81
  private
85
82
 
83
+ def make_call(request, try_access_token_auth, retry_limit = 0, retry_delay = 0)
84
+ attempt = 0
85
+ success = false
86
+ while !success && (attempt <= retry_limit)
87
+ delay(retry_delay) if attempt.positive? && retry_delay.positive?
88
+ try_authorize(request) if try_access_token_auth
89
+ response = @sync_net_provider.make_request(request)
90
+ if !response.error.nil?
91
+ log_failure(response.request, "Error occurred during request: #{response.error}")
92
+ elsif response.code / 100 != 2
93
+ if ((response.code == 401) || (response.code == 403)) && response.request.access_token
94
+ @log_func&.call("Unexpected rejection of access token '#{response.request.access_token}'")
95
+ @access_token_source.discard_token(response.request.access_token)
96
+ if attempt == retry_limit
97
+ try_access_token_auth = false
98
+ retry_delay = 0
99
+ request.authorize(nil)
100
+ attempt -= 1
101
+ end
102
+ end
103
+ log_failure(response.request, "Received unexpected status code '#{response.code}'")
104
+ else
105
+ success = true
106
+ end
107
+ attempt += 1
108
+ end
109
+ success ? response.body : false
110
+ end
111
+
86
112
  def log_failure(request, message)
87
113
  return if @log_func.nil?
88
114
 
@@ -99,15 +125,11 @@ module Kameleoon
99
125
  timeout.nil? ? @default_timeout : timeout
100
126
  end
101
127
 
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
128
+ def try_authorize(request)
129
+ token = @access_token_source.get_token
130
+ return if request.authorize(token)
131
+
132
+ @log_func&.call("Failed to authorize #{request.method} call '#{request.url}'")
111
133
  end
112
134
  end
113
135
  end