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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d30a63b44e459ce5e10a2b64ce89e3ee59b59ceea67969fda6a1f5b962e43e4
4
- data.tar.gz: 2bc7318f4e7476830c09fc4c9cf3fbcb9b8e21c0ad6bfdc34ed4e74d321eb8a6
3
+ metadata.gz: 17c6afc3c1c54a584ecec20f9213d1bf3d90fa603e4d3f378fd4014fafe7fbbe
4
+ data.tar.gz: cc43890c658daf69fa6e3307ea00a78a6e6d16d94e7f1efc1f7aa8ede5466b2b
5
5
  SHA512:
6
- metadata.gz: 2323d41086d92479bdafae351c9aabce8ad99ec4b870b30fb72ada7a8783bf211dbe6380fe80590d79fdd90d077e935d8961dea30436f3211c37cf2a52bb8da6
7
- data.tar.gz: e3c8ff2bb1018d0d2db497fdbd307a4cac69edee35823f8b35adf47168bfa3339f3ef03401f864f4b918ee83c823e423aa472b44da67ca918f78fcdee02eca53
6
+ metadata.gz: 715c0071fb927d9f8947e47135e1c60036a19ac88ead19d2d8c482821291e04acb4c0adc57027b5e7a6ec305718c68ef64ffe2a5238835045855daf48d1a3075
7
+ data.tar.gz: 2c56942cdf66b5a809970fa9ebabaa20b09e0fedf4a42f824234af9618eab0dd41be7b3c557fe3c597fa3175b35b29440cd74a4f8261d0ef0ffa91d1af651aee
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
+ /.idea/
1
2
  /.bundle/
2
3
  /.yardoc
3
4
  /_yardoc/
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
- configuration_fetched_handler = proc do |e|
27
- if !e.nil? && e.fetcher_status == Rox::Core::FetcherStatus::APPLIED_FROM_NETWORK
28
- TestVars.configuration_fetched_count += 1
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
- impression_handler = proc do |e|
33
- if !e.nil? && !e.reporting_value.nil? && (e.reporting_value.name == 'flag_for_impression')
34
- TestVars.is_impression_raised = true
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
- option = Rox::Server::RoxOptions.new(
40
- configuration_fetched_handler: configuration_fetched_handler,
41
- impression_handler: impression_handler,
42
- dev_mode_key: '67e39e708444aa953414e444',
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 = '611e70975d05440313ccda68'.freeze
8
- #DEV_MODE_SECRET = 'e56cda16749d8d0a9b91d34c'.freeze
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
- #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
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
- nil, !roxy_path.nil?, @user_unhandled_error_invoker)
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
- #return if @analytics_client.nil?
147
+ return if @analytics_client.nil?
142
148
 
143
- #@analytics_client.flush
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)
@@ -4,7 +4,9 @@ module Rox
4
4
  module Core
5
5
  class RoxDouble < RoxString
6
6
  def value(context = nil)
7
- internal_value(context, false, Float)
7
+ return_value = internal_value(context, false, Float)
8
+ send_impressions(return_value, context)
9
+ return_value
8
10
  end
9
11
  end
10
12
  end
@@ -4,7 +4,9 @@ module Rox
4
4
  module Core
5
5
  class RoxInt < RoxString
6
6
  def value(context = nil)
7
- internal_value(context, false, Integer)
7
+ return_value = internal_value(context, false, Integer)
8
+ send_impressions(return_value, context)
9
+ return_value
8
10
  end
9
11
  end
10
12
  end
@@ -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 = (Time.now.to_f * 1000.0).to_i
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
- #call_analytics_gateway
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
@@ -1,3 +1,3 @@
1
1
  module Rox
2
- VERSION = '5.0.3'.freeze
2
+ VERSION = '5.1.0'.freeze
3
3
  end
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.3
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-01-04 00:00:00.000000000 Z
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