airbrake-ruby 4.1.0 → 5.0.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.
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