rox-rollout 5.0.3 → 5.1.1

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/e2e_tests.yaml +52 -0
  3. data/.github/workflows/{ruby.yml → unit_tests.yaml} +4 -5
  4. data/.gitignore +1 -0
  5. data/Rakefile +0 -6
  6. data/e2e-server/.gitignore +1 -0
  7. data/e2e-server/run_server.sh +7 -3
  8. data/e2e-server/server.rb +26 -56
  9. data/example/local.rb +7 -7
  10. data/lib/rox/core/analytics/backoff_policy.rb +51 -0
  11. data/lib/rox/core/analytics/client.rb +187 -0
  12. data/lib/rox/core/analytics/defaults.rb +31 -0
  13. data/lib/rox/core/analytics/logging.rb +62 -0
  14. data/lib/rox/core/analytics/message_batch.rb +74 -0
  15. data/lib/rox/core/analytics/response.rb +17 -0
  16. data/lib/rox/core/analytics/test_queue.rb +58 -0
  17. data/lib/rox/core/analytics/transport.rb +144 -0
  18. data/lib/rox/core/analytics/utils.rb +89 -0
  19. data/lib/rox/core/analytics/worker.rb +67 -0
  20. data/lib/rox/core/consts/environment.rb +62 -54
  21. data/lib/rox/core/core.rb +19 -9
  22. data/lib/rox/core/entities/flag.rb +3 -1
  23. data/lib/rox/core/entities/rox_double.rb +3 -1
  24. data/lib/rox/core/entities/rox_int.rb +3 -1
  25. data/lib/rox/core/entities/rox_string.rb +3 -2
  26. data/lib/rox/core/impression/impression_invoker.rb +4 -6
  27. data/lib/rox/core/network/request_configuration_builder.rb +1 -1
  28. data/lib/rox/core/network/response.rb +2 -2
  29. data/lib/rox/core/network/state_sender.rb +1 -1
  30. data/lib/rox/server/network_configurations_options.rb +20 -0
  31. data/lib/rox/server/rox_options.rb +3 -9
  32. data/lib/rox/server/self_managed_options.rb +12 -0
  33. data/lib/rox/version.rb +1 -1
  34. data/rox.gemspec +2 -2
  35. metadata +25 -16
  36. data/.circleci/config.yml +0 -73
  37. data/e2e/container.rb +0 -35
  38. data/e2e/custom_props.rb +0 -55
  39. data/e2e/rox_e2e_test.rb +0 -157
  40. data/e2e/test_vars.rb +0 -21
