rox-rollout 5.0.0 → 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: 26085340d144a78eaae9eb85e07374d912d1fd2e516fdb51f38e940028ca191b
4
- data.tar.gz: 7bc2113d91aeab8e2778a1c0b71d3c1ebd340b902b8b4a48483d5e1a74f59d5f
3
+ metadata.gz: 17c6afc3c1c54a584ecec20f9213d1bf3d90fa603e4d3f378fd4014fafe7fbbe
4
+ data.tar.gz: cc43890c658daf69fa6e3307ea00a78a6e6d16d94e7f1efc1f7aa8ede5466b2b
5
5
  SHA512:
6
- metadata.gz: 4b5385b8da1f783e23b029834f7cb811c8f233ad962aa432447ea5766691610f6cdf63beca6d4d10bd43ee21b8bb2942bd56c78aa97134d6444886f4bbf22b31
7
- data.tar.gz: ec71870af8f708fb7941a7455d130b672951f8119e793fb3590188b61fe61ea8fdff4823c9efb48f51beb2eab269bcabdffcc7f6bd488555da764d9b0eedc60a
6
+ metadata.gz: 715c0071fb927d9f8947e47135e1c60036a19ac88ead19d2d8c482821291e04acb4c0adc57027b5e7a6ec305718c68ef64ffe2a5238835045855daf48d1a3075
7
+ data.tar.gz: 2c56942cdf66b5a809970fa9ebabaa20b09e0fedf4a42f824234af9618eab0dd41be7b3c557fe3c597fa3175b35b29440cd74a4f8261d0ef0ffa91d1af651aee
@@ -8,13 +8,14 @@ jobs:
8
8
  fail-fast: false
9
9
  matrix:
10
10
  os: [ubuntu-latest]
11
- ruby: [2.4, 2.5, 2.6, 2.7, 3.0]
11
+ ruby: [2.4, 2.5, 2.6, 2.7, '3.0', 3.1]
12
12
  runs-on: ${{ matrix.os }}
13
13
  steps:
14
14
  - uses: actions/checkout@v2
15
15
  - uses: ruby/setup-ruby@v1
16
16
  with:
17
17
  ruby-version: ${{ matrix.ruby }}
18
+ bundler: 2.3.3
18
19
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
19
20
  - run: bundle exec rake test
20
21
  - run: bundle exec rake e2e
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
@@ -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