kameleoon-client-ruby 3.21.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 +119 -31
- 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 +140 -64
- 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,50 +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
|
|
6
|
-
# ClientReadiness
|
|
7
|
-
#
|
|
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.
|
|
8
12
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# read/write lock (whose write lock must be released by the acquiring thread).
|
|
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.
|
|
13
16
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# when the configuration can never be loaded.
|
|
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).
|
|
18
20
|
class ClientReadiness
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
26
|
+
|
|
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
|
|
39
|
+
|
|
40
|
+
# @return [Boolean] whether the state was settled before the timeout elapsed.
|
|
41
|
+
def wait(timeout_second)
|
|
42
|
+
@event.wait(timeout_second)
|
|
43
|
+
end
|
|
22
44
|
end
|
|
45
|
+
private_constant :State
|
|
23
46
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@
|
|
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
|
|
27
53
|
end
|
|
28
54
|
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
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
|
|
85
|
+
end
|
|
86
|
+
end
|
|
35
87
|
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
38
98
|
end
|
|
39
99
|
|
|
40
|
-
#
|
|
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.
|
|
41
108
|
#
|
|
42
|
-
# @param
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 }
|
|
48
136
|
end
|
|
49
137
|
end
|
|
50
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
|
|
@@ -95,25 +105,53 @@ module Kameleoon
|
|
|
95
105
|
##
|
|
96
106
|
# Block until the SDK has loaded its configuration and is ready to use.
|
|
97
107
|
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
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.
|
|
102
117
|
#
|
|
103
118
|
# @param [Integer, nil] timeout_millisecond Maximum time to wait, in
|
|
104
119
|
# milliseconds. When nil or negative, config.default_timeout_millisecond
|
|
105
120
|
# is applied.
|
|
106
|
-
# @return [Boolean] true if the SDK is ready, false if the
|
|
121
|
+
# @return [Boolean] true if the SDK is ready, false if the fetch failed or
|
|
122
|
+
# the timeout elapsed first.
|
|
107
123
|
def wait_init(timeout_millisecond = nil)
|
|
108
124
|
Logging::KameleoonLogger.info('CALL: KameleoonClient.wait_init(timeout_millisecond: %s)', timeout_millisecond)
|
|
109
125
|
if timeout_millisecond.nil? || timeout_millisecond.negative?
|
|
110
126
|
timeout_millisecond = @config.default_timeout_millisecond
|
|
111
127
|
end
|
|
112
|
-
|
|
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?
|
|
113
135
|
Logging::KameleoonLogger.info('RETURN: KameleoonClient.wait_init -> (result: %s)', result)
|
|
114
136
|
result
|
|
115
137
|
end
|
|
116
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
|
+
|
|
117
155
|
##
|
|
118
156
|
# Obtain a visitor code.
|
|
119
157
|
#
|
|
@@ -712,6 +750,24 @@ module Kameleoon
|
|
|
712
750
|
map_active_features
|
|
713
751
|
end
|
|
714
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
|
+
|
|
715
771
|
##
|
|
716
772
|
# The `on_update_configuration()` method allows you to handle the event when configuration
|
|
717
773
|
# has updated data. It takes one input parameter: callable **handler**. The handler
|
|
@@ -719,7 +775,13 @@ module Kameleoon
|
|
|
719
775
|
#
|
|
720
776
|
# @param handler [Callable | NilClass] The handler that will be called when the configuration
|
|
721
777
|
# is updated using a real-time configuration event.
|
|
778
|
+
#
|
|
779
|
+
# DEPRECATED. Please use `set_event_handler(Kameleoon::Events::EventType::DATAFILE_UPDATE, handler)` instead.
|
|
722
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
|
+
)
|
|
723
785
|
Logging::KameleoonLogger.info('CALL/RETURN: KameleoonClient.on_update_configuration(handler)')
|
|
724
786
|
@update_configuration_handler = handler
|
|
725
787
|
end
|
|
@@ -826,44 +888,41 @@ module Kameleoon
|
|
|
826
888
|
|
|
827
889
|
HYBRID_EXPIRATION_TIME = 5
|
|
828
890
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
rescue StandardError => e
|
|
837
|
-
Logging::KameleoonLogger.error('Initial configuration fetch failed: %s', e)
|
|
838
|
-
end
|
|
839
|
-
@readiness.set(ok)
|
|
840
|
-
real_time_update = ok && @data_manager.data_file.settings.real_time_update
|
|
841
|
-
manage_configuration_update(real_time_update)
|
|
842
|
-
end
|
|
843
|
-
end
|
|
844
|
-
|
|
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.
|
|
845
898
|
def fetch_configuration_job(time_stamp = nil)
|
|
846
899
|
Thread.new do
|
|
900
|
+
cause = nil
|
|
847
901
|
ok = false
|
|
848
902
|
begin
|
|
849
903
|
ok = obtain_configuration(time_stamp)
|
|
850
904
|
rescue StandardError => e
|
|
905
|
+
cause = e
|
|
851
906
|
Logging::KameleoonLogger.error('Error occurred during configuration fetching: %s', e)
|
|
852
907
|
end
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
@readiness.set(ok)
|
|
856
|
-
real_time_update = @data_manager.data_file.settings.real_time_update
|
|
857
|
-
if !ok && real_time_update
|
|
858
|
-
@data_manager.data_file.settings.real_time_update = false
|
|
859
|
-
real_time_update = false
|
|
860
|
-
Logging::KameleoonLogger.warning('Switching to polling mode due to failed fetch')
|
|
861
|
-
end
|
|
862
|
-
manage_configuration_update(real_time_update)
|
|
908
|
+
settle_readiness(ok, cause)
|
|
909
|
+
manage_configuration_update(ok && @data_manager.data_file.settings.real_time_update)
|
|
863
910
|
end
|
|
864
911
|
end
|
|
865
912
|
|
|
866
|
-
|
|
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)
|
|
922
|
+
end
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
def start_polling_datafile_update
|
|
867
926
|
return unless @fetch_configuration_update_job.nil?
|
|
868
927
|
|
|
869
928
|
@fetch_configuration_update_job = @scheduler.schedule_every @config.refresh_interval_second do
|
|
@@ -872,7 +931,7 @@ module Kameleoon
|
|
|
872
931
|
end
|
|
873
932
|
end
|
|
874
933
|
|
|
875
|
-
def
|
|
934
|
+
def stop_polling_datafile_update
|
|
876
935
|
return if @fetch_configuration_update_job.nil?
|
|
877
936
|
|
|
878
937
|
@fetch_configuration_update_job&.unschedule
|
|
@@ -880,27 +939,34 @@ module Kameleoon
|
|
|
880
939
|
Logging::KameleoonLogger.info('Scheduled job to fetch configuration is stopped.')
|
|
881
940
|
end
|
|
882
941
|
|
|
883
|
-
def
|
|
884
|
-
return unless @
|
|
942
|
+
def start_real_time_event_service_if_needed
|
|
943
|
+
return unless @real_time_event_service.nil?
|
|
885
944
|
|
|
886
945
|
url = @network_manager.url_provider.make_real_time_url
|
|
887
946
|
fetch_func = proc { |real_time_event| fetch_configuration_job(real_time_event.time_stamp) }
|
|
888
|
-
@
|
|
889
|
-
Kameleoon::RealTime::
|
|
947
|
+
@real_time_event_service =
|
|
948
|
+
Kameleoon::RealTime::RealTimeEventService.new(url, fetch_func)
|
|
890
949
|
end
|
|
891
950
|
|
|
892
|
-
def
|
|
893
|
-
@
|
|
894
|
-
@
|
|
951
|
+
def stop_real_time_event_service_if_needed
|
|
952
|
+
@real_time_event_service&.close
|
|
953
|
+
@real_time_event_service = nil
|
|
895
954
|
end
|
|
896
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.
|
|
897
960
|
def manage_configuration_update(is_real_time_update)
|
|
898
|
-
if
|
|
899
|
-
|
|
900
|
-
|
|
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
|
|
901
967
|
else
|
|
902
|
-
|
|
903
|
-
|
|
968
|
+
stop_real_time_event_service_if_needed
|
|
969
|
+
start_polling_datafile_update
|
|
904
970
|
end
|
|
905
971
|
end
|
|
906
972
|
|
|
@@ -927,34 +993,44 @@ module Kameleoon
|
|
|
927
993
|
if response.configuration
|
|
928
994
|
configuration = JSON.parse(response.configuration)
|
|
929
995
|
data_file = Configuration::DataFile.new(@config.environment, configuration, response.last_modified)
|
|
930
|
-
|
|
931
|
-
|
|
996
|
+
source = time_stamp.nil? ? Events::DataFileUpdateEvent::Source::POLLING
|
|
997
|
+
: Events::DataFileUpdateEvent::Source::STREAMING
|
|
998
|
+
apply_new_configuration(data_file, source)
|
|
932
999
|
Logging::KameleoonLogger.info('Feature flags are fetched: %s', response.inspect)
|
|
933
1000
|
end
|
|
934
1001
|
true
|
|
935
1002
|
end
|
|
936
1003
|
|
|
937
|
-
def apply_new_configuration(data_file)
|
|
938
|
-
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)
|
|
939
1007
|
@data_manager.data_file = data_file
|
|
940
1008
|
@network_manager.url_provider.apply_data_api_domain(data_file.settings.data_api_domain)
|
|
941
|
-
|
|
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)
|
|
942
1013
|
end
|
|
943
1014
|
|
|
944
1015
|
##
|
|
945
|
-
# Call the handler when configuration was updated
|
|
946
|
-
#
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
return if
|
|
950
|
-
|
|
951
|
-
|
|
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
|
|
952
1027
|
end
|
|
953
1028
|
|
|
954
1029
|
def dispose(_object_id = nil)
|
|
955
1030
|
Logging::KameleoonLogger.debug('CALL: KameleoonClient.dispose')
|
|
956
|
-
|
|
957
|
-
|
|
1031
|
+
@disposed = true
|
|
1032
|
+
stop_polling_datafile_update
|
|
1033
|
+
stop_real_time_event_service_if_needed
|
|
958
1034
|
@visitor_manager.stop
|
|
959
1035
|
@tracking_manager.stop
|
|
960
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
|