@@ -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,144 @@
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
+ logger.debug("Using Analytics url #{uri}")
22
+ @headers = {
23
+ 'Accept' => 'application/json',
24
+ 'Content-Type' => 'application/json',
25
+ 'User-Agent' => "ruby/#{device_properties.lib_version}"
26
+ }
27
+ @path = uri.path + '/impression/' + device_properties.rollout_key
28
+ @retries = RETRIES
29
+ @backoff_policy = Rox::Core::Analytics::BackoffPolicy.new
30
+
31
+ http = Net::HTTP.new(uri.host, uri.port)
32
+ http.use_ssl = uri.scheme == 'https'
33
+ http.read_timeout = 8
34
+ http.open_timeout = 4
35
+
36
+ @http = http
37
+ end
38
+
39
+ # Sends a batch of messages to the API
40
+ #
41
+ # @return [Response] API response
42
+ def send(batch)
43
+ logger.debug("Sending request for #{batch.length} items")
44
+
45
+ last_response, exception = retry_with_backoff(@retries) do
46
+ status_code, body = send_request(batch)
47
+ should_retry = should_retry_request?(status_code, body)
48
+ logger.debug("Response status code: #{status_code}")
49
+ logger.debug("Response error: #{body}") if status_code != 200
50
+
51
+ [Response.new(status_code, body), should_retry]
52
+ end
53
+
54
+ if exception
55
+ logger.error(exception.message)
56
+ exception.backtrace.each { |line| logger.error(line) }
57
+ Response.new(-1, exception.to_s)
58
+ else
59
+ last_response
60
+ end
61
+ end
62
+
63
+ # Closes a persistent connection if it exists
64
+ def shutdown
65
+ @http.finish if @http.started?
66
+ end
67
+
68
+ private
69
+
70
+ def should_retry_request?(status_code, body)
71
+ if status_code >= 500
72
+ true # Server error
73
+ elsif status_code == 429
74
+ true # Rate limited
75
+ elsif status_code >= 400
76
+ logger.error(body)
77
+ false # Client error. Do not retry, but log
78
+ else
79
+ false
80
+ end
81
+ end
82
+
83
+ # Takes a block that returns [result, should_retry].
84
+ #
85
+ # Retries upto `retries_remaining` times, if `should_retry` is false or
86
+ # an exception is raised. `@backoff_policy` is used to determine the
87
+ # duration to sleep between attempts
88
+ #
89
+ # Returns [last_result, raised_exception]
90
+ def retry_with_backoff(retries_remaining, &block)
91
+ result, caught_exception = nil
92
+ should_retry = false
93
+
94
+ begin
95
+ result, should_retry = yield
96
+ return [result, nil] unless should_retry
97
+ rescue StandardError => e
98
+ should_retry = true
99
+ caught_exception = e
100
+ end
101
+
102
+ if should_retry && (retries_remaining > 1)
103
+ logger.debug("Retrying request, #{retries_remaining} retries left")
104
+ sleep(@backoff_policy.next_interval.to_f / 1000)
105
+ retry_with_backoff(retries_remaining - 1, &block)
106
+ else
107
+ [result, caught_exception]
108
+ end
109
+ end
110
+
111
+ # Sends a request for the batch, returns [status_code, body]
112
+ def send_request(batch)
113
+ payload = JSON.generate(
114
+ :analyticsVersion => '1.0.0',
115
+ :sdkVersion => @device_properties.lib_version,
116
+ :time => DateTime.now.strftime('%Q').to_i,
117
+ :platform => @device_properties.all_properties[PropertyType::PLATFORM.name],
118
+ :rolloutKey => @device_properties.rollout_key,
119
+ :events => batch
120
+ )
121
+ request = Net::HTTP::Post.new(@path, @headers)
122
+
123
+ if self.class.stub
124
+ logger.debug "stubbed request to #{@path}: #{JSON.generate(batch)}"
125
+
126
+ [200, '{}']
127
+ else
128
+ @http.start unless @http.started? # Maintain a persistent connection
129
+ response = @http.request(request, payload)
130
+ [response.code.to_i, response.body]
131
+ end
132
+ end
133
+
134
+ class << self
135
+ attr_writer :stub
136
+
137
+ def stub
138
+ @stub || ENV['STUB']
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ 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
@@ -1,77 +1,85 @@
1
1
  module Rox
2
2
  module Core
3
3
  module Environment
4
+ def self.reset(rox_options = nil)
5
+ @roxy_internal_path = 'device/request_configuration'
6
+ if !rox_options&.network_configurations_options.nil?
7
+ @cdn_path = rox_options&.network_configurations_options&.get_config_cloud_endpoint
8
+ @api_path = rox_options&.network_configurations_options&.get_config_api_endpoint
9
+ @state_cdn_path = rox_options&.network_configurations_options&.send_state_cloud_endpoint
10
+ @state_api_path = rox_options&.network_configurations_options&.send_state_api_endpoint
11
+ @analytics_path = rox_options&.network_configurations_options&.analytics_endpoint
12
+ @notifications_path = rox_options&.network_configurations_options&.push_notification_endpoint
13
+ else
14
+ case ENV['ROLLOUT_MODE']
15
+ when 'QA'
16
+ setQA
17
+ when 'LOCAL'
18
+ setLOCAL
19
+ else
20
+ setDefault(rox_options)
21
+ end
22
+ end
23
+ end
24
+
4
25
  def self.roxy_internal_path
5
- 'device/request_configuration'
26
+ @roxy_internal_path
6
27
  end
7
28
 
