rox-rollout 5.0.3 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/e2e/rox_e2e_test.rb +63 -18
- data/e2e/test_vars.rb +2 -1
- data/e2e-server/.gitignore +1 -0
- data/example/local.rb +7 -7
- data/lib/rox/core/analytics/backoff_policy.rb +51 -0
- data/lib/rox/core/analytics/client.rb +187 -0
- data/lib/rox/core/analytics/defaults.rb +31 -0
- data/lib/rox/core/analytics/logging.rb +62 -0
- data/lib/rox/core/analytics/message_batch.rb +74 -0
- data/lib/rox/core/analytics/response.rb +17 -0
- data/lib/rox/core/analytics/test_queue.rb +58 -0
- data/lib/rox/core/analytics/transport.rb +143 -0
- data/lib/rox/core/analytics/utils.rb +89 -0
- data/lib/rox/core/analytics/worker.rb +67 -0
- data/lib/rox/core/core.rb +15 -3
- data/lib/rox/core/entities/flag.rb +3 -1
- data/lib/rox/core/entities/rox_double.rb +3 -1
- data/lib/rox/core/entities/rox_int.rb +3 -1
- data/lib/rox/core/entities/rox_string.rb +3 -2
- data/lib/rox/core/impression/impression_invoker.rb +4 -6
- data/lib/rox/version.rb +1 -1
- metadata +13 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 17c6afc3c1c54a584ecec20f9213d1bf3d90fa603e4d3f378fd4014fafe7fbbe
|
4
|
+
data.tar.gz: cc43890c658daf69fa6e3307ea00a78a6e6d16d94e7f1efc1f7aa8ede5466b2b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 715c0071fb927d9f8947e47135e1c60036a19ac88ead19d2d8c482821291e04acb4c0adc57027b5e7a6ec305718c68ef64ffe2a5238835045855daf48d1a3075
|
7
|
+
data.tar.gz: 2c56942cdf66b5a809970fa9ebabaa20b09e0fedf4a42f824234af9618eab0dd41be7b3c557fe3c597fa3175b35b29440cd74a4f8261d0ef0ffa91d1af651aee
|
data/.gitignore
CHANGED
data/e2e/rox_e2e_test.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'minitest/autorun'
|
2
2
|
require 'rox/server/rox_options'
|
3
3
|
require 'rox/server/rox_server'
|
4
|
+
require 'rox/server/logging/server_logger'
|
4
5
|
require_relative 'container'
|
5
6
|
require_relative 'custom_props'
|
6
7
|
require_relative 'test_vars'
|
@@ -23,30 +24,58 @@ module E2E
|
|
23
24
|
class RoxE2ETest < Minitest::Test
|
24
25
|
ENV['ROLLOUT_MODE'] = 'QA'
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
27
|
+
@@setupComplete = false
|
28
|
+
@@testsRun = 0
|
29
|
+
|
30
|
+
def setup
|
31
|
+
@@testsRun += 1
|
32
|
+
if !@@setupComplete
|
33
|
+
|
34
|
+
configuration_fetched_handler = proc do |e|
|
35
|
+
if !e.nil? && e.fetcher_status == Rox::Core::FetcherStatus::APPLIED_FROM_NETWORK
|
36
|
+
TestVars.configuration_fetched_count += 1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
impression_handler = proc do |e|
|
41
|
+
puts "targeting: #{e.reporting_value.targeting}"
|
42
|
+
if e.reporting_value.targeting
|
43
|
+
puts "#{Time.now}: flag #{e.reporting_value.name} value is #{e.reporting_value.value}"
|
44
|
+
else
|
45
|
+
puts "#{Time.now}: flag #{e.reporting_value.name} has no conditions, or targeting is off. default value #{!!e.reporting_value.value == e.reporting_value.value ? (e.reporting_value.value ? 'true' : 'false') : e.reporting_value.value} was used"
|
46
|
+
end
|
47
|
+
if !e.nil? && !e.reporting_value.nil? && (e.reporting_value.name == 'flag_for_impression')
|
48
|
+
TestVars.is_impression_raised = true
|
49
|
+
TestVars.impression_raised_times += 1
|
50
|
+
end
|
51
|
+
TestVars.impression_returned_args = e
|
52
|
+
end
|
53
|
+
|
54
|
+
option = Rox::Server::RoxOptions.new(
|
55
|
+
configuration_fetched_handler: configuration_fetched_handler,
|
56
|
+
impression_handler: impression_handler,
|
57
|
+
dev_mode_key: '67e39e708444aa953414e444',
|
58
|
+
logger: Rox::Server::ServerLogger.new
|
59
|
+
)
|
60
|
+
|
61
|
+
@@container = Container.new
|
62
|
+
Rox::Server::RoxServer.register(@@container)
|
63
|
+
CustomProps.create_custom_props
|
64
|
+
Rox::Server::RoxServer.setup('5b82864ebc3aec37aff1fdd5', option).join
|
65
|
+
@@setupComplete = true
|
29
66
|
end
|
30
67
|
end
|
31
68
|
|
32
|
-
|
33
|
-
if
|
34
|
-
|
69
|
+
def teardown
|
70
|
+
if @@testsRun == RoxE2ETest.runnable_methods.length
|
71
|
+
Rox::Server::RoxServer.shutdown
|
35
72
|
end
|
36
|
-
TestVars.impression_returned_args = e
|
37
73
|
end
|
38
74
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
logger: Logger.new
|
44
|
-
)
|
45
|
-
|
46
|
-
@@container = Container.new
|
47
|
-
Rox::Server::RoxServer.register(@@container)
|
48
|
-
CustomProps.create_custom_props
|
49
|
-
Rox::Server::RoxServer.setup('5b82864ebc3aec37aff1fdd5', option).join
|
75
|
+
def before_setup
|
76
|
+
TestVars.is_impression_raised = false
|
77
|
+
TestVars.impression_raised_times = 0
|
78
|
+
end
|
50
79
|
|
51
80
|
def test_simple_flag
|
52
81
|
assert_equal true, @@container.simple_flag.enabled?
|
@@ -135,6 +164,22 @@ module E2E
|
|
135
164
|
assert_equal 'val', TestVars.impression_returned_args.context['var']
|
136
165
|
end
|
137
166
|
|
167
|
+
def test_static_flag_impression_raised_once
|
168
|
+
assert_equal false, TestVars.is_impression_raised
|
169
|
+
assert_equal 0, TestVars.impression_raised_times
|
170
|
+
@@container.flag_for_impression.enabled?
|
171
|
+
assert_equal true, TestVars.is_impression_raised
|
172
|
+
assert_equal 1, TestVars.impression_raised_times
|
173
|
+
end
|
174
|
+
|
175
|
+
def test_dynamic_flag_impression_raised_once
|
176
|
+
assert_equal false, TestVars.is_impression_raised
|
177
|
+
assert_equal 0, TestVars.impression_raised_times
|
178
|
+
Rox::Server::RoxServer::dynamic_api.enabled?('flag_for_impression', true)
|
179
|
+
assert_equal true, TestVars.is_impression_raised
|
180
|
+
assert_equal 1, TestVars.impression_raised_times
|
181
|
+
end
|
182
|
+
|
138
183
|
def test_flag_dependency
|
139
184
|
TestVars.is_prop_for_target_group_for_dependency = true
|
140
185
|
assert_equal true, @@container.flag_for_dependency.enabled?
|
data/e2e/test_vars.rb
CHANGED
@@ -2,7 +2,7 @@ module E2E
|
|
2
2
|
module TestVars
|
3
3
|
class << self
|
4
4
|
attr_accessor :is_computed_boolean_prop_called, :is_computed_string_prop_called, :is_computed_int_prop_called,
|
5
|
-
:is_computed_float_prop_called, :is_computed_semver_prop_called, :target_group1, :target_group2, :is_impression_raised, :is_prop_for_target_group_for_dependency, :configuration_fetched_count, :impression_returned_args
|
5
|
+
:is_computed_float_prop_called, :is_computed_semver_prop_called, :target_group1, :target_group2, :is_impression_raised, :impression_raised_times, :is_prop_for_target_group_for_dependency, :configuration_fetched_count, :impression_returned_args
|
6
6
|
end
|
7
7
|
|
8
8
|
@is_computed_boolean_prop_called = false
|
@@ -13,6 +13,7 @@ module E2E
|
|
13
13
|
@target_group1 = false
|
14
14
|
@target_group2 = false
|
15
15
|
@is_impression_raised = false
|
16
|
+
@impression_raised_times = 0
|
16
17
|
@is_prop_for_target_group_for_dependency = false
|
17
18
|
|
18
19
|
@configuration_fetched_count = 0
|
@@ -0,0 +1 @@
|
|
1
|
+
*.out
|
data/example/local.rb
CHANGED
@@ -4,8 +4,8 @@ require 'rox/server/rox_server'
|
|
4
4
|
require 'rox/server/rox_options'
|
5
5
|
|
6
6
|
API_HOST = 'http://localhost:8557'.freeze
|
7
|
-
APP_KEY = '
|
8
|
-
|
7
|
+
APP_KEY = '600571e330819d4842999e4f'.freeze
|
8
|
+
DEV_MODE_SECRET = 'e56cda16749d8d0a9b91d34c'.freeze
|
9
9
|
|
10
10
|
class Flags
|
11
11
|
attr_accessor :boolean_flag, :string_flag, :int_flag, :double_flag
|
@@ -23,11 +23,11 @@ flags = Flags.new
|
|
23
23
|
Rox::Server::RoxServer.register(flags)
|
24
24
|
|
25
25
|
options = Rox::Server::RoxOptions.new(
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
26
|
+
self_managed_options: Rox::Server::SelfManagedOptions.new(
|
27
|
+
server_url: API_HOST,
|
28
|
+
analytics_url: 'http://127.0.0.1:8787'
|
29
|
+
),
|
30
|
+
dev_mode_key: DEV_MODE_SECRET
|
31
31
|
)
|
32
32
|
|
33
33
|
Rox::Server::RoxServer.setup(APP_KEY, options)
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rox/core/analytics/defaults'
|
2
|
+
|
3
|
+
module Rox
|
4
|
+
module Core
|
5
|
+
class Analytics
|
6
|
+
class BackoffPolicy
|
7
|
+
include Rox::Core::Analytics::Defaults::BackoffPolicy
|
8
|
+
|
9
|
+
# @param [Hash] opts
|
10
|
+
# @option opts [Numeric] :min_timeout_ms The minimum backoff timeout
|
11
|
+
# @option opts [Numeric] :max_timeout_ms The maximum backoff timeout
|
12
|
+
# @option opts [Numeric] :multiplier The value to multiply the current
|
13
|
+
# interval with for each retry attempt
|
14
|
+
# @option opts [Numeric] :randomization_factor The randomization factor
|
15
|
+
# to use to create a range around the retry interval
|
16
|
+
def initialize(opts = {})
|
17
|
+
@min_timeout_ms = opts[:min_timeout_ms] || MIN_TIMEOUT_MS
|
18
|
+
@max_timeout_ms = opts[:max_timeout_ms] || MAX_TIMEOUT_MS
|
19
|
+
@multiplier = opts[:multiplier] || MULTIPLIER
|
20
|
+
@randomization_factor = opts[:randomization_factor] || RANDOMIZATION_FACTOR
|
21
|
+
|
22
|
+
@attempts = 0
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Numeric] the next backoff interval, in milliseconds.
|
26
|
+
def next_interval
|
27
|
+
interval = @min_timeout_ms * (@multiplier ** @attempts)
|
28
|
+
interval = add_jitter(interval, @randomization_factor)
|
29
|
+
|
30
|
+
@attempts += 1
|
31
|
+
|
32
|
+
[interval, @max_timeout_ms].min
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def add_jitter(base, randomization_factor)
|
38
|
+
random_number = rand
|
39
|
+
max_deviation = base * randomization_factor
|
40
|
+
deviation = random_number * max_deviation
|
41
|
+
|
42
|
+
if random_number < 0.5
|
43
|
+
base - deviation
|
44
|
+
else
|
45
|
+
base + deviation
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'time'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
require 'rox/core/analytics/defaults'
|
6
|
+
require 'rox/core/analytics/logging'
|
7
|
+
require 'rox/core/analytics/utils'
|
8
|
+
require 'rox/core/analytics/worker'
|
9
|
+
|
10
|
+
module Rox
|
11
|
+
module Core
|
12
|
+
class Analytics
|
13
|
+
class Client
|
14
|
+
include Rox::Core::Analytics::Utils
|
15
|
+
include Rox::Core::Analytics::Logging
|
16
|
+
|
17
|
+
# @param [Rox::Core::DeviceProperties] device_properties
|
18
|
+
def initialize(device_properties)
|
19
|
+
@queue = Queue.new
|
20
|
+
@max_queue_size = Defaults::Queue::MAX_SIZE
|
21
|
+
@worker_mutex = Mutex.new
|
22
|
+
@worker = Worker.new(@queue, device_properties)
|
23
|
+
@worker_thread = nil
|
24
|
+
|
25
|
+
at_exit { @worker_thread && @worker_thread[:should_exit] = true }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Synchronously waits until the worker has flushed the queue.
|
29
|
+
#
|
30
|
+
# Use only for scripts which are not long-running, and will specifically
|
31
|
+
# exit
|
32
|
+
def flush
|
33
|
+
while !@queue.empty? || @worker.is_requesting?
|
34
|
+
ensure_worker_running
|
35
|
+
sleep(0.1)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @!macro common_attrs
|
40
|
+
# @option attrs [String] :anonymous_id ID for a user when you don't know
|
41
|
+
# who they are yet. (optional but you must provide either an
|
42
|
+
# `anonymous_id` or `user_id`)
|
43
|
+
# @option attrs [Hash] :context ({})
|
44
|
+
# @option attrs [Hash] :integrations What integrations this event
|
45
|
+
# goes to (optional)
|
46
|
+
# @option attrs [String] :message_id ID that uniquely
|
47
|
+
# identifies a message across the API. (optional)
|
48
|
+
# @option attrs [Time] :timestamp When the event occurred (optional)
|
49
|
+
# @option attrs [String] :user_id The ID for this user in your database
|
50
|
+
# (optional but you must provide either an `anonymous_id` or `user_id`)
|
51
|
+
# @option attrs [Hash] :options Options such as user traits (optional)
|
52
|
+
|
53
|
+
# Tracks an event
|
54
|
+
#
|
55
|
+
# @see https://segment.com/docs/sources/server/ruby/#track
|
56
|
+
#
|
57
|
+
# @param [Hash] attrs
|
58
|
+
#
|
59
|
+
# @option attrs [String] :event Event name
|
60
|
+
# @option attrs [Hash] :properties Event properties (optional)
|
61
|
+
# @macro common_attrs
|
62
|
+
def track(attrs)
|
63
|
+
symbolize_keys! attrs
|
64
|
+
enqueue(attrs)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Identifies a user
|
68
|
+
#
|
69
|
+
# @see https://segment.com/docs/sources/server/ruby/#identify
|
70
|
+
#
|
71
|
+
# @param [Hash] attrs
|
72
|
+
#
|
73
|
+
# @option attrs [Hash] :traits User traits (optional)
|
74
|
+
# @macro common_attrs
|
75
|
+
def identify(attrs)
|
76
|
+
symbolize_keys! attrs
|
77
|
+
enqueue(FieldParser.parse_for_identify(attrs))
|
78
|
+
end
|
79
|
+
|
80
|
+
# Aliases a user from one id to another
|
81
|
+
#
|
82
|
+
# @see https://segment.com/docs/sources/server/ruby/#alias
|
83
|
+
#
|
84
|
+
# @param [Hash] attrs
|
85
|
+
#
|
86
|
+
# @option attrs [String] :previous_id The ID to alias from
|
87
|
+
# @macro common_attrs
|
88
|
+
def alias(attrs)
|
89
|
+
symbolize_keys! attrs
|
90
|
+
enqueue(FieldParser.parse_for_alias(attrs))
|
91
|
+
end
|
92
|
+
|
93
|
+
# Associates a user identity with a group.
|
94
|
+
#
|
95
|
+
# @see https://segment.com/docs/sources/server/ruby/#group
|
96
|
+
#
|
97
|
+
# @param [Hash] attrs
|
98
|
+
#
|
99
|
+
# @option attrs [String] :group_id The ID of the group
|
100
|
+
# @option attrs [Hash] :traits User traits (optional)
|
101
|
+
# @macro common_attrs
|
102
|
+
def group(attrs)
|
103
|
+
symbolize_keys! attrs
|
104
|
+
enqueue(FieldParser.parse_for_group(attrs))
|
105
|
+
end
|
106
|
+
|
107
|
+
# Records a page view
|
108
|
+
#
|
109
|
+
# @see https://segment.com/docs/sources/server/ruby/#page
|
110
|
+
#
|
111
|
+
# @param [Hash] attrs
|
112
|
+
#
|
113
|
+
# @option attrs [String] :name Name of the page
|
114
|
+
# @option attrs [Hash] :properties Page properties (optional)
|
115
|
+
# @macro common_attrs
|
116
|
+
def page(attrs)
|
117
|
+
symbolize_keys! attrs
|
118
|
+
enqueue(FieldParser.parse_for_page(attrs))
|
119
|
+
end
|
120
|
+
|
121
|
+
# Records a screen view (for a mobile app)
|
122
|
+
#
|
123
|
+
# @param [Hash] attrs
|
124
|
+
#
|
125
|
+
# @option attrs [String] :name Name of the screen
|
126
|
+
# @option attrs [Hash] :properties Screen properties (optional)
|
127
|
+
# @option attrs [String] :category The screen category (optional)
|
128
|
+
# @macro common_attrs
|
129
|
+
def screen(attrs)
|
130
|
+
symbolize_keys! attrs
|
131
|
+
enqueue(FieldParser.parse_for_screen(attrs))
|
132
|
+
end
|
133
|
+
|
134
|
+
# @return [Fixnum] number of messages in the queue
|
135
|
+
def queued_messages
|
136
|
+
@queue.length
|
137
|
+
end
|
138
|
+
|
139
|
+
def test_queue
|
140
|
+
unless @test
|
141
|
+
raise 'Test queue only available when setting :test to true.'
|
142
|
+
end
|
143
|
+
|
144
|
+
@test_queue ||= TestQueue.new
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
# private: Enqueues the action.
|
150
|
+
#
|
151
|
+
# returns Boolean of whether the item was added to the queue.
|
152
|
+
def enqueue(action)
|
153
|
+
|
154
|
+
if @test
|
155
|
+
test_queue << action
|
156
|
+
return true
|
157
|
+
end
|
158
|
+
|
159
|
+
while @queue.length >= @max_queue_size
|
160
|
+
# remove the oldest impression,
|
161
|
+
# and then add the new one (otherwise it just rejects the newer one)
|
162
|
+
@queue.pop
|
163
|
+
end
|
164
|
+
|
165
|
+
@queue << action
|
166
|
+
ensure_worker_running
|
167
|
+
|
168
|
+
true
|
169
|
+
end
|
170
|
+
|
171
|
+
def ensure_worker_running
|
172
|
+
return if worker_running?
|
173
|
+
@worker_mutex.synchronize do
|
174
|
+
return if worker_running?
|
175
|
+
@worker_thread = Thread.new do
|
176
|
+
@worker.run
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def worker_running?
|
182
|
+
@worker_thread && @worker_thread.alive?
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Rox
|
2
|
+
module Core
|
3
|
+
class Analytics
|
4
|
+
module Defaults
|
5
|
+
module Request
|
6
|
+
RETRIES = 10
|
7
|
+
end
|
8
|
+
|
9
|
+
module Queue
|
10
|
+
MAX_SIZE = 10000
|
11
|
+
end
|
12
|
+
|
13
|
+
module Message
|
14
|
+
MAX_BYTES = 32768 # 32Kb
|
15
|
+
end
|
16
|
+
|
17
|
+
module MessageBatch
|
18
|
+
MAX_BYTES = 512_000 # 500Kb
|
19
|
+
MAX_SIZE = 100
|
20
|
+
end
|
21
|
+
|
22
|
+
module BackoffPolicy
|
23
|
+
MIN_TIMEOUT_MS = 100
|
24
|
+
MAX_TIMEOUT_MS = 10000
|
25
|
+
MULTIPLIER = 1.5
|
26
|
+
RANDOMIZATION_FACTOR = 0.5
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Rox
|
4
|
+
module Core
|
5
|
+
class Analytics
|
6
|
+
# Wraps an existing logger and adds a prefix to all messages
|
7
|
+
class PrefixedLogger
|
8
|
+
def initialize(logger, prefix)
|
9
|
+
@logger = logger
|
10
|
+
@prefix = prefix
|
11
|
+
end
|
12
|
+
|
13
|
+
def debug(msg)
|
14
|
+
@logger.debug("#{@prefix} #{msg}")
|
15
|
+
end
|
16
|
+
|
17
|
+
def info(msg)
|
18
|
+
@logger.info("#{@prefix} #{msg}")
|
19
|
+
end
|
20
|
+
|
21
|
+
def warn(msg)
|
22
|
+
@logger.warn("#{@prefix} #{msg}")
|
23
|
+
end
|
24
|
+
|
25
|
+
def error(msg)
|
26
|
+
@logger.error("#{@prefix} #{msg}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module Logging
|
31
|
+
class << self
|
32
|
+
def logger
|
33
|
+
return @logger if @logger
|
34
|
+
|
35
|
+
base_logger = if defined?(Rails)
|
36
|
+
Rails.logger
|
37
|
+
else
|
38
|
+
logger = Logger.new STDOUT
|
39
|
+
logger.progname = 'Rox::Core::Analytics'
|
40
|
+
logger
|
41
|
+
end
|
42
|
+
@logger = PrefixedLogger.new(base_logger, '[analytics-ruby]')
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_writer :logger
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.included(base)
|
49
|
+
class << base
|
50
|
+
def logger
|
51
|
+
Logging.logger
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def logger
|
57
|
+
Logging.logger
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'rox/core/analytics/logging'
|
3
|
+
|
4
|
+
module Rox
|
5
|
+
module Core
|
6
|
+
class Analytics
|
7
|
+
# A batch of `Message`s to be sent to the API
|
8
|
+
class MessageBatch
|
9
|
+
class JSONGenerationError < StandardError; end
|
10
|
+
|
11
|
+
extend Forwardable
|
12
|
+
include Rox::Core::Analytics::Logging
|
13
|
+
include Rox::Core::Analytics::Defaults::MessageBatch
|
14
|
+
|
15
|
+
def initialize(max_message_count)
|
16
|
+
@messages = []
|
17
|
+
@max_message_count = max_message_count
|
18
|
+
@json_size = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
def <<(message)
|
22
|
+
begin
|
23
|
+
message_json = message.to_json
|
24
|
+
rescue StandardError => e
|
25
|
+
raise JSONGenerationError, "Serialization error: #{e}"
|
26
|
+
end
|
27
|
+
|
28
|
+
message_json_size = message_json.bytesize
|
29
|
+
if message_too_big?(message_json_size)
|
30
|
+
logger.error('a message exceeded the maximum allowed size')
|
31
|
+
else
|
32
|
+
@messages << message
|
33
|
+
@json_size += message_json_size + 1 # One byte for the comma
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def full?
|
38
|
+
item_count_exhausted? || size_exhausted?
|
39
|
+
end
|
40
|
+
|
41
|
+
def clear
|
42
|
+
@messages.clear
|
43
|
+
@json_size = 0
|
44
|
+
end
|
45
|
+
|
46
|
+
def_delegators :@messages, :to_json
|
47
|
+
def_delegators :@messages, :empty?
|
48
|
+
def_delegators :@messages, :length
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def item_count_exhausted?
|
53
|
+
@messages.length >= @max_message_count
|
54
|
+
end
|
55
|
+
|
56
|
+
def message_too_big?(message_json_size)
|
57
|
+
message_json_size > Defaults::Message::MAX_BYTES
|
58
|
+
end
|
59
|
+
|
60
|
+
# We consider the max size here as just enough to leave room for one more
|
61
|
+
# message of the largest size possible. This is a shortcut that allows us
|
62
|
+
# to use a native Ruby `Queue` that doesn't allow peeking. The tradeoff
|
63
|
+
# here is that we might fit in less messages than possible into a batch.
|
64
|
+
#
|
65
|
+
# The alternative is to use our own `Queue` implementation that allows
|
66
|
+
# peeking, and to consider the next message size when calculating whether
|
67
|
+
# the message can be accomodated in this batch.
|
68
|
+
def size_exhausted?
|
69
|
+
@json_size >= (MAX_BYTES - Defaults::Message::MAX_BYTES)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Rox
|
2
|
+
module Core
|
3
|
+
class Analytics
|
4
|
+
class Response
|
5
|
+
attr_reader :status, :error
|
6
|
+
|
7
|
+
# public: Simple class to wrap responses from the API
|
8
|
+
#
|
9
|
+
#
|
10
|
+
def initialize(status = 200, error = nil)
|
11
|
+
@status = status
|
12
|
+
@error = error
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Rox
|
2
|
+
module Core
|
3
|
+
class Analytics
|
4
|
+
class TestQueue
|
5
|
+
attr_reader :messages
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
reset!
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](key)
|
12
|
+
all[key]
|
13
|
+
end
|
14
|
+
|
15
|
+
def count
|
16
|
+
all.count
|
17
|
+
end
|
18
|
+
|
19
|
+
def <<(message)
|
20
|
+
all << message
|
21
|
+
send(message[:type]) << message
|
22
|
+
end
|
23
|
+
|
24
|
+
def alias
|
25
|
+
messages[:alias] ||= []
|
26
|
+
end
|
27
|
+
|
28
|
+
def all
|
29
|
+
messages[:all] ||= []
|
30
|
+
end
|
31
|
+
|
32
|
+
def group
|
33
|
+
messages[:group] ||= []
|
34
|
+
end
|
35
|
+
|
36
|
+
def identify
|
37
|
+
messages[:identify] ||= []
|
38
|
+
end
|
39
|
+
|
40
|
+
def page
|
41
|
+
messages[:page] ||= []
|
42
|
+
end
|
43
|
+
|
44
|
+
def screen
|
45
|
+
messages[:screen] ||= []
|
46
|
+
end
|
47
|
+
|
48
|
+
def track
|
49
|
+
messages[:track] ||= []
|
50
|
+
end
|
51
|
+
|
52
|
+
def reset!
|
53
|
+
@messages = {}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'rox/core/analytics/defaults'
|
2
|
+
require 'rox/core/analytics/utils'
|
3
|
+
require 'rox/core/analytics/response'
|
4
|
+
require 'rox/core/analytics/logging'
|
5
|
+
require 'rox/core/analytics/backoff_policy'
|
6
|
+
require 'net/http'
|
7
|
+
require 'net/https'
|
8
|
+
require 'json'
|
9
|
+
|
10
|
+
module Rox
|
11
|
+
module Core
|
12
|
+
class Analytics
|
13
|
+
class Transport
|
14
|
+
include Rox::Core::Analytics::Defaults::Request
|
15
|
+
include Rox::Core::Analytics::Utils
|
16
|
+
include Rox::Core::Analytics::Logging
|
17
|
+
|
18
|
+
def initialize(device_properties)
|
19
|
+
@device_properties = device_properties
|
20
|
+
uri = URI.parse(Rox::Core::Environment.analytics_path)
|
21
|
+
@headers = {
|
22
|
+
'Accept' => 'application/json',
|
23
|
+
'Content-Type' => 'application/json',
|
24
|
+
'User-Agent' => "ruby/#{device_properties.lib_version}"
|
25
|
+
}
|
26
|
+
@path = uri.path + '/impression/' + device_properties.rollout_key
|
27
|
+
@retries = RETRIES
|
28
|
+
@backoff_policy = Rox::Core::Analytics::BackoffPolicy.new
|
29
|
+
|
30
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
31
|
+
http.use_ssl = uri.scheme == 'https'
|
32
|
+
http.read_timeout = 8
|
33
|
+
http.open_timeout = 4
|
34
|
+
|
35
|
+
@http = http
|
36
|
+
end
|
37
|
+
|
38
|
+
# Sends a batch of messages to the API
|
39
|
+
#
|
40
|
+
# @return [Response] API response
|
41
|
+
def send(batch)
|
42
|
+
logger.debug("Sending request for #{batch.length} items")
|
43
|
+
|
44
|
+
last_response, exception = retry_with_backoff(@retries) do
|
45
|
+
status_code, body = send_request(batch)
|
46
|
+
should_retry = should_retry_request?(status_code, body)
|
47
|
+
logger.debug("Response status code: #{status_code}")
|
48
|
+
logger.debug("Response error: #{body}") if status_code != 200
|
49
|
+
|
50
|
+
[Response.new(status_code, body), should_retry]
|
51
|
+
end
|
52
|
+
|
53
|
+
if exception
|
54
|
+
logger.error(exception.message)
|
55
|
+
exception.backtrace.each { |line| logger.error(line) }
|
56
|
+
Response.new(-1, exception.to_s)
|
57
|
+
else
|
58
|
+
last_response
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Closes a persistent connection if it exists
|
63
|
+
def shutdown
|
64
|
+
@http.finish if @http.started?
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def should_retry_request?(status_code, body)
|
70
|
+
if status_code >= 500
|
71
|
+
true # Server error
|
72
|
+
elsif status_code == 429
|
73
|
+
true # Rate limited
|
74
|
+
elsif status_code >= 400
|
75
|
+
logger.error(body)
|
76
|
+
false # Client error. Do not retry, but log
|
77
|
+
else
|
78
|
+
false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Takes a block that returns [result, should_retry].
|
83
|
+
#
|
84
|
+
# Retries upto `retries_remaining` times, if `should_retry` is false or
|
85
|
+
# an exception is raised. `@backoff_policy` is used to determine the
|
86
|
+
# duration to sleep between attempts
|
87
|
+
#
|
88
|
+
# Returns [last_result, raised_exception]
|
89
|
+
def retry_with_backoff(retries_remaining, &block)
|
90
|
+
result, caught_exception = nil
|
91
|
+
should_retry = false
|
92
|
+
|
93
|
+
begin
|
94
|
+
result, should_retry = yield
|
95
|
+
return [result, nil] unless should_retry
|
96
|
+
rescue StandardError => e
|
97
|
+
should_retry = true
|
98
|
+
caught_exception = e
|
99
|
+
end
|
100
|
+
|
101
|
+
if should_retry && (retries_remaining > 1)
|
102
|
+
logger.debug("Retrying request, #{retries_remaining} retries left")
|
103
|
+
sleep(@backoff_policy.next_interval.to_f / 1000)
|
104
|
+
retry_with_backoff(retries_remaining - 1, &block)
|
105
|
+
else
|
106
|
+
[result, caught_exception]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Sends a request for the batch, returns [status_code, body]
|
111
|
+
def send_request(batch)
|
112
|
+
payload = JSON.generate(
|
113
|
+
:analyticsVersion => '1.0.0',
|
114
|
+
:sdkVersion => @device_properties.lib_version,
|
115
|
+
:time => DateTime.now.strftime('%Q').to_i,
|
116
|
+
:platform => @device_properties.all_properties[PropertyType::PLATFORM.name],
|
117
|
+
:rolloutKey => @device_properties.rollout_key,
|
118
|
+
:events => batch
|
119
|
+
)
|
120
|
+
request = Net::HTTP::Post.new(@path, @headers)
|
121
|
+
|
122
|
+
if self.class.stub
|
123
|
+
logger.debug "stubbed request to #{@path}: #{JSON.generate(batch)}"
|
124
|
+
|
125
|
+
[200, '{}']
|
126
|
+
else
|
127
|
+
@http.start unless @http.started? # Maintain a persistent connection
|
128
|
+
response = @http.request(request, payload)
|
129
|
+
[response.code.to_i, response.body]
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
class << self
|
134
|
+
attr_writer :stub
|
135
|
+
|
136
|
+
def stub
|
137
|
+
@stub || ENV['STUB']
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module Rox
|
4
|
+
module Core
|
5
|
+
class Analytics
|
6
|
+
module Utils
|
7
|
+
extend self
|
8
|
+
|
9
|
+
# public: Return a new hash with keys converted from strings to symbols
|
10
|
+
#
|
11
|
+
def symbolize_keys(hash)
|
12
|
+
hash.each_with_object({}) do |(k, v), memo|
|
13
|
+
memo[k.to_sym] = v
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# public: Convert hash keys from strings to symbols in place
|
18
|
+
#
|
19
|
+
def symbolize_keys!(hash)
|
20
|
+
hash.replace symbolize_keys hash
|
21
|
+
end
|
22
|
+
|
23
|
+
# public: Return a new hash with keys as strings
|
24
|
+
#
|
25
|
+
def stringify_keys(hash)
|
26
|
+
hash.each_with_object({}) do |(k, v), memo|
|
27
|
+
memo[k.to_s] = v
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# public: Returns a new hash with all the date values in the into iso8601
|
32
|
+
# strings
|
33
|
+
#
|
34
|
+
def isoify_dates(hash)
|
35
|
+
hash.each_with_object({}) do |(k, v), memo|
|
36
|
+
memo[k] = datetime_in_iso8601(v)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# public: Converts all the date values in the into iso8601 strings in place
|
41
|
+
#
|
42
|
+
def isoify_dates!(hash)
|
43
|
+
hash.replace isoify_dates hash
|
44
|
+
end
|
45
|
+
|
46
|
+
# public: Returns a uid string
|
47
|
+
#
|
48
|
+
def uid
|
49
|
+
arr = SecureRandom.random_bytes(16).unpack('NnnnnN')
|
50
|
+
arr[2] = (arr[2] & 0x0fff) | 0x4000
|
51
|
+
arr[3] = (arr[3] & 0x3fff) | 0x8000
|
52
|
+
'%08x-%04x-%04x-%04x-%04x%08x' % arr
|
53
|
+
end
|
54
|
+
|
55
|
+
def datetime_in_iso8601(datetime)
|
56
|
+
case datetime
|
57
|
+
when Time
|
58
|
+
time_in_iso8601 datetime
|
59
|
+
when DateTime
|
60
|
+
time_in_iso8601 datetime.to_time
|
61
|
+
when Date
|
62
|
+
date_in_iso8601 datetime
|
63
|
+
else
|
64
|
+
datetime
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def time_in_iso8601(time)
|
69
|
+
"#{time.strftime('%Y-%m-%dT%H:%M:%S.%6N')}#{formatted_offset(time, true, 'Z')}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def date_in_iso8601(date)
|
73
|
+
date.strftime('%F')
|
74
|
+
end
|
75
|
+
|
76
|
+
def formatted_offset(time, colon = true, alternate_utc_string = nil)
|
77
|
+
time.utc? && alternate_utc_string || seconds_to_utc_offset(time.utc_offset, colon)
|
78
|
+
end
|
79
|
+
|
80
|
+
def seconds_to_utc_offset(seconds, colon = true)
|
81
|
+
(colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON) % [(seconds < 0 ? '-' : '+'), (seconds.abs / 3600), ((seconds.abs % 3600) / 60)]
|
82
|
+
end
|
83
|
+
|
84
|
+
UTC_OFFSET_WITH_COLON = '%s%02d:%02d'
|
85
|
+
UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.sub(':', '')
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'rox/core/analytics/defaults'
|
2
|
+
require 'rox/core/analytics/message_batch'
|
3
|
+
require 'rox/core/analytics/transport'
|
4
|
+
require 'rox/core/analytics/utils'
|
5
|
+
|
6
|
+
module Rox
|
7
|
+
module Core
|
8
|
+
class Analytics
|
9
|
+
class Worker
|
10
|
+
include Rox::Core::Analytics::Utils
|
11
|
+
include Rox::Core::Analytics::Defaults
|
12
|
+
include Rox::Core::Analytics::Logging
|
13
|
+
|
14
|
+
# public: Creates a new worker
|
15
|
+
#
|
16
|
+
# The worker continuously takes messages off the queue
|
17
|
+
# and makes requests to the segment.io api
|
18
|
+
#
|
19
|
+
# queue - Queue synchronized between client and worker
|
20
|
+
# @param [Rox::Core::DeviceProperties] device_properties
|
21
|
+
#
|
22
|
+
def initialize(queue, device_properties)
|
23
|
+
@queue = queue
|
24
|
+
@device_properties = device_properties
|
25
|
+
@on_error = proc { |status, error| }
|
26
|
+
batch_size = Defaults::MessageBatch::MAX_SIZE
|
27
|
+
@batch = MessageBatch.new(batch_size)
|
28
|
+
@lock = Mutex.new
|
29
|
+
@transport = Transport.new(device_properties)
|
30
|
+
end
|
31
|
+
|
32
|
+
# public: Continuously runs the loop to check for new events
|
33
|
+
#
|
34
|
+
def run
|
35
|
+
until Thread.current[:should_exit]
|
36
|
+
return if @queue.empty?
|
37
|
+
|
38
|
+
@lock.synchronize do
|
39
|
+
consume_message_from_queue! until @batch.full? || @queue.empty?
|
40
|
+
end
|
41
|
+
|
42
|
+
res = @transport.send @batch
|
43
|
+
@on_error.call(res.status, res.error) unless res.status == 200
|
44
|
+
|
45
|
+
@lock.synchronize { @batch.clear }
|
46
|
+
end
|
47
|
+
ensure
|
48
|
+
@transport.shutdown
|
49
|
+
end
|
50
|
+
|
51
|
+
# public: Check whether we have outstanding requests.
|
52
|
+
#
|
53
|
+
def is_requesting?
|
54
|
+
@lock.synchronize { !@batch.empty? }
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def consume_message_from_queue!
|
60
|
+
@batch << @queue.pop
|
61
|
+
rescue MessageBatch::JSONGenerationError => e
|
62
|
+
@on_error.call(-1, e.to_s)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/rox/core/core.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'rox/core/analytics/client'
|
1
2
|
require 'rox/core/repositories/flag_repository'
|
2
3
|
require 'rox/core/repositories/custom_property_repository'
|
3
4
|
require 'rox/core/repositories/target_group_repository'
|
@@ -48,6 +49,7 @@ module Rox
|
|
48
49
|
@last_configurations = nil
|
49
50
|
@internal_flags = nil
|
50
51
|
@push_updates_listener = nil
|
52
|
+
@analytics_client = nil
|
51
53
|
end
|
52
54
|
|
53
55
|
def userspace_unhandled_error_handler=(handler)
|
@@ -71,9 +73,13 @@ module Rox
|
|
71
73
|
# TODO: Analytics.Analytics.Initialize(deviceProperties.RolloutKey, deviceProperties)
|
72
74
|
@internal_flags = InternalFlags.new(@experiment_repository, @parser, @rox_options)
|
73
75
|
|
76
|
+
if !@rox_options.self_managed? && roxy_path.nil?
|
77
|
+
@analytics_client = create_analytics_client(device_properties)
|
78
|
+
end
|
79
|
+
|
74
80
|
# TODO: impressionInvoker = new ImpressionInvoker(internalFlags, customPropertyRepository, deviceProperties, Analytics.Analytics.Client, roxyPath != null);
|
75
81
|
@impression_invoker = ImpressionInvoker.new(@internal_flags, @custom_property_repository, device_properties,
|
76
|
-
|
82
|
+
@analytics_client, !roxy_path.nil?, @user_unhandled_error_invoker)
|
77
83
|
@flag_setter = FlagSetter.new(@flag_repository, @parser, @experiment_repository, @impression_invoker)
|
78
84
|
buid = BUID.new(sdk_settings, device_properties, @flag_repository, @custom_property_repository)
|
79
85
|
|
@@ -138,9 +144,9 @@ module Rox
|
|
138
144
|
@push_updates_listener = nil
|
139
145
|
end
|
140
146
|
|
141
|
-
|
147
|
+
return if @analytics_client.nil?
|
142
148
|
|
143
|
-
|
149
|
+
@analytics_client.flush
|
144
150
|
end
|
145
151
|
|
146
152
|
def fetch
|
@@ -224,6 +230,12 @@ module Rox
|
|
224
230
|
raise ArgumentError, 'Illegal Rollout api key'
|
225
231
|
end
|
226
232
|
end
|
233
|
+
|
234
|
+
private
|
235
|
+
|
236
|
+
def create_analytics_client(device_properties)
|
237
|
+
Rox::Core::Analytics::Client.new(device_properties)
|
238
|
+
end
|
227
239
|
end
|
228
240
|
end
|
229
241
|
end
|
@@ -26,7 +26,9 @@ module Rox
|
|
26
26
|
|
27
27
|
def value(context = nil)
|
28
28
|
merged_context = MergedContext.new(@parser&.global_context, context)
|
29
|
-
internal_value(merged_context, false)
|
29
|
+
return_value = internal_value(merged_context, false)
|
30
|
+
send_impressions(return_value, merged_context)
|
31
|
+
return_value
|
30
32
|
end
|
31
33
|
|
32
34
|
def internal_enabled?(context, nil_instead_of_default = false)
|
@@ -43,7 +43,9 @@ module Rox
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def value(context = nil)
|
46
|
-
internal_value(context, false)
|
46
|
+
return_value = internal_value(context, false)
|
47
|
+
send_impressions(return_value, context)
|
48
|
+
return_value
|
47
49
|
end
|
48
50
|
|
49
51
|
def internal_value(context, nil_instead_of_default, evaluated_type = String)
|
@@ -59,7 +61,6 @@ module Rox
|
|
59
61
|
end
|
60
62
|
end
|
61
63
|
|
62
|
-
send_impressions(return_value, merged_context)
|
63
64
|
return_value
|
64
65
|
end
|
65
66
|
end
|
@@ -18,7 +18,6 @@ module Rox
|
|
18
18
|
@mutex = Mutex.new
|
19
19
|
end
|
20
20
|
|
21
|
-
# TODO: write analytics client and initiate it before using it
|
22
21
|
def call_analytics_gateway(reporting_value, stickiness_property, context)
|
23
22
|
begin
|
24
23
|
analytics_enabled = @internal_flags.enabled?('rox.internal.analytics')
|
@@ -30,9 +29,9 @@ module Rox
|
|
30
29
|
distinct_id = prop_value if prop_value.instance_of?(String)
|
31
30
|
end
|
32
31
|
|
33
|
-
event_time =
|
32
|
+
event_time = DateTime.now.strftime('%Q').to_i
|
34
33
|
begin
|
35
|
-
event_time = ENV['rox.analytics.ms'].to_i
|
34
|
+
event_time = ENV['rox.analytics.ms'].to_i if ENV['rox.analytics.ms']
|
36
35
|
rescue StandardError
|
37
36
|
end
|
38
37
|
|
@@ -40,7 +39,6 @@ module Rox
|
|
40
39
|
flag: reporting_value.name,
|
41
40
|
value: reporting_value.value,
|
42
41
|
distinctId: distinct_id,
|
43
|
-
experimentVersion: '0',
|
44
42
|
type: 'IMPRESSION',
|
45
43
|
time: event_time
|
46
44
|
})
|
@@ -49,9 +47,9 @@ module Rox
|
|
49
47
|
Logging.logger.error('Failed to send analytics', ex)
|
50
48
|
end
|
51
49
|
end
|
52
|
-
|
50
|
+
|
53
51
|
def invoke(reporting_value, stickiness_property, context)
|
54
|
-
|
52
|
+
call_analytics_gateway(reporting_value, stickiness_property, context)
|
55
53
|
raise_impression_event(ImpressionArgs.new(reporting_value, context))
|
56
54
|
end
|
57
55
|
|
data/lib/rox/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rox-rollout
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.0
|
4
|
+
version: 5.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- CloudBees
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-04-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: em-eventsource
|
@@ -126,6 +126,7 @@ files:
|
|
126
126
|
- Rakefile
|
127
127
|
- bin/console
|
128
128
|
- bin/setup
|
129
|
+
- e2e-server/.gitignore
|
129
130
|
- e2e-server/run_server.sh
|
130
131
|
- e2e-server/server.rb
|
131
132
|
- e2e/container.rb
|
@@ -134,6 +135,16 @@ files:
|
|
134
135
|
- e2e/test_vars.rb
|
135
136
|
- example/local.rb
|
136
137
|
- lib/rox.rb
|
138
|
+
- lib/rox/core/analytics/backoff_policy.rb
|
139
|
+
- lib/rox/core/analytics/client.rb
|
140
|
+
- lib/rox/core/analytics/defaults.rb
|
141
|
+
- lib/rox/core/analytics/logging.rb
|
142
|
+
- lib/rox/core/analytics/message_batch.rb
|
143
|
+
- lib/rox/core/analytics/response.rb
|
144
|
+
- lib/rox/core/analytics/test_queue.rb
|
145
|
+
- lib/rox/core/analytics/transport.rb
|
146
|
+
- lib/rox/core/analytics/utils.rb
|
147
|
+
- lib/rox/core/analytics/worker.rb
|
137
148
|
- lib/rox/core/client/buid.rb
|
138
149
|
- lib/rox/core/client/device_properties.rb
|
139
150
|
- lib/rox/core/client/dynamic_api.rb
|