kameleoon-client-ruby 2.3.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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