8
29
  def self.cdn_path
9
- case ENV['ROLLOUT_MODE']
10
- when 'QA'
11
- 'https://qa-conf.rollout.io'
12
- when 'LOCAL'
13
- 'https://development-conf.rollout.io'
14
- else
15
- 'https://conf.rollout.io'
16
- end
30
+ @cdn_path
17
31
  end
18
32
 
19
- def self.api_path(api_url = nil)
20
- case ENV['ROLLOUT_MODE']
21
- when 'QA'
22
- 'https://qa-api.rollout.io/device/get_configuration'
23
- when 'LOCAL'
24
- 'http://127.0.0.1:8557/device/get_configuration'
25
- else
26
- api_url ||= 'https://x-api.rollout.io'
27
- "#{api_url}/device/get_configuration"
28
- end
33
+ def self.api_path
34
+ @api_path
29
35
  end
30
36
 
31
37
  def self.state_cdn_path
32
- case ENV['ROLLOUT_MODE']
33
- when 'QA'
34
- 'https://qa-statestore.rollout.io'
35
- when 'LOCAL'
36
- 'https://development-statestore.rollout.io'
37
- else
38
- 'https://statestore.rollout.io'
39
- end
38
+ @state_cdn_path
40
39
  end
41
40
 
42
- def self.state_api_path(api_url = nil)
43
- case ENV['ROLLOUT_MODE']
44
- when 'QA'
45
- 'https://qa-api.rollout.io/device/update_state_store'
46
- when 'LOCAL'
47
- 'http://127.0.0.1:8557/device/update_state_store'
48
- else
49
- api_url ||= 'https://x-api.rollout.io'
50
- "#{api_url}/device/update_state_store"
51
- end
41
+ def self.state_api_path
42
+ @state_api_path
52
43
  end
53
44
 
54
- def self.analytics_path(analytics_url = 'https://analytic.rollout.io')
55
- case ENV['ROLLOUT_MODE']
56
- when 'QA'
57
- 'https://qaanalytic.rollout.io'
58
- when 'LOCAL'
59
- 'http://127.0.0.1:8787'
60
- else
61
- analytics_url
62
- end
45
+ def self.analytics_path
46
+ @analytics_path
63
47
  end
64
48
 
65
49
  def self.notifications_path
66
- case ENV['ROLLOUT_MODE']
67
- when 'QA'
68
- 'https://qax-push.rollout.io/sse'
69
- when 'LOCAL'
70
- 'http://127.0.0.1:8887/sse'
71
- else
72
- 'https://push.rollout.io/sse'
73
- end
50
+ @notifications_path
51
+ end
52
+
53
+ private
54
+ def self.setQA()
55
+ @cdn_path = 'https://qa-conf.rollout.io'
56
+ @api_path = 'https://qa-api.rollout.io/device/get_configuration'
57
+ @state_cdn_path = 'https://qa-statestore.rollout.io'
58
+ @state_api_path = 'https://qa-api.rollout.io/device/update_state_store'
59
+ @analytics_path = 'https://qaanalytic.rollout.io'
60
+ @notifications_path = 'https://qax-push.rollout.io/sse'
61
+ end
62
+
63
+ def self.setLOCAL()
64
+ @cdn_path = 'https://development-conf.rollout.io'
65
+ @api_path = 'http://127.0.0.1:8557/device/get_configuration'
66
+ @state_cdn_path = 'https://development-statestore.rollout.io'
67
+ @state_api_path = 'http://127.0.0.1:8557/device/update_state_store'
68
+ @analytics_path = 'http://127.0.0.1:8787'
69
+ @notifications_path = 'http://127.0.0.1:8887/sse'
70
+ end
71
+
72
+ def self.setDefault(rox_options)
73
+ @cdn_path = 'https://conf.rollout.io'
74
+ @state_cdn_path = 'https://statestore.rollout.io'
75
+ alternative_api_url = rox_options&.self_managed_options&.server_url || 'https://x-api.rollout.io'
76
+ @api_path = "#{alternative_api_url}/device/get_configuration"
77
+ @state_api_path = "#{alternative_api_url}/device/update_state_store"
78
+ @analytics_path = rox_options&.self_managed_options&.analytics_url ||'https://analytic.rollout.io'
79
+ @notifications_path = 'https://push.rollout.io/sse'
74
80
  end
