rox-rollout 5.0.3 → 5.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|