airbrake-ruby 4.1.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +5 -5
  2. data/lib/airbrake-ruby/async_sender.rb +22 -96
  3. data/lib/airbrake-ruby/backtrace.rb +8 -7
  4. data/lib/airbrake-ruby/benchmark.rb +39 -0
  5. data/lib/airbrake-ruby/code_hunk.rb +1 -1
  6. data/lib/airbrake-ruby/config/processor.rb +84 -0
  7. data/lib/airbrake-ruby/config/validator.rb +9 -3
  8. data/lib/airbrake-ruby/config.rb +76 -20
  9. data/lib/airbrake-ruby/deploy_notifier.rb +1 -1
  10. data/lib/airbrake-ruby/file_cache.rb +6 -0
  11. data/lib/airbrake-ruby/filter_chain.rb +16 -1
  12. data/lib/airbrake-ruby/filters/dependency_filter.rb +1 -0
  13. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +2 -2
  14. data/lib/airbrake-ruby/filters/gem_root_filter.rb +1 -0
  15. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +5 -5
  16. data/lib/airbrake-ruby/filters/git_repository_filter.rb +3 -0
  17. data/lib/airbrake-ruby/filters/git_revision_filter.rb +2 -0
  18. data/lib/airbrake-ruby/filters/{keys_whitelist.rb → keys_allowlist.rb} +3 -3
  19. data/lib/airbrake-ruby/filters/{keys_blacklist.rb → keys_blocklist.rb} +3 -3
  20. data/lib/airbrake-ruby/filters/keys_filter.rb +39 -20
  21. data/lib/airbrake-ruby/filters/root_directory_filter.rb +1 -0
  22. data/lib/airbrake-ruby/filters/sql_filter.rb +30 -6
  23. data/lib/airbrake-ruby/filters/system_exit_filter.rb +1 -0
  24. data/lib/airbrake-ruby/filters/thread_filter.rb +4 -2
  25. data/lib/airbrake-ruby/grouppable.rb +12 -0
  26. data/lib/airbrake-ruby/ignorable.rb +1 -0
  27. data/lib/airbrake-ruby/inspectable.rb +2 -2
  28. data/lib/airbrake-ruby/loggable.rb +2 -2
  29. data/lib/airbrake-ruby/mergeable.rb +12 -0
  30. data/lib/airbrake-ruby/monotonic_time.rb +48 -0
  31. data/lib/airbrake-ruby/notice.rb +10 -20
  32. data/lib/airbrake-ruby/notice_notifier.rb +23 -42
  33. data/lib/airbrake-ruby/performance_breakdown.rb +52 -0
  34. data/lib/airbrake-ruby/performance_notifier.rb +126 -49
  35. data/lib/airbrake-ruby/promise.rb +1 -0
  36. data/lib/airbrake-ruby/query.rb +26 -11
  37. data/lib/airbrake-ruby/queue.rb +65 -0
  38. data/lib/airbrake-ruby/remote_settings/settings_data.rb +120 -0
  39. data/lib/airbrake-ruby/remote_settings.rb +145 -0
  40. data/lib/airbrake-ruby/request.rb +20 -6
  41. data/lib/airbrake-ruby/stashable.rb +15 -0
  42. data/lib/airbrake-ruby/stat.rb +34 -24
  43. data/lib/airbrake-ruby/sync_sender.rb +3 -2
  44. data/lib/airbrake-ruby/tdigest.rb +43 -58
  45. data/lib/airbrake-ruby/thread_pool.rb +138 -0
  46. data/lib/airbrake-ruby/timed_trace.rb +58 -0
  47. data/lib/airbrake-ruby/truncator.rb +10 -4
  48. data/lib/airbrake-ruby/version.rb +11 -1
  49. data/lib/airbrake-ruby.rb +219 -53
  50. data/spec/airbrake_spec.rb +428 -9
  51. data/spec/async_sender_spec.rb +26 -110
  52. data/spec/backtrace_spec.rb +44 -44
  53. data/spec/benchmark_spec.rb +33 -0
  54. data/spec/code_hunk_spec.rb +11 -11
  55. data/spec/config/processor_spec.rb +209 -0
  56. data/spec/config/validator_spec.rb +23 -6
  57. data/spec/config_spec.rb +77 -7
  58. data/spec/deploy_notifier_spec.rb +2 -2
  59. data/spec/{file_cache.rb → file_cache_spec.rb} +2 -4
  60. data/spec/filter_chain_spec.rb +28 -1
  61. data/spec/filters/dependency_filter_spec.rb +1 -1
  62. data/spec/filters/gem_root_filter_spec.rb +9 -9
  63. data/spec/filters/git_last_checkout_filter_spec.rb +21 -4
  64. data/spec/filters/git_repository_filter.rb +1 -1
  65. data/spec/filters/git_revision_filter_spec.rb +13 -11
  66. data/spec/filters/{keys_whitelist_spec.rb → keys_allowlist_spec.rb} +29 -28
  67. data/spec/filters/{keys_blacklist_spec.rb → keys_blocklist_spec.rb} +39 -29
  68. data/spec/filters/root_directory_filter_spec.rb +9 -9
  69. data/spec/filters/sql_filter_spec.rb +110 -55
  70. data/spec/filters/system_exit_filter_spec.rb +1 -1
  71. data/spec/filters/thread_filter_spec.rb +33 -31
  72. data/spec/fixtures/project_root/code.rb +9 -9
  73. data/spec/loggable_spec.rb +17 -0
  74. data/spec/monotonic_time_spec.rb +23 -0
  75. data/spec/{notice_notifier_spec → notice_notifier}/options_spec.rb +19 -21
  76. data/spec/notice_notifier_spec.rb +20 -80
  77. data/spec/notice_spec.rb +9 -11
  78. data/spec/performance_breakdown_spec.rb +11 -0
  79. data/spec/performance_notifier_spec.rb +360 -85
  80. data/spec/query_spec.rb +11 -0
  81. data/spec/queue_spec.rb +18 -0
  82. data/spec/remote_settings/settings_data_spec.rb +365 -0
  83. data/spec/remote_settings_spec.rb +230 -0
  84. data/spec/request_spec.rb +9 -0
  85. data/spec/response_spec.rb +8 -8
  86. data/spec/spec_helper.rb +9 -13
  87. data/spec/stashable_spec.rb +23 -0
  88. data/spec/stat_spec.rb +17 -15
  89. data/spec/sync_sender_spec.rb +14 -12
  90. data/spec/tdigest_spec.rb +6 -6
  91. data/spec/thread_pool_spec.rb +187 -0
  92. data/spec/timed_trace_spec.rb +125 -0
  93. data/spec/truncator_spec.rb +12 -12
  94. metadata +55 -18