75
81
  end
76
82
  end
77
83
  end
84
+
85
+ Rox::Core::Environment.reset
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)
@@ -57,6 +59,7 @@ module Rox
57
59
  def setup(sdk_settings, device_properties)
58
60
  @sdk_settings = sdk_settings
59
61
  @rox_options = device_properties.rox_options
62
+ Rox::Core::Environment.reset(@rox_options)
60
63
 
61
64
  experiments_extensions = ExperimentsExtensions.new(@parser, @target_group_repository, @flag_repository,
62
65
  @experiment_repository)
@@ -68,12 +71,14 @@ module Rox
68
71
 
69
72
  validate_api_key(sdk_settings&.api_key) if roxy_path.nil?
70
73
 
71
- # TODO: Analytics.Analytics.Initialize(deviceProperties.RolloutKey, deviceProperties)
72
74
  @internal_flags = InternalFlags.new(@experiment_repository, @parser, @rox_options)
73
75
 
74
- # TODO: impressionInvoker = new ImpressionInvoker(internalFlags, customPropertyRepository, deviceProperties, Analytics.Analytics.Client, roxyPath != null);
76
+ if roxy_path.nil?
77
+ @analytics_client = create_analytics_client(device_properties)
78
+ end
79
+
75
80
  @impression_invoker = ImpressionInvoker.new(@internal_flags, @custom_property_repository, device_properties,
76
- nil, !roxy_path.nil?, @user_unhandled_error_invoker)
81
+ @analytics_client, !roxy_path.nil?, @user_unhandled_error_invoker)
77
82
  @flag_setter = FlagSetter.new(@flag_repository, @parser, @experiment_repository, @impression_invoker)
78
83
  buid = BUID.new(sdk_settings, device_properties, @flag_repository, @custom_property_repository)
79
84
 
@@ -103,14 +108,13 @@ module Rox
103
108
  @configuration_fetched_invoker)
104
109
  end
105
110
 
106
- configuration_fetched_handler = nil
107
- configuration_fetched_handler = @rox_options.configuration_fetched_handler unless @rox_options.nil?
108
-
109
111
  @configuration_fetched_invoker.register_start_stop_push(proc do |args|
110
112
  start_or_stop_push_updated_listener unless args.fetcher_status == FetcherStatus::ERROR_FETCHED_FAILED
111
113
  end)
112
114
 
113
- @configuration_fetched_invoker.register_fetched_handler(&configuration_fetched_handler)
115
+ if !@rox_options.nil? && !@rox_options.configuration_fetched_handler.nil?
116
+ @configuration_fetched_invoker.register_fetched_handler(&@rox_options.configuration_fetched_handler)
117
+ end
114
118
 
115
119
  @thread = Thread.new do
116
120
  Thread.current.report_on_exception = false if Thread.current.respond_to?(:report_on_exception)
@@ -138,9 +142,9 @@ module Rox
138
142
  @push_updates_listener = nil
139
143
  end
140
144
 
141
- #return if @analytics_client.nil?
145
+ return if @analytics_client.nil?
142
146
 
143
- #@analytics_client.flush
147
+ @analytics_client.flush
144
148
  end
145
149
 
146
150
  def fetch
@@ -224,6 +228,12 @@ module Rox
224
228
  raise ArgumentError, 'Illegal Rollout api key'
225
229
  end
226
230
  end
231
+
232
+ private
233
+
234
+ def create_analytics_client(device_properties)
235
+ Rox::Core::Analytics::Client.new(device_properties)
236
+ end
227
237
  end
228
238
  end
229
239
  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 = Rox::Server::NormalizeFlagType.normalize_float(internal_value(context, false, Float))
8
+ send_impressions(return_value, context)
9
+ return_value
8
10
  end
9
11
  end
10
12
  end