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.
- checksums.yaml +4 -4
- data/lib/kameleoon/client_readiness.rb +40 -0
- data/lib/kameleoon/configuration/data_file.rb +41 -0
- data/lib/kameleoon/configuration/feature_flag.rb +3 -2
- data/lib/kameleoon/configuration/rule.rb +18 -3
- data/lib/kameleoon/configuration/settings.rb +5 -0
- data/lib/kameleoon/configuration/variable.rb +0 -2
- data/lib/kameleoon/configuration/variation_exposition.rb +0 -2
- data/lib/kameleoon/data/browser.rb +1 -1
- data/lib/kameleoon/data/conversion.rb +1 -1
- data/lib/kameleoon/data/custom_data.rb +10 -14
- data/lib/kameleoon/data/data.rb +22 -3
- data/lib/kameleoon/data/device.rb +2 -1
- data/lib/kameleoon/data/manager/assigned_variation.rb +38 -0
- data/lib/kameleoon/data/manager/data_array_storage.rb +43 -0
- data/lib/kameleoon/data/manager/data_map_storage.rb +43 -0
- data/lib/kameleoon/data/manager/page_view_visit.rb +19 -0
- data/lib/kameleoon/data/manager/visitor.rb +142 -0
- data/lib/kameleoon/data/manager/visitor_manager.rb +71 -0
- data/lib/kameleoon/data/page_view.rb +2 -1
- data/lib/kameleoon/exceptions.rb +30 -35
- data/lib/kameleoon/hybrid/manager.rb +13 -31
- data/lib/kameleoon/{client.rb → kameleoon_client.rb} +194 -334
- data/lib/kameleoon/kameleoon_client_config.rb +91 -0
- data/lib/kameleoon/kameleoon_client_factory.rb +42 -0
- data/lib/kameleoon/managers/warehouse/warehouse_manager.rb +33 -0
- data/lib/kameleoon/network/access_token_source.rb +109 -0
- data/lib/kameleoon/network/activity_event.rb +6 -3
- data/lib/kameleoon/network/cookie/cookie_manager.rb +84 -0
- data/lib/kameleoon/network/net_provider.rb +25 -58
- data/lib/kameleoon/network/network_manager.rb +65 -43
- data/lib/kameleoon/network/request.rb +7 -2
- data/lib/kameleoon/network/response.rb +0 -8
- data/lib/kameleoon/network/url_provider.rb +30 -12
- data/lib/kameleoon/real_time/sse_client.rb +2 -0
- data/lib/kameleoon/targeting/conditions/browser_condition.rb +2 -3
- data/lib/kameleoon/targeting/conditions/conversion_condition.rb +12 -4
- data/lib/kameleoon/targeting/conditions/custom_datum.rb +19 -13
- data/lib/kameleoon/targeting/conditions/device_condition.rb +3 -4
- data/lib/kameleoon/targeting/conditions/exclusive_experiment.rb +2 -1
- data/lib/kameleoon/targeting/conditions/page_title_condition.rb +11 -4
- data/lib/kameleoon/targeting/conditions/page_url_condition.rb +18 -4
- data/lib/kameleoon/targeting/conditions/string_value_condition.rb +2 -0
- data/lib/kameleoon/targeting/conditions/target_experiment.rb +11 -6
- data/lib/kameleoon/utils.rb +41 -4
- data/lib/kameleoon/version.rb +1 -1
- data/lib/kameleoon.rb +4 -2
- metadata +16 -10
- data/lib/kameleoon/client_config.rb +0 -44
- data/lib/kameleoon/configuration/experiment.rb +0 -42
- data/lib/kameleoon/cookie.rb +0 -88
- data/lib/kameleoon/factory.rb +0 -43
- data/lib/kameleoon/network/experiment_event.rb +0 -35
- data/lib/kameleoon/storage/variation_storage.rb +0 -42
- 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
|
-
|
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:
|
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(
|
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 =
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
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 =
|
15
|
-
TRACKING_CALL_ATTEMPT_NUMBER =
|
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
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
70
|
-
|
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
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|