@@ -9,7 +9,7 @@ module Airbrake
9
9
  # @return [Array<Class>] filters to be executed first
10
10
  DEFAULT_FILTERS = [
11
11
  Airbrake::Filters::SystemExitFilter,
12
- Airbrake::Filters::GemRootFilter
12
+ Airbrake::Filters::GemRootFilter,
13
13
 
14
14
  # Optional filters (must be included by users):
15
15
  # Airbrake::Filters::ThreadFilter
@@ -25,34 +25,38 @@ module Airbrake
25
25
  @async_sender = AsyncSender.new
26
26
  @sync_sender = SyncSender.new
27
27
 
28
- add_default_filters
28
+ DEFAULT_FILTERS.each { |filter| add_filter(filter.new) }
29
+
30
+ add_filter(Airbrake::Filters::ContextFilter.new(@context))
31
+ add_filter(Airbrake::Filters::ExceptionAttributesFilter.new)
29
32
  end
30
33
 
31
- # @macro see_public_api_method
34
+ # @see Airbrake.notify
32
35
  def notify(exception, params = {}, &block)
33
36
  send_notice(exception, params, default_sender, &block)
34
37
  end
35
38
 
36
- # @macro see_public_api_method
39
+ # @see Airbrake.notify_sync
37
40
  def notify_sync(exception, params = {}, &block)
38
41
  send_notice(exception, params, @sync_sender, &block).value
39
42
  end
40
43
 
41
- # @macro see_public_api_method
44
+ # @see Airbrake.add_filte
42
45
  def add_filter(filter = nil, &block)
43
46
  @filter_chain.add_filter(block_given? ? block : filter)
44
47
  end
45
48
 
46
- # @macro see_public_api_method
49
+ # @see Airbrake.delete_filter
47
50
  def delete_filter(filter_class)
48
51
  @filter_chain.delete_filter(filter_class)
49
52
  end
50
53
 
51
- # @macro see_public_api_method
54
+ # @see Airbrake.build_notice
52
55
  def build_notice(exception, params = {})
53
56
  if @async_sender.closed?
54
57
  raise Airbrake::Error,
55
- "attempted to build #{exception} with closed Airbrake instance"
58
+ "Airbrake is closed; can't build exception: " \
59
+ "#{exception.class}: #{exception}"
56
60
  end
57
61
 
58
62
  if exception.is_a?(Airbrake::Notice)
@@ -63,21 +67,27 @@ module Airbrake
63
67
  end
64
68
  end
65
69
 
66
- # @macro see_public_api_method
70
+ # @see Airbrake.close
67
71
  def close
68
72
  @async_sender.close
69
73
  end
70
74
 
71
- # @macro see_public_api_method
75
+ # @see Airbrake.configured?
72
76
  def configured?
73
77
  @config.valid?
74
78
  end
75
79
 
76
- # @macro see_public_api_method
80
+ # @see Airbrake.merge_context
77
81
  def merge_context(context)
78
82
  @context.merge!(context)
79
83
  end
80
84
 
85
+ # @return [Boolean]
86
+ # @since v4.14.0
87
+ def has_filter?(filter_class) # rubocop:disable Naming/PredicateName
88
+ @filter_chain.includes?(filter_class)
89
+ end
90
+
81
91
  private
82
92
 
83
93
  def convert_to_exception(ex)
@@ -113,7 +123,7 @@ module Airbrake
113
123
 
114
124
  logger.warn(
115
125
  "#{LOG_LABEL} falling back to sync delivery because there are no " \
116
- "running async workers"
126
+ "running async workers",
117
127
  )
118
128
  @sync_sender
119
129
  end
@@ -125,37 +135,8 @@ module Airbrake
125
135
  # If true, then it's likely an internal library error. In this case return
126
136
  # at least some backtrace to simplify debugging.
127
137
  return caller_copy if clean_bt.empty?
128
- clean_bt
129
- end
130
-
131
- # rubocop:disable Metrics/AbcSize
132
- def add_default_filters
133
- DEFAULT_FILTERS.each { |f| add_filter(f.new) }
134
-
135
- if (whitelist_keys = @config.whitelist_keys).any?
136
- add_filter(Airbrake::Filters::KeysWhitelist.new(whitelist_keys))
137
- end
138
138
 
139
- if (blacklist_keys = @config.blacklist_keys).any?
140
- add_filter(Airbrake::Filters::KeysBlacklist.new(blacklist_keys))
141
- end
142
-
143
- add_filter(Airbrake::Filters::ContextFilter.new(@context))
144
- add_filter(Airbrake::Filters::ExceptionAttributesFilter.new)
145
-
146
- return unless (root_directory = @config.root_directory)
147
- [
148
- Airbrake::Filters::RootDirectoryFilter,
149
- Airbrake::Filters::GitRevisionFilter,
150
- Airbrake::Filters::GitRepositoryFilter
151
- ].each do |filter|
152
- add_filter(filter.new(root_directory))
153
- end
154
-
155
- add_filter(
156
- Airbrake::Filters::GitLastCheckoutFilter.new(root_directory)
157
- )
139
+ clean_bt
158
140
  end
159
- # rubocop:enable Metrics/AbcSize
160
141
  end
161
142
  end
@@ -0,0 +1,52 @@
1
+ module Airbrake
2
+ # PerformanceBreakdown holds data that shows how much time a request spent
3
+ # doing certaing subtasks such as (DB querying, view rendering, etc).
4
+ #
5
+ # @see Airbrake.notify_breakdown
6
+ # @api public
7
+ # @since v4.2.0
8
+ # rubocop:disable Metrics/ParameterLists
9
+ class PerformanceBreakdown
10
+ include HashKeyable
11
+ include Ignorable
12
+ include Stashable
13
+ include Mergeable
14
+
15
+ attr_accessor :method, :route, :response_type, :groups, :timing, :time
16
+
17
+ def initialize(
18
+ method:,
19
+ route:,
20
+ response_type:,
21
+ groups:,
22
+ timing: nil,
23
+ time: Time.now
24
+ )
25
+ @time_utc = TimeTruncate.utc_truncate_minutes(time)
26
+ @method = method
27
+ @route = route
28
+ @response_type = response_type
29
+ @groups = groups
30
+ @timing = timing
31
+ @time = time
32
+ end
33
+
34
+ def destination
35
+ 'routes-breakdowns'
36
+ end
37
+
38
+ def cargo
39
+ 'routes'
40
+ end
41
+
42
+ def to_h
43
+ {
44
+ 'method' => method,
45
+ 'route' => route,
46
+ 'responseType' => response_type,
47
+ 'time' => @time_utc,
48
+ }.delete_if { |_key, val| val.nil? }
49
+ end
50
+ end
51
+ # rubocop:enable Metrics/ParameterLists
52
+ end
@@ -4,6 +4,7 @@ module Airbrake
4
4
  #
5
5
  # @api public
6
6
  # @since v3.2.0
7
+ # rubocop:disable Metrics/ClassLength
7
8
  class PerformanceNotifier
8
9
  include Inspectable
9
10
  include Loggable
@@ -11,40 +12,29 @@ module Airbrake
11
12
  def initialize
12
13
  @config = Airbrake::Config.instance
13
14
  @flush_period = Airbrake::Config.instance.performance_stats_flush_period
14
- @sender = SyncSender.new(:put)
15
- @payload = {}
15
+ @async_sender = AsyncSender.new(:put)
16
+ @sync_sender = SyncSender.new(:put)
16
17
  @schedule_flush = nil
17
- @mutex = Mutex.new
18
18
  @filter_chain = FilterChain.new
19
+
20
+ @payload = {}.extend(MonitorMixin)
21
+ @has_payload = @payload.new_cond
19
22
  end
20
23
 
21
24
  # @param [Hash] resource
22
25
  # @see Airbrake.notify_query
23
26
  # @see Airbrake.notify_request
24
27
  def notify(resource)
25
- promise = @config.check_configuration
26
- return promise if promise.rejected?
27
-
28
- promise = Airbrake::Promise.new
29
- unless @config.performance_stats
30
- return promise.reject("The Performance Stats feature is disabled")
31
- end
32
-
33
- @filter_chain.refine(resource)
34
- return if resource.ignored?
35
-
36
- @mutex.synchronize do
37
- @payload[resource] ||= Airbrake::Stat.new
38
- @payload[resource].increment(resource.start_time, resource.end_time)
39
-
40
- if @flush_period > 0
41
- schedule_flush(promise)
42
- else
43
- send(@payload, promise)
44
- end
28
+ @payload.synchronize do
29
+ send_resource(resource, sync: false)
45
30
  end
31
+ end
46
32
 
47
- promise
33
+ # @param [Hash] resource
34
+ # @since v4.10.0
35
+ # @see Airbrake.notify_queue_sync
36
+ def notify_sync(resource)
37
+ send_resource(resource, sync: true).value
48
38
  end
49
39
 
50
40
  # @see Airbrake.add_performance_filter
@@ -57,44 +47,131 @@ module Airbrake
57
47
  @filter_chain.delete_filter(filter_class)
58
48
  end
59
49
 
50
+ def close
51
+ @payload.synchronize do
52
+ @schedule_flush.kill if @schedule_flush
53
+ @async_sender.close
54
+ logger.debug("#{LOG_LABEL} performance notifier closed")
55
+ end
56
+ end
57
+
60
58
  private
61
59
 
62
- def schedule_flush(promise)
60
+ def schedule_flush
63
61
  @schedule_flush ||= Thread.new do
64
- sleep(@flush_period)
65
-
66
- payload = nil
67
- @mutex.synchronize do
68
- payload = @payload
69
- @payload = {}
70
- @schedule_flush = nil
62
+ loop do
63
+ @payload.synchronize do
64
+ @last_flush_time ||= MonotonicTime.time_in_s
65
+
66
+ while (MonotonicTime.time_in_s - @last_flush_time) < @flush_period
67
+ @has_payload.wait(@flush_period)
68
+ end
69
+
70
+ if @payload.none?
71
+ @last_flush_time = nil
72
+ next
73
+ end
74
+
75
+ send(@async_sender, @payload, Airbrake::Promise.new)
76
+ @payload.clear
77
+ end
71
78
  end
79
+ end
80
+ end
81
+
82
+ def send_resource(resource, sync:)
83
+ promise = check_configuration(resource)
84
+ return promise if promise.rejected?
72
85
 
73
- send(payload, promise)
86
+ @filter_chain.refine(resource)
87
+ if resource.ignored?
88
+ return Promise.new.reject("#{resource.class} was ignored by a filter")
89
+ end
90
+
91
+ update_payload(resource)
92
+ if sync || @flush_period == 0
93
+ send(@sync_sender, @payload, promise)
94
+ else
95
+ @has_payload.signal
96
+ schedule_flush
74
97
  end
75
98
  end
76
99
 
77
- # rubocop:disable Metrics/AbcSize
78
- def send(payload, promise)
79
- signature = "#{self.class.name}##{__method__}"
80
- raise "#{signature}: payload (#{payload}) cannot be empty. Race?" if payload.none?
100
+ def update_payload(resource)
101
+ if (total_stat = @payload[resource])
102
+ @payload.key(total_stat).merge(resource)
103
+ else
104
+ @payload[resource] = { total: Airbrake::Stat.new }
105
+ end
81
106
 
82
- logger.debug("#{LOG_LABEL} #{signature}: #{payload}")
107
+ @payload[resource][:total].increment_ms(resource.timing)
83
108
 
84
- payload.group_by { |k, _v| k.name }.each do |resource_name, data|
85
- data = { resource_name => data.map { |k, v| k.to_h.merge!(v.to_h) } }
86
- data['environment'] = @config.environment if @config.environment
109
+ resource.groups.each do |name, ms|
110
+ @payload[resource][name] ||= Airbrake::Stat.new
111
+ @payload[resource][name].increment_ms(ms)
112
+ end
113
+ end
87
114
 
88
- @sender.send(
89
- data,
90
- promise,
91
- URI.join(
92
- @config.host,
93
- "api/v5/projects/#{@config.project_id}/#{resource_name}-stats"
94
- )
115
+ def check_configuration(resource)
116
+ promise = @config.check_configuration
117
+ return promise if promise.rejected?
118
+
119
+ promise = @config.check_performance_options(resource)
120
+ return promise if promise.rejected?
121
+
122
+ if resource.timing && resource.timing == 0
123
+ return Promise.new.reject(':timing cannot be zero')
124
+ end
125
+
126
+ Promise.new
127
+ end
128
+
129
+ def send(sender, payload, promise)
130
+ raise "payload cannot be empty. Race?" if payload.none?
131
+
132
+ with_grouped_payload(payload) do |resource_hash, destination|
133
+ url = URI.join(
134
+ @config.apm_host,
135
+ "api/v5/projects/#{@config.project_id}/#{destination}",
95
136
  )
137
+
138
+ logger.debug do
139
+ "#{LOG_LABEL} #{self.class.name}##{__method__}: #{resource_hash}"
140
+ end
141
+ sender.send(resource_hash, promise, url)
142
+ end
143
+
144
+ promise
145
+ end
146
+
147
+ def with_grouped_payload(raw_payload)
148
+ grouped_payload = raw_payload.group_by do |resource, _stats|
149
+ [resource.cargo, resource.destination]
150
+ end
151
+
152
+ grouped_payload.each do |(cargo, destination), resources|
153
+ payload = {}
154
+ payload[cargo] = serialize_resources(resources)
155
+ payload['environment'] = @config.environment if @config.environment
156
+
157
+ yield(payload, destination)
158
+ end
159
+ end
160
+
161
+ def serialize_resources(resources)
162
+ resources.map do |resource, stats|
163
+ resource_hash = resource.to_h.merge!(stats[:total].to_h)
164
+
165
+ if resource.groups.any?
166
+ group_stats = stats.reject { |name, _stat| name == :total }
167
+ resource_hash['groups'] = group_stats.merge(group_stats) do |_name, stat|
168
+ stat.to_h
169
+ end
170
+ end
171
+
172
+ resource_hash
96
173
  end
97
174
  end
98
- # rubocop:enable Metrics/AbcSize
99
175
  end
176
+ # rubocop:enable Metrics/ClassLength
100
177
  end
@@ -103,6 +103,7 @@ module Airbrake
103
103
  # needed for compatibility but it shouldn't exist in the future
104
104
  def value
105
105
  return @value['ok'] if resolved?
106
+
106
107
  @value
107
108
  end
108
109
  end
@@ -4,12 +4,15 @@ module Airbrake
4
4
  # @see Airbrake.notify_query
5
5
  # @api public
6
6
  # @since v3.2.0
7
- # rubocop:disable Metrics/ParameterLists, Metrics/BlockLength
8
- Query = Struct.new(
9
- :method, :route, :query, :func, :file, :line, :start_time, :end_time
10
- ) do
7
+ # rubocop:disable Metrics/ParameterLists
8
+ class Query
11
9
  include HashKeyable
12
10
  include Ignorable
11
+ include Stashable
12
+ include Mergeable
13
+ include Grouppable
14
+
15
+ attr_accessor :method, :route, :query, :func, :file, :line, :timing, :time
13
16
 
14
17
  def initialize(
15
18
  method:,
@@ -18,13 +21,25 @@ module Airbrake
18
21
  func: nil,
19
22
  file: nil,
20
23
  line: nil,
21
- start_time:,
22
- end_time: Time.now
24
+ timing: nil,
25
+ time: Time.now
23
26
  )
24
- super(method, route, query, func, file, line, start_time, end_time)
27
+ @time_utc = TimeTruncate.utc_truncate_minutes(time)
28
+ @method = method
29
+ @route = route
30
+ @query = query
31
+ @func = func
32
+ @file = file
33
+ @line = line
34
+ @timing = timing
35
+ @time = time
36
+ end
37
+
38
+ def destination
39
+ 'queries-stats'
25
40
  end
26
41
 
27
- def name
42
+ def cargo
28
43
  'queries'
29
44
  end
30
45
 
@@ -33,12 +48,12 @@ module Airbrake
33
48
  'method' => method,
34
49
  'route' => route,
35
50
  'query' => query,
36
- 'time' => TimeTruncate.utc_truncate_minutes(start_time),
51
+ 'time' => @time_utc,
37
52
  'function' => func,
38
53
  'file' => file,
39
- 'line' => line
54
+ 'line' => line,
40
55
  }.delete_if { |_key, val| val.nil? }
41
56
  end
42
- # rubocop:enable Metrics/ParameterLists, Metrics/BlockLength
57
+ # rubocop:enable Metrics/ParameterLists
43
58
  end
44
59
  end
@@ -0,0 +1,65 @@
1
+ module Airbrake
2
+ # Queue represents a queue (worker).
3
+ #
4
+ # @see Airbrake.notify_queue
5
+ # @api public
6
+ # @since v4.9.0
7
+ class Queue
8
+ include HashKeyable
9
+ include Ignorable
10
+ include Stashable
11
+
12
+ attr_accessor :queue, :error_count, :groups, :timing, :time
13
+
14
+ def initialize(
15
+ queue:,
16
+ error_count:,
17
+ groups: {},
18
+ timing: nil,
19
+ time: Time.now
20
+ )
21
+ @time_utc = TimeTruncate.utc_truncate_minutes(time)
22
+ @queue = queue
23
+ @error_count = error_count
24
+ @groups = groups
25
+ @timing = timing
26
+ @time = time
27
+ end
28
+
29
+ def destination
30
+ 'queues-stats'
31
+ end
32
+
33
+ def cargo
34
+ 'queues'
35
+ end
36
+
37
+ def to_h
38
+ {
39
+ 'queue' => queue,
40
+ 'errorCount' => error_count,
41
+ 'time' => @time_utc,
42
+ }
43
+ end
44
+
45
+ def hash
46
+ {
47
+ 'queue' => queue,
48
+ 'time' => @time_utc,
49
+ }.hash
50
+ end
51
+
52
+ def merge(other)
53
+ self.error_count += other.error_count
54
+ end
55
+
56
+ # Queues don't have routes, but we want to define this to make sure our
57
+ # filter API is consistent (other models define this property)
58
+ #
59
+ # @return [String] empty route
60
+ # @see https://github.com/airbrake/airbrake-ruby/pull/537
61
+ def route
62
+ ''
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,120 @@
1
+ module Airbrake
2
+ class RemoteSettings
3
+ # SettingsData is a container, which wraps JSON payload returned by the
4
+ # remote settings API. It exposes the payload via convenient methods and
5
+ # also ensures that in case some data from the payload is missing, a default
6
+ # value would be returned instead.
7
+ #
8
+ # @example
9
+ # # Create the object and pass initial data (empty hash).
10
+ # settings_data = SettingsData.new({})
11
+ #
12
+ # settings_data.interval #=> 600
13
+ #
14
+ # @since 5.0.0
15
+ # @api private
16
+ class SettingsData
17
+ # @return [Integer] how frequently we should poll the config API
18
+ DEFAULT_INTERVAL = 600
19
+
20
+ # @return [String] API version of the S3 API to poll
21
+ API_VER = '2020-06-18'.freeze
22
+
23
+ # @return [String] what path to poll
24
+ CONFIG_ROUTE_PATTERN =
25
+ "%<host>s/#{API_VER}/config/%<project_id>s/config.json".freeze
26
+
27
+ # @return [Hash{Symbol=>String}] the hash of all supported settings where
28
+ # the value is the name of the setting returned by the API
29
+ SETTINGS = {
30
+ errors: 'errors'.freeze,
31
+ apm: 'apm'.freeze,
32
+ }.freeze
33
+
34
+ # @param [Integer] project_id
35
+ # @param [Hash{String=>Object}] data
36
+ def initialize(project_id, data)
37
+ @project_id = project_id
38
+ @data = data
39
+ end
40
+
41
+ # Merges the given +hash+ with internal data.
42
+ #
43
+ # @param [Hash{String=>Object}] hash
44
+ # @return [self]
45
+ def merge!(hash)
46
+ @data.merge!(hash)
47
+
48
+ self
49
+ end
50
+
51
+ # @return [Integer] how frequently we should poll for the config
52
+ def interval
53
+ return DEFAULT_INTERVAL if !@data.key?('poll_sec') || !@data['poll_sec']
54
+
55
+ @data['poll_sec'] > 0 ? @data['poll_sec'] : DEFAULT_INTERVAL
56
+ end
57
+
58
+ # @param [String] remote_config_host
59
+ # @return [String] where the config is stored on S3.
60
+ def config_route(remote_config_host)
61
+ if !@data.key?('config_route') || !@data['config_route']
62
+ return format(
63
+ CONFIG_ROUTE_PATTERN,
64
+ host: remote_config_host.chomp('/'),
65
+ project_id: @project_id,
66
+ )
67
+ end
68
+
69
+ format(
70
+ CONFIG_ROUTE_PATTERN,
71
+ host: @data['config_route'].chomp('/'),
72
+ project_id: @project_id,
73
+ )
74
+ end
75
+
76
+ # @return [Boolean] whether error notifications are enabled
77
+ def error_notifications?
78
+ return true unless (s = find_setting(SETTINGS[:errors]))
79
+
80
+ s['enabled']
81
+ end
82
+
83
+ # @return [Boolean] whether APM is enabled
84
+ def performance_stats?
85
+ return true unless (s = find_setting(SETTINGS[:apm]))
86
+
87
+ s['enabled']
88
+ end
89
+
90
+ # @return [String, nil] the host, which provides the API endpoint to which
91
+ # exceptions should be sent
92
+ def error_host
93
+ return unless (s = find_setting(SETTINGS[:errors]))
94
+
95
+ s['endpoint']
96
+ end
97
+
98
+ # @return [String, nil] the host, which provides the API endpoint to which
99
+ # APM data should be sent
100
+ def apm_host
101
+ return unless (s = find_setting(SETTINGS[:apm]))
102
+
103
+ s['endpoint']
104
+ end
105
+
106
+ # @return [Hash{String=>Object}] raw representation of JSON payload
107
+ def to_h
108
+ @data.dup
109
+ end
110
+
111
+ private
112
+
113
+ def find_setting(name)
114
+ return unless @data.key?('settings')
115
+
116
+ @data['settings'].find { |s| s['name'] == name }
117
+ end
118
+ end
119
+ end
120
+ end