kameleoon-client-ruby 3.20.0 → 3.22.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 +120 -22
- data/lib/kameleoon/events/data_file_update_event.rb +37 -0
- data/lib/kameleoon/events/data_file_update_handler.rb +22 -0
- data/lib/kameleoon/events/event_handler.rb +14 -0
- data/lib/kameleoon/events/event_manager.rb +79 -0
- data/lib/kameleoon/events/event_type.rb +17 -0
- data/lib/kameleoon/events/http_request_failure.rb +63 -0
- data/lib/kameleoon/events/http_request_handler.rb +32 -0
- data/lib/kameleoon/events/request_type.rb +29 -0
- data/lib/kameleoon/exceptions.rb +16 -0
- data/lib/kameleoon/kameleoon_client.rb +152 -57
- data/lib/kameleoon/kameleoon_client_factory.rb +1 -1
- data/lib/kameleoon/network/network_manager.rb +33 -10
- data/lib/kameleoon/real_time/{real_time_configuration_service.rb → real_time_event_service.rb} +16 -7
- data/lib/kameleoon/targeting/conditions/unknown_condition.rb +3 -3
- data/lib/kameleoon/version.rb +1 -1
- metadata +11 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7f492badef67f0b4e09561d3a3d423e4497dbd7efe22d07641cac9485fe9e100
|
|
4
|
+
data.tar.gz: 95f7268b78ecd64bd4707c8fc8581251b8f07038a32eb044aebdb0b1cab652b6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 194b5aeee439890f068abe5b0b4c0f11fb508844de81e54e5b5582cefed5c7849d17054cb469994b57244177b61b9cfb89ccfecf771d9c135cc7126a277140e8
|
|
7
|
+
data.tar.gz: 114ecb570390aa32386abf7074463d903884d6faadf8c7b3de77953bc38152dc70cdf3eb7d5d8ac64de127014f318a2d2e53359c830cb9af5d8f3118d581b8d7
|
|
@@ -1,40 +1,138 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'concurrent'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
require 'kameleoon/exceptions'
|
|
4
6
|
|
|
5
7
|
module Kameleoon
|
|
8
|
+
# ClientReadiness tracks whether the SDK has successfully loaded its configuration
|
|
9
|
+
# and exposes that state to `KameleoonClient#wait_init` and `KameleoonClient#is_ready?`:
|
|
10
|
+
# - a successful fetch settles the state successfully;
|
|
11
|
+
# - a failed fetch settles it with an `Exception::Initialization` error.
|
|
12
|
+
#
|
|
13
|
+
# Readiness is monotonic: once the SDK is ready it stays ready, and a later failed fetch
|
|
14
|
+
# can never revert it. A failure reported before the first success does not prevent a
|
|
15
|
+
# subsequent successful retry from marking the SDK ready.
|
|
16
|
+
#
|
|
17
|
+
# It is created on the thread that builds the client but settled from the background
|
|
18
|
+
# thread(s) performing configuration fetches, so it relies on Concurrent::Event
|
|
19
|
+
# (which allows cross-thread release).
|
|
6
20
|
class ClientReadiness
|
|
7
|
-
|
|
21
|
+
# State is a single settlement of the readiness state: `error` is written at most
|
|
22
|
+
# once, before the event is set, and never changes afterwards, so a waiter which
|
|
23
|
+
# obtained the state observes exactly the outcome it was registered for.
|
|
24
|
+
class State
|
|
25
|
+
attr_accessor :error
|
|
8
26
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
27
|
+
def initialize
|
|
28
|
+
@event = Concurrent::Event.new
|
|
29
|
+
@error = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def settle
|
|
33
|
+
@event.set
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def settled?
|
|
37
|
+
@event.set?
|
|
38
|
+
end
|
|
15
39
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@is_initializing = true
|
|
20
|
-
@condition.acquire_write_lock
|
|
40
|
+
# @return [Boolean] whether the state was settled before the timeout elapsed.
|
|
41
|
+
def wait(timeout_second)
|
|
42
|
+
@event.wait(timeout_second)
|
|
21
43
|
end
|
|
22
44
|
end
|
|
45
|
+
private_constant :State
|
|
46
|
+
|
|
47
|
+
def initialize(site_code, environment)
|
|
48
|
+
@mutex = Mutex.new
|
|
49
|
+
@state = State.new
|
|
50
|
+
@ready = false
|
|
51
|
+
@site_code = site_code
|
|
52
|
+
@environment = environment
|
|
53
|
+
end
|
|
23
54
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@
|
|
28
|
-
@
|
|
55
|
+
# Marks the SDK ready and releases all waiters.
|
|
56
|
+
def mark_ready
|
|
57
|
+
@mutex.synchronize do
|
|
58
|
+
@ready = true
|
|
59
|
+
if @state.settled?
|
|
60
|
+
unless @state.error.nil?
|
|
61
|
+
# Recovery after a failure: the failed state is left settled for its waiters,
|
|
62
|
+
# and a fresh, ready state is published for the new ones.
|
|
63
|
+
state = State.new
|
|
64
|
+
state.settle
|
|
65
|
+
@state = state
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
@state.settle
|
|
69
|
+
end
|
|
29
70
|
end
|
|
30
71
|
end
|
|
31
72
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
73
|
+
# Reports a failed fetch with an `Exception::Initialization` error, but only while
|
|
74
|
+
# the SDK is not yet ready. A failure never reverts an already-ready client and is
|
|
75
|
+
# reported at most once, so the first reported cause is the one waiters observe.
|
|
76
|
+
#
|
|
77
|
+
# @param cause [StandardError, nil] the failure which prevented the SDK from
|
|
78
|
+
# loading its configuration.
|
|
79
|
+
def mark_not_ready(cause)
|
|
80
|
+
@mutex.synchronize do
|
|
81
|
+
unless @state.settled?
|
|
82
|
+
@state.error = Exception::Initialization.new(@site_code, @environment, cause)
|
|
83
|
+
@state.settle
|
|
84
|
+
end # else: already ready, or the failure was already reported
|
|
36
85
|
end
|
|
37
|
-
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return [Boolean] `true` if the SDK has been successfully initialized, `false`
|
|
89
|
+
# otherwise (including while the initialization is still pending or has failed).
|
|
90
|
+
# It never blocks.
|
|
91
|
+
#
|
|
92
|
+
# This is the per-request hot path, so it is lock-free: `@ready` is only ever
|
|
93
|
+
# flipped false -> true (readiness is monotonic), making a plain ivar read safe.
|
|
94
|
+
# At worst a reader briefly observes `false` while `mark_ready` is completing,
|
|
95
|
+
# which is indistinguishable from calling a moment earlier.
|
|
96
|
+
def ready?
|
|
97
|
+
@ready
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Blocks until the readiness state is settled, but no longer than `timeout_second`.
|
|
101
|
+
# Returns the result of the configuration fetch: nil once the SDK is ready, or the
|
|
102
|
+
# `Exception::Initialization` error the fetch failure was reported with (fail-fast -
|
|
103
|
+
# it does not wait for background retries). If no fetch result is available within
|
|
104
|
+
# the timeout, returns an `Exception::Initialization` error caused by the expired
|
|
105
|
+
# timeout. A non-positive timeout expires immediately unless a result is already
|
|
106
|
+
# available. An expired timeout does not settle the readiness state: once the SDK
|
|
107
|
+
# becomes ready, a subsequent call returns nil.
|
|
108
|
+
#
|
|
109
|
+
# @param timeout_second [Numeric] maximum number of seconds to wait.
|
|
110
|
+
# @return [Kameleoon::Exception::Initialization, nil] nil if the SDK is ready.
|
|
111
|
+
def wait_with_timeout(timeout_second)
|
|
112
|
+
state = current_state
|
|
113
|
+
return state.error if state.settled?
|
|
114
|
+
return timeout_failure(timeout_second) if timeout_second <= 0
|
|
115
|
+
|
|
116
|
+
return state.error if state.wait(timeout_second)
|
|
117
|
+
|
|
118
|
+
timeout_failure(timeout_second)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
# The timeout failure wraps a `Timeout::Error` cause (the counterpart of the
|
|
124
|
+
# timeout error types the other SDKs report), so a caller can distinguish an
|
|
125
|
+
# expired timeout from a failed fetch.
|
|
126
|
+
def timeout_failure(timeout_second)
|
|
127
|
+
cause = Timeout::Error.new(
|
|
128
|
+
"initialization did not complete within #{(timeout_second * 1000).round} ms"
|
|
129
|
+
)
|
|
130
|
+
Exception::Initialization.new(@site_code, @environment, cause)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Synchronized for safe publication of the state swapped in by a recovery.
|
|
134
|
+
def current_state
|
|
135
|
+
@mutex.synchronize { @state }
|
|
38
136
|
end
|
|
39
137
|
end
|
|
40
138
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kameleoon
|
|
4
|
+
module Events
|
|
5
|
+
##
|
|
6
|
+
# Describes an update of the SDK data file (configuration) reported to `DataFileUpdateHandler`.
|
|
7
|
+
class DataFileUpdateEvent
|
|
8
|
+
##
|
|
9
|
+
# Source of a data file update.
|
|
10
|
+
module Source
|
|
11
|
+
##
|
|
12
|
+
# The data file was updated by periodic polling.
|
|
13
|
+
POLLING = :polling
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# The data file was updated by a real-time (streaming) notification.
|
|
17
|
+
STREAMING = :streaming
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Symbol] The source of the data file update, one of the `Source` constants.
|
|
21
|
+
attr_reader :source
|
|
22
|
+
|
|
23
|
+
# @return [Integer] The date of the last modification of the data file,
|
|
24
|
+
# in Unix time milliseconds.
|
|
25
|
+
attr_reader :date_modified
|
|
26
|
+
|
|
27
|
+
def initialize(source, date_modified)
|
|
28
|
+
@source = source
|
|
29
|
+
@date_modified = date_modified
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_s
|
|
33
|
+
"DataFileUpdateEvent{source:#{@source},date_modified:#{@date_modified}}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'kameleoon/events/event_handler'
|
|
4
|
+
|
|
5
|
+
module Kameleoon
|
|
6
|
+
module Events
|
|
7
|
+
##
|
|
8
|
+
# Handler of `EventType::DATAFILE_UPDATE` SDK events.
|
|
9
|
+
#
|
|
10
|
+
# A handler must respond to the following method:
|
|
11
|
+
#
|
|
12
|
+
# # Called when the SDK data file (configuration) is updated.
|
|
13
|
+
# #
|
|
14
|
+
# # @param event [Kameleoon::Events::DataFileUpdateEvent] The data file update details.
|
|
15
|
+
# def on_update(event); end
|
|
16
|
+
#
|
|
17
|
+
# Including this module is optional and serves documentation purposes only.
|
|
18
|
+
module DataFileUpdateHandler
|
|
19
|
+
include EventHandler
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kameleoon
|
|
4
|
+
module Events
|
|
5
|
+
##
|
|
6
|
+
# Base module for all SDK event handlers. See `EventType` for the supported event types
|
|
7
|
+
# and their corresponding handler modules.
|
|
8
|
+
#
|
|
9
|
+
# Including the handler modules is optional: any object which responds to the methods
|
|
10
|
+
# required by the selected event type is accepted by `KameleoonClient#set_event_handler`.
|
|
11
|
+
module EventHandler
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'kameleoon/events/data_file_update_event'
|
|
4
|
+
require 'kameleoon/events/event_type'
|
|
5
|
+
require 'kameleoon/events/http_request_failure'
|
|
6
|
+
require 'kameleoon/events/request_type'
|
|
7
|
+
require 'kameleoon/logging/kameleoon_logger'
|
|
8
|
+
|
|
9
|
+
module Kameleoon
|
|
10
|
+
module Events
|
|
11
|
+
##
|
|
12
|
+
# EventManager stores SDK event handlers and fires SDK events.
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
15
|
+
class EventManager
|
|
16
|
+
HANDLER_METHODS = {
|
|
17
|
+
EventType::HTTP_REQUEST => %i[on_request_succeeded on_request_failed],
|
|
18
|
+
EventType::DATAFILE_UPDATE => %i[on_update]
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@event_handlers = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def set_event_handler(event_type, handler)
|
|
26
|
+
handler_methods = HANDLER_METHODS[event_type]
|
|
27
|
+
if handler_methods.nil?
|
|
28
|
+
Logging::KameleoonLogger.error("Unknown event type '%s'", event_type)
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
unless handler.nil? || handler_methods.all? { |method| handler.respond_to?(method) }
|
|
32
|
+
Logging::KameleoonLogger.error(
|
|
33
|
+
"Handler for event type '%s' must respond to the following methods: %s",
|
|
34
|
+
event_type, handler_methods.join(', ')
|
|
35
|
+
)
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
if handler.nil?
|
|
39
|
+
@event_handlers.delete(event_type)
|
|
40
|
+
else
|
|
41
|
+
@event_handlers[event_type] = handler
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def fire_http_request_succeeded(request_type, http_status, duration_millis)
|
|
46
|
+
handler = @event_handlers[EventType::HTTP_REQUEST]
|
|
47
|
+
return if handler.nil?
|
|
48
|
+
|
|
49
|
+
begin
|
|
50
|
+
handler.on_request_succeeded(request_type, http_status, duration_millis)
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
Logging::KameleoonLogger.warning('HTTP request event handler failed: %s', e)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def fire_http_request_failed(request_type, failure, duration_millis)
|
|
57
|
+
handler = @event_handlers[EventType::HTTP_REQUEST]
|
|
58
|
+
return if handler.nil?
|
|
59
|
+
|
|
60
|
+
begin
|
|
61
|
+
handler.on_request_failed(request_type, failure, duration_millis)
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
Logging::KameleoonLogger.warning('HTTP request event handler failed: %s', e)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def fire_data_file_update(event)
|
|
68
|
+
handler = @event_handlers[EventType::DATAFILE_UPDATE]
|
|
69
|
+
return if handler.nil?
|
|
70
|
+
|
|
71
|
+
begin
|
|
72
|
+
handler.on_update(event)
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
Logging::KameleoonLogger.warning('Data file update event handler failed: %s', e)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kameleoon
|
|
4
|
+
module Events
|
|
5
|
+
##
|
|
6
|
+
# SDK event types which can be handled with `KameleoonClient#set_event_handler`.
|
|
7
|
+
module EventType
|
|
8
|
+
##
|
|
9
|
+
# HTTP request event. Requires a handler implementing the `HttpRequestHandler` methods.
|
|
10
|
+
HTTP_REQUEST = :http_request
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# Data file update event. Requires a handler implementing the `DataFileUpdateHandler` methods.
|
|
14
|
+
DATAFILE_UPDATE = :datafile_update
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kameleoon
|
|
4
|
+
module Events
|
|
5
|
+
##
|
|
6
|
+
# Describes the failure of an SDK HTTP request reported to `HttpRequestHandler`.
|
|
7
|
+
class HttpRequestFailure
|
|
8
|
+
##
|
|
9
|
+
# Reason of an SDK HTTP request failure.
|
|
10
|
+
module Reason
|
|
11
|
+
##
|
|
12
|
+
# The request completed with an unexpected HTTP status code.
|
|
13
|
+
HTTP_STATUS = :http_status
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# The request failed with an error or exception (e.g. a network error).
|
|
17
|
+
EXCEPTION = :exception
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# The request was cancelled (for example, due to a timeout).
|
|
21
|
+
CANCELLED = :cancelled
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Symbol] The reason of the failure, one of the `Reason` constants.
|
|
25
|
+
attr_reader :reason
|
|
26
|
+
|
|
27
|
+
# @return [Integer, nil] The HTTP status code of the response. Not `nil` only if
|
|
28
|
+
# `reason` is `Reason::HTTP_STATUS`.
|
|
29
|
+
attr_reader :http_status
|
|
30
|
+
|
|
31
|
+
# @return [Exception, nil] The exception caused the failure. Not `nil` only if
|
|
32
|
+
# `reason` is `Reason::EXCEPTION`.
|
|
33
|
+
attr_reader :cause
|
|
34
|
+
|
|
35
|
+
# @api private
|
|
36
|
+
def self.from_http_status(http_status)
|
|
37
|
+
new(Reason::HTTP_STATUS, http_status, nil)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @api private
|
|
41
|
+
def self.from_exception(cause)
|
|
42
|
+
new(Reason::EXCEPTION, nil, cause)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @api private
|
|
46
|
+
def self.of_cancellation
|
|
47
|
+
new(Reason::CANCELLED, nil, nil)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_s
|
|
51
|
+
"HttpRequestFailure{reason:#{@reason},http_status:#{@http_status || 'nil'}," \
|
|
52
|
+
"cause:#{@cause.nil? ? 'nil' : @cause.class}}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initialize(reason, http_status, cause)
|
|
56
|
+
@reason = reason
|
|
57
|
+
@http_status = http_status
|
|
58
|
+
@cause = cause
|
|
59
|
+
end
|
|
60
|
+
private_class_method :new
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'kameleoon/events/event_handler'
|
|
4
|
+
|
|
5
|
+
module Kameleoon
|
|
6
|
+
module Events
|
|
7
|
+
##
|
|
8
|
+
# Handler of `EventType::HTTP_REQUEST` SDK events. The handler is called once per each
|
|
9
|
+
# actual HTTP request attempt, including retries.
|
|
10
|
+
#
|
|
11
|
+
# A handler must respond to the following methods:
|
|
12
|
+
#
|
|
13
|
+
# # Called when an SDK HTTP request completes successfully.
|
|
14
|
+
# #
|
|
15
|
+
# # @param request_type [Symbol] The type of the request, one of the `RequestType` constants.
|
|
16
|
+
# # @param http_status [Integer] The HTTP status code of the response.
|
|
17
|
+
# # @param duration_millis [Integer] The duration of the request in milliseconds.
|
|
18
|
+
# def on_request_succeeded(request_type, http_status, duration_millis); end
|
|
19
|
+
#
|
|
20
|
+
# # Called when an SDK HTTP request fails.
|
|
21
|
+
# #
|
|
22
|
+
# # @param request_type [Symbol] The type of the request, one of the `RequestType` constants.
|
|
23
|
+
# # @param failure [Kameleoon::Events::HttpRequestFailure] The failure details.
|
|
24
|
+
# # @param duration_millis [Integer] The duration of the request in milliseconds.
|
|
25
|
+
# def on_request_failed(request_type, failure, duration_millis); end
|
|
26
|
+
#
|
|
27
|
+
# Including this module is optional and serves documentation purposes only.
|
|
28
|
+
module HttpRequestHandler
|
|
29
|
+
include EventHandler
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kameleoon
|
|
4
|
+
module Events
|
|
5
|
+
##
|
|
6
|
+
# Type of an HTTP request performed by the SDK, reported to `HttpRequestHandler`.
|
|
7
|
+
module RequestType
|
|
8
|
+
##
|
|
9
|
+
# Fetching of the SDK configuration (data file).
|
|
10
|
+
DATAFILE = :datafile
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# Sending of tracking data.
|
|
14
|
+
TRACKING = :tracking
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
# Fetching of remote visitor data.
|
|
18
|
+
REMOTE_VISITOR_DATA = :remote_visitor_data
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# Fetching of remote data.
|
|
22
|
+
REMOTE_DATA = :remote_data
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# Fetching of the Kameleoon API access token.
|
|
26
|
+
ACCESS_TOKEN = :access_token
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/kameleoon/exceptions.rb
CHANGED
|
@@ -70,6 +70,22 @@ module Kameleoon
|
|
|
70
70
|
class SiteCodeIsEmpty < KameleoonError
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
# SDK could not be initialized: its configuration failed to load or no
|
|
74
|
+
# initialization result was available within the requested timeout.
|
|
75
|
+
# The failure which prevented the initialization is available via `cause`;
|
|
76
|
+
# it is also reported in the message so a caller which only logs the
|
|
77
|
+
# message still learns why the SDK is not initialized.
|
|
78
|
+
class Initialization < KameleoonError
|
|
79
|
+
# The failure which prevented the SDK from loading its configuration.
|
|
80
|
+
attr_reader :cause
|
|
81
|
+
|
|
82
|
+
def initialize(site_code, environment, cause)
|
|
83
|
+
@cause = cause
|
|
84
|
+
super("SDK is not initialized for siteCode: '#{site_code}', environment: '#{environment}'. " \
|
|
85
|
+
"Reason: #{cause.nil? ? 'unknown' : cause.message}")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
73
89
|
# Visitor Code Not Valid (empty or length > 255)
|
|
74
90
|
class VisitorCodeInvalid < KameleoonError
|
|
75
91
|
def initialize(visitor_code)
|
|
@@ -10,6 +10,13 @@ require 'kameleoon/data/manager/assigned_variation'
|
|
|
10
10
|
require 'kameleoon/data/manager/forced_experiment_variation'
|
|
11
11
|
require 'kameleoon/data/manager/legal_consent'
|
|
12
12
|
require 'kameleoon/data/manager/visitor_manager'
|
|
13
|
+
require 'kameleoon/events/data_file_update_event'
|
|
14
|
+
require 'kameleoon/events/data_file_update_handler'
|
|
15
|
+
require 'kameleoon/events/event_manager'
|
|
16
|
+
require 'kameleoon/events/event_type'
|
|
17
|
+
require 'kameleoon/events/http_request_failure'
|
|
18
|
+
require 'kameleoon/events/http_request_handler'
|
|
19
|
+
require 'kameleoon/events/request_type'
|
|
13
20
|
require 'kameleoon/exceptions'
|
|
14
21
|
require 'kameleoon/hybrid/manager'
|
|
15
22
|
require 'kameleoon/managers/data/data_manager'
|
|
@@ -22,7 +29,7 @@ require 'kameleoon/network/activity_event'
|
|
|
22
29
|
require 'kameleoon/network/network_manager'
|
|
23
30
|
require 'kameleoon/network/url_provider'
|
|
24
31
|
require 'kameleoon/network/cookie/cookie_manager'
|
|
25
|
-
require 'kameleoon/real_time/
|
|
32
|
+
require 'kameleoon/real_time/real_time_event_service'
|
|
26
33
|
require 'kameleoon/storage/cache_factory'
|
|
27
34
|
require 'kameleoon/targeting/models'
|
|
28
35
|
require 'kameleoon/targeting/targeting_manager'
|
|
@@ -58,7 +65,8 @@ module Kameleoon
|
|
|
58
65
|
@scheduler = Rufus::Scheduler.new
|
|
59
66
|
@site_code = site_code
|
|
60
67
|
@config = config
|
|
61
|
-
@
|
|
68
|
+
@disposed = false
|
|
69
|
+
@real_time_event_service = nil
|
|
62
70
|
@update_configuration_handler = nil
|
|
63
71
|
@fetch_configuration_update_job = nil
|
|
64
72
|
data_file = Configuration::DataFile.new(config.environment, nil)
|
|
@@ -67,11 +75,13 @@ module Kameleoon
|
|
|
67
75
|
@data_manager, config.session_duration_second, @scheduler
|
|
68
76
|
)
|
|
69
77
|
@hybrid_manager = Hybrid::ManagerImpl.new(HYBRID_EXPIRATION_TIME, @data_manager)
|
|
78
|
+
@event_manager = Events::EventManager.new
|
|
70
79
|
@network_manager = Network::NetworkManager.new(
|
|
71
80
|
config.environment,
|
|
72
81
|
config.default_timeout_millisecond,
|
|
73
82
|
Network::AccessTokenSourceFactory.new(config.client_id, config.client_secret),
|
|
74
|
-
Network::UrlProvider.new(site_code, config.network_domain)
|
|
83
|
+
Network::UrlProvider.new(site_code, config.network_domain),
|
|
84
|
+
@event_manager
|
|
75
85
|
)
|
|
76
86
|
@tracking_manager = Managers::Tracking::TrackingManager.new(
|
|
77
87
|
@data_manager, @network_manager, @visitor_manager, config.tracking_interval_second, @scheduler
|
|
@@ -81,7 +91,7 @@ module Kameleoon
|
|
|
81
91
|
@data_manager, @network_manager, @visitor_manager
|
|
82
92
|
)
|
|
83
93
|
@cookie_manager = Network::Cookie::CookieManager.new(@data_manager, @visitor_manager, config.top_level_domain)
|
|
84
|
-
@readiness = ClientReadiness.new
|
|
94
|
+
@readiness = ClientReadiness.new(site_code, config.environment)
|
|
85
95
|
@targeting_manager = Targeting::TargetingManager.new(@data_manager, @visitor_manager)
|
|
86
96
|
|
|
87
97
|
if @config.verbose_mode == true && Logging::KameleoonLogger.log_level == Logging::LogLevel::WARNING
|
|
@@ -92,13 +102,56 @@ module Kameleoon
|
|
|
92
102
|
site_code, config)
|
|
93
103
|
end
|
|
94
104
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
##
|
|
106
|
+
# Block until the SDK has loaded its configuration and is ready to use.
|
|
107
|
+
#
|
|
108
|
+
# The method returns the result of the configuration fetch: `true` once the
|
|
109
|
+
# SDK has been initialized, or `false` as soon as the fetch has failed
|
|
110
|
+
# (without waiting for background retries) or when no fetch result is
|
|
111
|
+
# available within the timeout. The failure which prevented the SDK from
|
|
112
|
+
# initializing is reported to the log.
|
|
113
|
+
#
|
|
114
|
+
# An expired timeout or a failed fetch does not affect the SDK state: the
|
|
115
|
+
# client keeps retrying in the background, and once the SDK becomes ready,
|
|
116
|
+
# a subsequent call returns true.
|
|
117
|
+
#
|
|
118
|
+
# @param [Integer, nil] timeout_millisecond Maximum time to wait, in
|
|
119
|
+
# milliseconds. When nil or negative, config.default_timeout_millisecond
|
|
120
|
+
# is applied.
|
|
121
|
+
# @return [Boolean] true if the SDK is ready, false if the fetch failed or
|
|
122
|
+
# the timeout elapsed first.
|
|
123
|
+
def wait_init(timeout_millisecond = nil)
|
|
124
|
+
Logging::KameleoonLogger.info('CALL: KameleoonClient.wait_init(timeout_millisecond: %s)', timeout_millisecond)
|
|
125
|
+
if timeout_millisecond.nil? || timeout_millisecond.negative?
|
|
126
|
+
timeout_millisecond = @config.default_timeout_millisecond
|
|
127
|
+
end
|
|
128
|
+
error = @readiness.wait_with_timeout(timeout_millisecond / 1000.0)
|
|
129
|
+
if error.nil?
|
|
130
|
+
Logging::KameleoonLogger.info('Kameleoon is initialized')
|
|
131
|
+
else
|
|
132
|
+
Logging::KameleoonLogger.error('Kameleoon failed to initialize due to error: %s', error.message)
|
|
133
|
+
end
|
|
134
|
+
result = error.nil?
|
|
98
135
|
Logging::KameleoonLogger.info('RETURN: KameleoonClient.wait_init -> (result: %s)', result)
|
|
99
136
|
result
|
|
100
137
|
end
|
|
101
138
|
|
|
139
|
+
##
|
|
140
|
+
# Indicates whether the SDK is ready for use, i.e. its configuration has been
|
|
141
|
+
# successfully loaded. Unlike `wait_init`, it returns immediately without blocking.
|
|
142
|
+
#
|
|
143
|
+
# It returns `true` if the SDK has been successfully initialized, `false` otherwise
|
|
144
|
+
# (including while the initialization is still pending or has failed).
|
|
145
|
+
#
|
|
146
|
+
# Readiness is monotonic: once the SDK becomes ready it stays ready.
|
|
147
|
+
#
|
|
148
|
+
# @return [Boolean] whether the SDK is ready for use.
|
|
149
|
+
def is_ready? # rubocop:disable Naming/PredicateName
|
|
150
|
+
ready = @readiness.ready?
|
|
151
|
+
Logging::KameleoonLogger.info('CALL/RETURN: KameleoonClient.is_ready? -> (ready: %s)', ready)
|
|
152
|
+
ready
|
|
153
|
+
end
|
|
154
|
+
|
|
102
155
|
##
|
|
103
156
|
# Obtain a visitor code.
|
|
104
157
|
#
|
|
@@ -697,6 +750,24 @@ module Kameleoon
|
|
|
697
750
|
map_active_features
|
|
698
751
|
end
|
|
699
752
|
|
|
753
|
+
##
|
|
754
|
+
# Sets the SDK event handler for the specified event type.
|
|
755
|
+
#
|
|
756
|
+
# The handler is called when the corresponding SDK event occurs. Supported event types are
|
|
757
|
+
# defined in `Kameleoon::Events::EventType`. Passing `nil` clears the handler for the
|
|
758
|
+
# specified event type.
|
|
759
|
+
#
|
|
760
|
+
# @param event_type [Symbol] The SDK event type to handle, one of the
|
|
761
|
+
# `Kameleoon::Events::EventType` constants.
|
|
762
|
+
# @param handler [Object, nil] The handler to register, or `nil` to remove the current handler.
|
|
763
|
+
# The handler must respond to the methods required by the selected event type
|
|
764
|
+
# (see `Kameleoon::Events::HttpRequestHandler` and `Kameleoon::Events::DataFileUpdateHandler`).
|
|
765
|
+
def set_event_handler(event_type, handler)
|
|
766
|
+
Logging::KameleoonLogger.info("CALL: KameleoonClient.set_event_handler(event_type: '%s', handler)", event_type)
|
|
767
|
+
@event_manager.set_event_handler(event_type, handler)
|
|
768
|
+
Logging::KameleoonLogger.info("RETURN: KameleoonClient.set_event_handler(event_type: '%s', handler)", event_type)
|
|
769
|
+
end
|
|
770
|
+
|
|
700
771
|
##
|
|
701
772
|
# The `on_update_configuration()` method allows you to handle the event when configuration
|
|
702
773
|
# has updated data. It takes one input parameter: callable **handler**. The handler
|
|
@@ -704,7 +775,13 @@ module Kameleoon
|
|
|
704
775
|
#
|
|
705
776
|
# @param handler [Callable | NilClass] The handler that will be called when the configuration
|
|
706
777
|
# is updated using a real-time configuration event.
|
|
778
|
+
#
|
|
779
|
+
# DEPRECATED. Please use `set_event_handler(Kameleoon::Events::EventType::DATAFILE_UPDATE, handler)` instead.
|
|
707
780
|
def on_update_configuration(handler)
|
|
781
|
+
Logging::KameleoonLogger.info(
|
|
782
|
+
'[DEPRECATION] `on_update_configuration` is deprecated. ' \
|
|
783
|
+
'Please use `set_event_handler(Kameleoon::Events::EventType::DATAFILE_UPDATE, handler)` instead.'
|
|
784
|
+
)
|
|
708
785
|
Logging::KameleoonLogger.info('CALL/RETURN: KameleoonClient.on_update_configuration(handler)')
|
|
709
786
|
@update_configuration_handler = handler
|
|
710
787
|
end
|
|
@@ -811,40 +888,41 @@ module Kameleoon
|
|
|
811
888
|
|
|
812
889
|
HYBRID_EXPIRATION_TIME = 5
|
|
813
890
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
rescue StandardError => e
|
|
822
|
-
Logging::KameleoonLogger.error('Initial configuration fetch failed: %s', e)
|
|
823
|
-
end
|
|
824
|
-
@readiness.set(ok)
|
|
825
|
-
manage_configuration_update(@data_manager.data_file.settings.real_time_update) if ok
|
|
826
|
-
end
|
|
827
|
-
end
|
|
828
|
-
|
|
891
|
+
# Fetches the configuration in a background thread; used for the initial fetch,
|
|
892
|
+
# the polling schedule, and the streaming events alike. The fetch outcome settles
|
|
893
|
+
# the client readiness, and a failed fetch always falls back to the polling mode:
|
|
894
|
+
# a streaming event means the configuration has changed, so staying in the
|
|
895
|
+
# streaming mode would leave the SDK serving the outdated configuration until
|
|
896
|
+
# the next event arrives. The server-provided settings are not mutated, so a
|
|
897
|
+
# later successful fetch switches back to streaming when requested.
|
|
829
898
|
def fetch_configuration_job(time_stamp = nil)
|
|
830
899
|
Thread.new do
|
|
900
|
+
cause = nil
|
|
831
901
|
ok = false
|
|
832
902
|
begin
|
|
833
903
|
ok = obtain_configuration(time_stamp)
|
|
834
904
|
rescue StandardError => e
|
|
905
|
+
cause = e
|
|
835
906
|
Logging::KameleoonLogger.error('Error occurred during configuration fetching: %s', e)
|
|
836
907
|
end
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
908
|
+
settle_readiness(ok, cause)
|
|
909
|
+
manage_configuration_update(ok && @data_manager.data_file.settings.real_time_update)
|
|
910
|
+
end
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
# A successful fetch marks the SDK ready, a failed one reports the failure so
|
|
914
|
+
# `wait_init` fails fast instead of sleeping out its timeout. A failure never
|
|
915
|
+
# reverts an already-ready client.
|
|
916
|
+
def settle_readiness(ok, cause)
|
|
917
|
+
if ok
|
|
918
|
+
@readiness.mark_ready
|
|
919
|
+
else
|
|
920
|
+
cause ||= Exception::KameleoonError.new('configuration fetch failed')
|
|
921
|
+
@readiness.mark_not_ready(cause)
|
|
844
922
|
end
|
|
845
923
|
end
|
|
846
924
|
|
|
847
|
-
def
|
|
925
|
+
def start_polling_datafile_update
|
|
848
926
|
return unless @fetch_configuration_update_job.nil?
|
|
849
927
|
|
|
850
928
|
@fetch_configuration_update_job = @scheduler.schedule_every @config.refresh_interval_second do
|
|
@@ -853,7 +931,7 @@ module Kameleoon
|
|
|
853
931
|
end
|
|
854
932
|
end
|
|
855
933
|
|
|
856
|
-
def
|
|
934
|
+
def stop_polling_datafile_update
|
|
857
935
|
return if @fetch_configuration_update_job.nil?
|
|
858
936
|
|
|
859
937
|
@fetch_configuration_update_job&.unschedule
|
|
@@ -861,27 +939,34 @@ module Kameleoon
|
|
|
861
939
|
Logging::KameleoonLogger.info('Scheduled job to fetch configuration is stopped.')
|
|
862
940
|
end
|
|
863
941
|
|
|
864
|
-
def
|
|
865
|
-
return unless @
|
|
942
|
+
def start_real_time_event_service_if_needed
|
|
943
|
+
return unless @real_time_event_service.nil?
|
|
866
944
|
|
|
867
945
|
url = @network_manager.url_provider.make_real_time_url
|
|
868
946
|
fetch_func = proc { |real_time_event| fetch_configuration_job(real_time_event.time_stamp) }
|
|
869
|
-
@
|
|
870
|
-
Kameleoon::RealTime::
|
|
947
|
+
@real_time_event_service =
|
|
948
|
+
Kameleoon::RealTime::RealTimeEventService.new(url, fetch_func)
|
|
871
949
|
end
|
|
872
950
|
|
|
873
|
-
def
|
|
874
|
-
@
|
|
875
|
-
@
|
|
951
|
+
def stop_real_time_event_service_if_needed
|
|
952
|
+
@real_time_event_service&.close
|
|
953
|
+
@real_time_event_service = nil
|
|
876
954
|
end
|
|
877
955
|
|
|
956
|
+
# A disposed client must not (re)start the update services: a fetch which was
|
|
957
|
+
# in flight when `dispose` ran would otherwise resurrect them. `@disposed` is
|
|
958
|
+
# set before `dispose` stops the services, so a fetch completing after that
|
|
959
|
+
# point can only stop things.
|
|
878
960
|
def manage_configuration_update(is_real_time_update)
|
|
879
|
-
if
|
|
880
|
-
|
|
881
|
-
|
|
961
|
+
if @disposed
|
|
962
|
+
stop_polling_datafile_update
|
|
963
|
+
stop_real_time_event_service_if_needed
|
|
964
|
+
elsif is_real_time_update
|
|
965
|
+
stop_polling_datafile_update
|
|
966
|
+
start_real_time_event_service_if_needed
|
|
882
967
|
else
|
|
883
|
-
|
|
884
|
-
|
|
968
|
+
stop_real_time_event_service_if_needed
|
|
969
|
+
start_polling_datafile_update
|
|
885
970
|
end
|
|
886
971
|
end
|
|
887
972
|
|
|
@@ -908,34 +993,44 @@ module Kameleoon
|
|
|
908
993
|
if response.configuration
|
|
909
994
|
configuration = JSON.parse(response.configuration)
|
|
910
995
|
data_file = Configuration::DataFile.new(@config.environment, configuration, response.last_modified)
|
|
911
|
-
|
|
912
|
-
|
|
996
|
+
source = time_stamp.nil? ? Events::DataFileUpdateEvent::Source::POLLING
|
|
997
|
+
: Events::DataFileUpdateEvent::Source::STREAMING
|
|
998
|
+
apply_new_configuration(data_file, source)
|
|
913
999
|
Logging::KameleoonLogger.info('Feature flags are fetched: %s', response.inspect)
|
|
914
1000
|
end
|
|
915
1001
|
true
|
|
916
1002
|
end
|
|
917
1003
|
|
|
918
|
-
def apply_new_configuration(data_file)
|
|
919
|
-
Logging::KameleoonLogger.debug('CALL: KameleoonClient.apply_new_configuration(data_file: %s)',
|
|
1004
|
+
def apply_new_configuration(data_file, source)
|
|
1005
|
+
Logging::KameleoonLogger.debug('CALL: KameleoonClient.apply_new_configuration(data_file: %s, source: %s)',
|
|
1006
|
+
data_file, source)
|
|
920
1007
|
@data_manager.data_file = data_file
|
|
921
1008
|
@network_manager.url_provider.apply_data_api_domain(data_file.settings.data_api_domain)
|
|
922
|
-
|
|
1009
|
+
@event_manager.fire_data_file_update(Events::DataFileUpdateEvent.new(source, data_file.date_modified))
|
|
1010
|
+
call_deprecated_update_handler if source == Events::DataFileUpdateEvent::Source::STREAMING
|
|
1011
|
+
Logging::KameleoonLogger.debug('RETURN: KameleoonClient.apply_new_configuration(data_file: %s, source: %s)',
|
|
1012
|
+
data_file, source)
|
|
923
1013
|
end
|
|
924
1014
|
|
|
925
1015
|
##
|
|
926
|
-
# Call the handler when configuration was updated
|
|
927
|
-
#
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
return if
|
|
931
|
-
|
|
932
|
-
|
|
1016
|
+
# Call the deprecated update configuration handler when configuration was updated
|
|
1017
|
+
# by a real-time (streaming) notification.
|
|
1018
|
+
def call_deprecated_update_handler
|
|
1019
|
+
handler = @update_configuration_handler
|
|
1020
|
+
return if handler.nil?
|
|
1021
|
+
|
|
1022
|
+
begin
|
|
1023
|
+
handler.call
|
|
1024
|
+
rescue StandardError => e
|
|
1025
|
+
Logging::KameleoonLogger.warning('Update configuration handler failed: %s', e)
|
|
1026
|
+
end
|
|
933
1027
|
end
|
|
934
1028
|
|
|
935
1029
|
def dispose(_object_id = nil)
|
|
936
1030
|
Logging::KameleoonLogger.debug('CALL: KameleoonClient.dispose')
|
|
937
|
-
|
|
938
|
-
|
|
1031
|
+
@disposed = true
|
|
1032
|
+
stop_polling_datafile_update
|
|
1033
|
+
stop_real_time_event_service_if_needed
|
|
939
1034
|
@visitor_manager.stop
|
|
940
1035
|
@tracking_manager.stop
|
|
941
1036
|
@scheduler.shutdown
|
|
@@ -24,7 +24,7 @@ module Kameleoon
|
|
|
24
24
|
key = get_client_key(site_code, config.environment)
|
|
25
25
|
client = @clients.compute_if_absent(key) do
|
|
26
26
|
client = KameleoonClient.new(site_code, config)
|
|
27
|
-
client.send(:
|
|
27
|
+
client.send(:fetch_configuration_job)
|
|
28
28
|
client
|
|
29
29
|
end
|
|
30
30
|
Logging::KameleoonLogger.info(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'kameleoon/events/event_manager'
|
|
3
4
|
require 'kameleoon/logging/kameleoon_logger'
|
|
4
5
|
require 'kameleoon/network/content_type'
|
|
5
6
|
require 'kameleoon/network/fetched_configuration'
|
|
@@ -14,8 +15,8 @@ module Kameleoon
|
|
|
14
15
|
##
|
|
15
16
|
# NetworkManager is used to make API calls.
|
|
16
17
|
class NetworkManager
|
|
17
|
-
FETCH_CONFIGURATION_ATTEMPT_NUMBER =
|
|
18
|
-
TRACKING_CALL_ATTEMPT_NUMBER =
|
|
18
|
+
FETCH_CONFIGURATION_ATTEMPT_NUMBER = 2
|
|
19
|
+
TRACKING_CALL_ATTEMPT_NUMBER = 2
|
|
19
20
|
TRACKING_CALL_RETRY_DELAY = 5.0 # in seconds
|
|
20
21
|
SDK_TYPE_HEADER = 'X-Kameleoon-SDK-Type'
|
|
21
22
|
SDK_VERSION_HEADER = 'X-Kameleoon-SDK-Version'
|
|
@@ -23,13 +24,14 @@ module Kameleoon
|
|
|
23
24
|
HEADER_IF_MODIFIED_SINCE = 'If-Modified-Since'
|
|
24
25
|
HEADER_LAST_MODIFIED = 'last-modified' # in lower case because the network lib casts response headers to lower
|
|
25
26
|
|
|
26
|
-
attr_reader :environment, :default_timeout, :access_token_source, :url_provider
|
|
27
|
+
attr_reader :environment, :default_timeout, :access_token_source, :url_provider, :event_manager
|
|
27
28
|
|
|
28
|
-
def initialize(environment, default_timeout, access_token_source_factory, url_provider)
|
|
29
|
+
def initialize(environment, default_timeout, access_token_source_factory, url_provider, event_manager)
|
|
29
30
|
@environment = environment
|
|
30
31
|
@default_timeout = default_timeout
|
|
31
32
|
@access_token_source = access_token_source_factory.create(self)
|
|
32
33
|
@url_provider = url_provider
|
|
34
|
+
@event_manager = event_manager
|
|
33
35
|
@sync_net_provider = SyncNetProvider.new
|
|
34
36
|
end
|
|
35
37
|
|
|
@@ -39,7 +41,8 @@ module Kameleoon
|
|
|
39
41
|
headers = { SDK_TYPE_HEADER => SDK_NAME, SDK_VERSION_HEADER => SDK_VERSION }
|
|
40
42
|
headers[HEADER_IF_MODIFIED_SINCE] = if_modified_since if if_modified_since
|
|
41
43
|
request = Request.new(Method::GET, url, ContentType::JSON, timeout, extra_headers: headers)
|
|
42
|
-
response, success = make_call(request, false,
|
|
44
|
+
response, success = make_call(Events::RequestType::DATAFILE, request, false,
|
|
45
|
+
FETCH_CONFIGURATION_ATTEMPT_NUMBER - 1)
|
|
43
46
|
return nil unless success
|
|
44
47
|
return FetchedConfiguration.new(nil, nil) if response.code == 304
|
|
45
48
|
|
|
@@ -51,14 +54,14 @@ module Kameleoon
|
|
|
51
54
|
url = @url_provider.make_api_data_get_request_url(key)
|
|
52
55
|
timeout = ensure_timeout(timeout)
|
|
53
56
|
request = Request.new(Method::GET, url, ContentType::JSON, timeout)
|
|
54
|
-
unwrap_response(*make_call(request, true))
|
|
57
|
+
unwrap_response(*make_call(Events::RequestType::REMOTE_DATA, request, true))
|
|
55
58
|
end
|
|
56
59
|
|
|
57
60
|
def get_remote_visitor_data(visitor_code, filter, is_unique_identifier, timeout = nil)
|
|
58
61
|
url = @url_provider.make_visitor_data_get_url(visitor_code, filter, is_unique_identifier)
|
|
59
62
|
timeout = ensure_timeout(timeout)
|
|
60
63
|
request = Request.new(Method::GET, url, ContentType::JSON, timeout)
|
|
61
|
-
unwrap_response(*make_call(request, true))
|
|
64
|
+
unwrap_response(*make_call(Events::RequestType::REMOTE_VISITOR_DATA, request, true))
|
|
62
65
|
end
|
|
63
66
|
|
|
64
67
|
def send_tracking_data(lines, timeout = nil)
|
|
@@ -67,7 +70,8 @@ module Kameleoon
|
|
|
67
70
|
url = @url_provider.make_tracking_url
|
|
68
71
|
timeout = ensure_timeout(timeout)
|
|
69
72
|
request = Request.new(Method::POST, url, ContentType::WILDCARD, timeout, data: lines)
|
|
70
|
-
unwrap_response(*make_call(request, true,
|
|
73
|
+
unwrap_response(*make_call(Events::RequestType::TRACKING, request, true,
|
|
74
|
+
TRACKING_CALL_ATTEMPT_NUMBER - 1, TRACKING_CALL_RETRY_DELAY))
|
|
71
75
|
end
|
|
72
76
|
|
|
73
77
|
def fetch_access_jwtoken(basic_auth_token, timeout = nil)
|
|
@@ -79,12 +83,12 @@ module Kameleoon
|
|
|
79
83
|
data = UriHelper.encode_query(data_map).encode('UTF-8')
|
|
80
84
|
request = Request.new(Method::POST, url, ContentType::FORM, timeout, data: data)
|
|
81
85
|
request.authorize(basic_auth_token)
|
|
82
|
-
unwrap_response(*make_call(request, false))
|
|
86
|
+
unwrap_response(*make_call(Events::RequestType::ACCESS_TOKEN, request, false))
|
|
83
87
|
end
|
|
84
88
|
|
|
85
89
|
private
|
|
86
90
|
|
|
87
|
-
def make_call(request, try_access_token_auth, retry_limit = 0, retry_delay = 0)
|
|
91
|
+
def make_call(request_type, request, try_access_token_auth, retry_limit = 0, retry_delay = 0)
|
|
88
92
|
Logging::KameleoonLogger.debug('Running request %s with access token %s, retry limit %s, retry delay %s ms',
|
|
89
93
|
request, try_access_token_auth, retry_limit, retry_delay)
|
|
90
94
|
attempt = 0
|
|
@@ -93,7 +97,10 @@ module Kameleoon
|
|
|
93
97
|
while !success && (attempt <= retry_limit)
|
|
94
98
|
delay(retry_delay) if attempt.positive? && retry_delay.positive?
|
|
95
99
|
try_authorize(request) if try_access_token_auth
|
|
100
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
|
96
101
|
response = @sync_net_provider.make_request(request)
|
|
102
|
+
duration_millis = (Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start_time).round
|
|
103
|
+
fire_http_request_event(request_type, response, duration_millis)
|
|
97
104
|
if response.success?
|
|
98
105
|
success = true
|
|
99
106
|
elsif !response.error.nil?
|
|
@@ -122,6 +129,22 @@ module Kameleoon
|
|
|
122
129
|
[response, success]
|
|
123
130
|
end
|
|
124
131
|
|
|
132
|
+
def fire_http_request_event(request_type, response, duration_millis)
|
|
133
|
+
if response.success?
|
|
134
|
+
@event_manager.fire_http_request_succeeded(request_type, response.code, duration_millis)
|
|
135
|
+
else
|
|
136
|
+
failure =
|
|
137
|
+
if response.error.nil?
|
|
138
|
+
Events::HttpRequestFailure.from_http_status(response.code)
|
|
139
|
+
elsif response.error.is_a?(Timeout::Error)
|
|
140
|
+
Events::HttpRequestFailure.of_cancellation
|
|
141
|
+
else
|
|
142
|
+
Events::HttpRequestFailure.from_exception(response.error)
|
|
143
|
+
end
|
|
144
|
+
@event_manager.fire_http_request_failed(request_type, failure, duration_millis)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
125
148
|
def get_log_level(attempt, attempt_count)
|
|
126
149
|
attempt < attempt_count ? Logging::LogLevel::WARNING : Logging::LogLevel::ERROR
|
|
127
150
|
end
|
data/lib/kameleoon/real_time/{real_time_configuration_service.rb → real_time_event_service.rb}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Kameleoon Real Time
|
|
3
|
+
# Kameleoon Real Time Event Service
|
|
4
4
|
|
|
5
5
|
require 'json'
|
|
6
6
|
require 'kameleoon/logging/kameleoon_logger'
|
|
@@ -10,19 +10,24 @@ require 'kameleoon/real_time/sse_client'
|
|
|
10
10
|
|
|
11
11
|
module Kameleoon
|
|
12
12
|
module RealTime
|
|
13
|
-
|
|
13
|
+
DATA_FILE_UPDATE_EVENT = 'configuration-update-event'
|
|
14
14
|
|
|
15
15
|
##
|
|
16
|
-
#
|
|
16
|
+
# RealTimeEventService is used for fetching updates of configuration
|
|
17
17
|
# (experiments and feature flags) in real time.
|
|
18
|
-
class
|
|
18
|
+
class RealTimeEventService
|
|
19
|
+
# Delay before re-establishing a failed SSE connection, so a persistent
|
|
20
|
+
# failure does not turn into a hot reconnect loop.
|
|
21
|
+
RECONNECT_DELAY_SECONDS = 3
|
|
22
|
+
|
|
19
23
|
##
|
|
20
24
|
# Parametrized initializer.
|
|
21
25
|
#
|
|
22
26
|
# @param url [String]
|
|
23
27
|
# @param update_handler [Callable[Kameleoon::RealTime::RealTimeEvent] | NilClass] Handler which
|
|
24
28
|
# is synchronously called for gotten RealTimeEvent objects.
|
|
25
|
-
|
|
29
|
+
# @param reconnect_delay [Numeric] Delay in seconds before a reconnect attempt.
|
|
30
|
+
def initialize(url, update_handler, sse_request_source = nil, reconnect_delay: RECONNECT_DELAY_SECONDS)
|
|
26
31
|
@url = url
|
|
27
32
|
@update_handler = update_handler
|
|
28
33
|
@need_close = false
|
|
@@ -32,6 +37,7 @@ module Kameleoon
|
|
|
32
37
|
'Connection': 'Keep-Alive'
|
|
33
38
|
}
|
|
34
39
|
@sse_request_source = sse_request_source
|
|
40
|
+
@reconnect_delay = reconnect_delay
|
|
35
41
|
@sse_thread = nil
|
|
36
42
|
@sse_client = nil
|
|
37
43
|
create_sse_client
|
|
@@ -42,7 +48,7 @@ module Kameleoon
|
|
|
42
48
|
def close
|
|
43
49
|
return if @need_close
|
|
44
50
|
|
|
45
|
-
Logging::KameleoonLogger.info('Real-time
|
|
51
|
+
Logging::KameleoonLogger.info('Real-time event service is shutting down')
|
|
46
52
|
@need_close = true
|
|
47
53
|
return if @sse_thread.nil?
|
|
48
54
|
|
|
@@ -60,7 +66,7 @@ module Kameleoon
|
|
|
60
66
|
def init_sse_client
|
|
61
67
|
message_handler = proc do |message|
|
|
62
68
|
Logging::KameleoonLogger.debug("Got SSE event: #{message.event}")
|
|
63
|
-
if message.event ==
|
|
69
|
+
if message.event == DATA_FILE_UPDATE_EVENT
|
|
64
70
|
event_dict = JSON.parse(message.data)
|
|
65
71
|
@update_handler&.call(RealTimeEvent.new(event_dict))
|
|
66
72
|
end
|
|
@@ -90,6 +96,9 @@ module Kameleoon
|
|
|
90
96
|
rescue StandardError => e
|
|
91
97
|
Logging::KameleoonLogger.error("Error occurred within SSE client: #{e}")
|
|
92
98
|
end
|
|
99
|
+
# The delay is applied to every re-establishment (a failed connection and
|
|
100
|
+
# a dropped stream alike), so a persistent failure cannot produce a hot loop.
|
|
101
|
+
sleep(@reconnect_delay) unless @need_close
|
|
93
102
|
end
|
|
94
103
|
end
|
|
95
104
|
end
|
|
@@ -5,11 +5,11 @@ require 'kameleoon/targeting/condition'
|
|
|
5
5
|
module Kameleoon
|
|
6
6
|
# @api private
|
|
7
7
|
module Targeting
|
|
8
|
-
# UnknownCondition represents not defined condition, always returns that visitor is targeted (
|
|
8
|
+
# UnknownCondition represents not defined condition, always returns that visitor is not targeted (false)
|
|
9
9
|
class UnknownCondition < Condition
|
|
10
10
|
def check(_data)
|
|
11
|
-
Logging::KameleoonLogger.warning('Condition of unknown type \'%s\' evaluated as
|
|
12
|
-
|
|
11
|
+
Logging::KameleoonLogger.warning('Condition of unknown type \'%s\' evaluated as false', type)
|
|
12
|
+
false
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
end
|
data/lib/kameleoon/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kameleoon-client-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.22.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kameleoon
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: em-http-request
|
|
@@ -114,6 +114,14 @@ files:
|
|
|
114
114
|
- lib/kameleoon/data/unique_identifier.rb
|
|
115
115
|
- lib/kameleoon/data/user_agent.rb
|
|
116
116
|
- lib/kameleoon/data/visitor_visits.rb
|
|
117
|
+
- lib/kameleoon/events/data_file_update_event.rb
|
|
118
|
+
- lib/kameleoon/events/data_file_update_handler.rb
|
|
119
|
+
- lib/kameleoon/events/event_handler.rb
|
|
120
|
+
- lib/kameleoon/events/event_manager.rb
|
|
121
|
+
- lib/kameleoon/events/event_type.rb
|
|
122
|
+
- lib/kameleoon/events/http_request_failure.rb
|
|
123
|
+
- lib/kameleoon/events/http_request_handler.rb
|
|
124
|
+
- lib/kameleoon/events/request_type.rb
|
|
117
125
|
- lib/kameleoon/exceptions.rb
|
|
118
126
|
- lib/kameleoon/hybrid/manager.rb
|
|
119
127
|
- lib/kameleoon/kameleoon_client.rb
|
|
@@ -141,8 +149,8 @@ files:
|
|
|
141
149
|
- lib/kameleoon/network/response.rb
|
|
142
150
|
- lib/kameleoon/network/uri_helper.rb
|
|
143
151
|
- lib/kameleoon/network/url_provider.rb
|
|
144
|
-
- lib/kameleoon/real_time/real_time_configuration_service.rb
|
|
145
152
|
- lib/kameleoon/real_time/real_time_event.rb
|
|
153
|
+
- lib/kameleoon/real_time/real_time_event_service.rb
|
|
146
154
|
- lib/kameleoon/real_time/sse_client.rb
|
|
147
155
|
- lib/kameleoon/real_time/sse_message.rb
|
|
148
156
|
- lib/kameleoon/real_time/sse_request.rb
|