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.
- 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
|