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 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