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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea79d87e992d2f5bf9829c8082427c18251101be90f50c769b46242a1f4fc272
4
- data.tar.gz: 305d07e5adee2b86cf58acc2b01f8a47f4161daeb06efbc8239e7a585ec355cd
3
+ metadata.gz: 7f492badef67f0b4e09561d3a3d423e4497dbd7efe22d07641cac9485fe9e100
4
+ data.tar.gz: 95f7268b78ecd64bd4707c8fc8581251b8f07038a32eb044aebdb0b1cab652b6
5
5
  SHA512:
6
- metadata.gz: c3319b271cf4053f01e9fab40b0133cd609a800f55fd3de84f19b76209d2fcbe5438672aa24a42dc39c1dd4dade97641956c2cff1f5c5c0cee8ece0d80636ca8
7
- data.tar.gz: 10dfdd35cf97d0adb20ed4a866c4b6739be6ec11458c267a550bf02f6e5ae2542b282b2c30ac33ae94797e3e4a8ce8dd6187406ef1d85603c2d02d0b9f09d44f
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 is a cross-thread latch signalling that the SDK has
7
- # successfully loaded its configuration.
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
- # It is created on the thread that builds the client but released (`set`) from
10
- # the background thread(s) performing configuration fetches, so it relies on
11
- # Concurrent::Event (which allows cross-thread release) rather than a
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
- # A failed fetch does NOT release the latch: the client keeps retrying in the
15
- # background and the latch is released as soon as any fetch succeeds. Callers
16
- # should bound their `wait` with a timeout so they are never blocked forever
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
- def initialize
20
- @success = false
21
- @event = Concurrent::Event.new
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
- # @return [Boolean] whether the SDK has become ready (configuration loaded).
25
- def success
26
- @success
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
- # Record a fetch outcome. A successful fetch marks the client ready and
30
- # unblocks all waiting threads; a failed fetch is ignored so that a later
31
- # successful retry can still release the latch. Idempotent and safe to call
32
- # from any thread.
33
- def set(success)
34
- return unless success
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
- @success = true
37
- @event.set
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
- # Block until the SDK becomes ready or the timeout elapses.
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 timeout [Numeric, nil] maximum number of seconds to wait;
43
- # nil waits indefinitely.
44
- # @return [Boolean] whether the SDK is ready.
45
- def wait(timeout = nil)
46
- @event.wait(timeout)
47
- @success
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
@@ -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/real_time_configuration_service'
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
- @real_time_configuration_service = nil
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
- # If the initial fetch fails the client keeps retrying in the background, so
99
- # this method returns as soon as any fetch succeeds. The wait is always
100
- # bounded by a timeout, so it never blocks indefinitely: when the timeout
101
- # elapses before the configuration is loaded, the method returns false.
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 timeout elapsed first.
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
- result = @readiness.wait(timeout_millisecond / 1000.0)
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
- def fetch_configuration_initially
830
- Logging::KameleoonLogger.info('Initial configuration fetch is started.')
831
- Thread.new do
832
- ok = false
833
- begin
834
- ok = obtain_configuration
835
- Logging::KameleoonLogger.error('Initial configuration fetch failed') unless ok
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
- # Release any threads still waiting in `wait_init` once a (re)fetch
854
- # finally succeeds after an unsuccessful initial fetch.
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
- def start_configuration_update_job_if_needed
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 stop_configuration_update_job_if_needed
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 start_real_time_configuration_service_if_needed
884
- return unless @real_time_configuration_service.nil?
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
- @real_time_configuration_service =
889
- Kameleoon::RealTime::RealTimeConfigurationService.new(url, fetch_func)
947
+ @real_time_event_service =
948
+ Kameleoon::RealTime::RealTimeEventService.new(url, fetch_func)
890
949
  end
891
950
 
892
- def stop_real_time_configuration_service_if_needed
893
- @real_time_configuration_service&.close
894
- @real_time_configuration_service = nil
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 is_real_time_update
899
- stop_configuration_update_job_if_needed
900
- start_real_time_configuration_service_if_needed
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
- stop_real_time_configuration_service_if_needed
903
- start_configuration_update_job_if_needed
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
- apply_new_configuration(data_file)
931
- call_update_handler_if_needed(!time_stamp.nil?)
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)', data_file)
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
- Logging::KameleoonLogger.debug('RETURN: KameleoonClient.apply_new_configuration(data_file: %s)', data_file)
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 with new time stamp.
946
- #
947
- # @param need_call [Bool] Indicates if we need to call handler or not.
948
- def call_update_handler_if_needed(need_call)
949
- return if !need_call || @update_configuration_handler.nil?
950
-
951
- @update_configuration_handler.call
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
- stop_configuration_update_job_if_needed
957
- stop_real_time_configuration_service_if_needed
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(:fetch_configuration_initially)
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 = 3
18
- TRACKING_CALL_ATTEMPT_NUMBER = 3
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, FETCH_CONFIGURATION_ATTEMPT_NUMBER - 1)
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, TRACKING_CALL_ATTEMPT_NUMBER - 1, TRACKING_CALL_RETRY_DELAY))
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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Kameleoon Real Time Configuration Service
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
- CONFIGURATION_UPDATE_EVENT = 'configuration-update-event'
13
+ DATA_FILE_UPDATE_EVENT = 'configuration-update-event'
14
14
 
15
15
  ##
16
- # RealTimeConfigurationService is used for fetching updates of configuration
16
+ # RealTimeEventService is used for fetching updates of configuration
17
17
  # (experiments and feature flags) in real time.
18
- class RealTimeConfigurationService
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
- def initialize(url, update_handler, sse_request_source = nil)
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 configuration service is shutting down')
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 == CONFIGURATION_UPDATE_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 (true)
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 true', type)
12
- true
11
+ Logging::KameleoonLogger.warning('Condition of unknown type \'%s\' evaluated as false', type)
12
+ false
13
13
  end
14
14
  end
15
15
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kameleoon
4
- SDK_VERSION = '3.21.0'
4
+ SDK_VERSION = '3.22.0'
5
5
  SDK_NAME = 'RUBY'
6
6
  end
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.21.0
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-07-02 00:00:00.000000000 Z
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