airbrake-ruby 4.8.0 → 5.2.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/lib/airbrake-ruby.rb +132 -57
  3. data/lib/airbrake-ruby/async_sender.rb +7 -30
  4. data/lib/airbrake-ruby/backtrace.rb +8 -7
  5. data/lib/airbrake-ruby/benchmark.rb +1 -1
  6. data/lib/airbrake-ruby/code_hunk.rb +1 -1
  7. data/lib/airbrake-ruby/config.rb +59 -15
  8. data/lib/airbrake-ruby/config/processor.rb +71 -0
  9. data/lib/airbrake-ruby/config/validator.rb +9 -3
  10. data/lib/airbrake-ruby/deploy_notifier.rb +1 -1
  11. data/lib/airbrake-ruby/file_cache.rb +1 -1
  12. data/lib/airbrake-ruby/filter_chain.rb +16 -1
  13. data/lib/airbrake-ruby/filters/dependency_filter.rb +1 -0
  14. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +2 -2
  15. data/lib/airbrake-ruby/filters/gem_root_filter.rb +1 -0
  16. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +5 -5
  17. data/lib/airbrake-ruby/filters/git_repository_filter.rb +3 -0
  18. data/lib/airbrake-ruby/filters/git_revision_filter.rb +2 -0
  19. data/lib/airbrake-ruby/filters/{keys_whitelist.rb → keys_allowlist.rb} +3 -3
  20. data/lib/airbrake-ruby/filters/{keys_blacklist.rb → keys_blocklist.rb} +3 -3
  21. data/lib/airbrake-ruby/filters/keys_filter.rb +39 -20
  22. data/lib/airbrake-ruby/filters/root_directory_filter.rb +1 -0
  23. data/lib/airbrake-ruby/filters/sql_filter.rb +7 -7
  24. data/lib/airbrake-ruby/filters/system_exit_filter.rb +1 -0
  25. data/lib/airbrake-ruby/filters/thread_filter.rb +5 -4
  26. data/lib/airbrake-ruby/grouppable.rb +12 -0
  27. data/lib/airbrake-ruby/ignorable.rb +1 -0
  28. data/lib/airbrake-ruby/inspectable.rb +2 -2
  29. data/lib/airbrake-ruby/loggable.rb +1 -1
  30. data/lib/airbrake-ruby/mergeable.rb +12 -0
  31. data/lib/airbrake-ruby/monotonic_time.rb +5 -0
  32. data/lib/airbrake-ruby/notice.rb +7 -14
  33. data/lib/airbrake-ruby/notice_notifier.rb +11 -3
  34. data/lib/airbrake-ruby/performance_breakdown.rb +16 -10
  35. data/lib/airbrake-ruby/performance_notifier.rb +80 -58
  36. data/lib/airbrake-ruby/promise.rb +1 -0
  37. data/lib/airbrake-ruby/query.rb +20 -15
  38. data/lib/airbrake-ruby/queue.rb +65 -0
  39. data/lib/airbrake-ruby/remote_settings.rb +105 -0
  40. data/lib/airbrake-ruby/remote_settings/callback.rb +44 -0
  41. data/lib/airbrake-ruby/remote_settings/settings_data.rb +116 -0
  42. data/lib/airbrake-ruby/request.rb +14 -12
  43. data/lib/airbrake-ruby/stat.rb +26 -33
  44. data/lib/airbrake-ruby/sync_sender.rb +3 -2
  45. data/lib/airbrake-ruby/tdigest.rb +43 -58
  46. data/lib/airbrake-ruby/thread_pool.rb +11 -1
  47. data/lib/airbrake-ruby/truncator.rb +10 -4
  48. data/lib/airbrake-ruby/version.rb +11 -1
  49. data/spec/airbrake_spec.rb +206 -71
  50. data/spec/async_sender_spec.rb +3 -12
  51. data/spec/backtrace_spec.rb +44 -44
  52. data/spec/code_hunk_spec.rb +11 -11
  53. data/spec/config/processor_spec.rb +143 -0
  54. data/spec/config/validator_spec.rb +23 -6
  55. data/spec/config_spec.rb +40 -14
  56. data/spec/deploy_notifier_spec.rb +2 -2
  57. data/spec/filter_chain_spec.rb +28 -1
  58. data/spec/filters/dependency_filter_spec.rb +1 -1
  59. data/spec/filters/gem_root_filter_spec.rb +9 -9
  60. data/spec/filters/git_last_checkout_filter_spec.rb +21 -4
  61. data/spec/filters/git_repository_filter.rb +1 -1
  62. data/spec/filters/git_revision_filter_spec.rb +10 -10
  63. data/spec/filters/{keys_whitelist_spec.rb → keys_allowlist_spec.rb} +29 -28
  64. data/spec/filters/{keys_blacklist_spec.rb → keys_blocklist_spec.rb} +39 -29
  65. data/spec/filters/root_directory_filter_spec.rb +9 -9
  66. data/spec/filters/sql_filter_spec.rb +58 -60
  67. data/spec/filters/system_exit_filter_spec.rb +1 -1
  68. data/spec/filters/thread_filter_spec.rb +32 -30
  69. data/spec/fixtures/project_root/code.rb +9 -9
  70. data/spec/loggable_spec.rb +17 -0
  71. data/spec/monotonic_time_spec.rb +11 -0
  72. data/spec/notice_notifier/options_spec.rb +17 -17
  73. data/spec/notice_notifier_spec.rb +20 -20
  74. data/spec/notice_spec.rb +6 -6
  75. data/spec/performance_breakdown_spec.rb +0 -1
  76. data/spec/performance_notifier_spec.rb +220 -73
  77. data/spec/query_spec.rb +1 -1
  78. data/spec/queue_spec.rb +18 -0
  79. data/spec/remote_settings/callback_spec.rb +143 -0
  80. data/spec/remote_settings/settings_data_spec.rb +348 -0
  81. data/spec/remote_settings_spec.rb +187 -0
  82. data/spec/request_spec.rb +1 -3
  83. data/spec/response_spec.rb +8 -8
  84. data/spec/spec_helper.rb +6 -6
  85. data/spec/stat_spec.rb +2 -12
  86. data/spec/sync_sender_spec.rb +14 -12
  87. data/spec/tdigest_spec.rb +7 -7
  88. data/spec/thread_pool_spec.rb +39 -10
  89. data/spec/timed_trace_spec.rb +1 -1
  90. data/spec/truncator_spec.rb +12 -12
  91. metadata +32 -14
@@ -4,7 +4,7 @@ module Airbrake
4
4
  # notice, but specified keys.
5
5
  #
6
6
  # @example
7
- # filter = Airbrake::Filters::KeysBlacklist.new(
7
+ # filter = Airbrake::Filters::KeysAllowlist.new(
8
8
  # [:email, /credit/i, 'password']
9
9
  # )
10
10
  # airbrake.add_filter(filter)
@@ -22,9 +22,9 @@ module Airbrake
22
22
  # # email: 'john@example.com',
23
23
  # # account_id: 42 }
24
24
  #
25
- # @see KeysBlacklist
25
+ # @see KeysBlocklist
26
26
  # @see KeysFilter
27
- class KeysWhitelist
27
+ class KeysAllowlist
28
28
  include KeysFilter
29
29
 
30
30
  def initialize(*)
@@ -4,7 +4,7 @@ module Airbrake
4
4
  # list of parameters in the payload of a notice.
5
5
  #
6
6
  # @example
7
- # filter = Airbrake::Filters::KeysBlacklist.new(
7
+ # filter = Airbrake::Filters::KeysBlocklist.new(
8
8
  # [:email, /credit/i, 'password']
9
9
  # )
10
10
  # airbrake.add_filter(filter)
@@ -22,10 +22,10 @@ module Airbrake
22
22
  # # email: '[Filtered]',
23
23
  # # credit_card: '[Filtered]' }
24
24
  #
25
- # @see KeysWhitelist
25
+ # @see KeysAllowlist
26
26
  # @see KeysFilter
27
27
  # @api private
28
- class KeysBlacklist
28
+ class KeysBlocklist
29
29
  include KeysFilter
30
30
 
31
31
  def initialize(*)
@@ -7,8 +7,8 @@ module Airbrake
7
7
  # class that includes this module must implement.
8
8
  #
9
9
  # @see Notice
10
- # @see KeysWhitelist
11
- # @see KeysBlacklist
10
+ # @see KeysAllowlist
11
+ # @see KeysBlocklist
12
12
  # @api private
13
13
  module KeysFilter
14
14
  # @return [String] The label to replace real values of filtered payload
@@ -19,19 +19,30 @@ module Airbrake
19
19
  VALID_PATTERN_CLASSES = [String, Symbol, Regexp].freeze
20
20
 
21
21
  # @return [Array<Symbol>] parts of a Notice's payload that can be modified
22
- # by blacklist/whitelist filters
22
+ # by blocklist/allowlist filters
23
23
  FILTERABLE_KEYS = %i[environment session params].freeze
24
24
 
25
25
  # @return [Array<Symbol>] parts of a Notice's *context* payload that can
26
- # be modified by blacklist/whitelist filters
27
- FILTERABLE_CONTEXT_KEYS = %i[user headers].freeze
26
+ # be modified by blocklist/allowlist filters
27
+ FILTERABLE_CONTEXT_KEYS = %i[
28
+ user
29
+
30
+ # Provided by Airbrake::Rack::HttpHeadersFilter
31
+ headers
32
+ referer
33
+ httpMethod
34
+
35
+ # Provided by Airbrake::Rack::ContextFilter
36
+ userAddr
37
+ userAgent
38
+ ].freeze
28
39
 
29
40
  include Loggable
30
41
 
31
42
  # @return [Integer]
32
43
  attr_reader :weight
33
44
 
34
- # Creates a new KeysBlacklist or KeysWhitelist filter that uses the given
45
+ # Creates a new KeysBlocklist or KeysAllowlist filter that uses the given
35
46
  # +patterns+ for filtering a notice's payload.
36
47
  #
37
48
  # @param [Array<String,Regexp,Symbol>] patterns
@@ -53,10 +64,14 @@ module Airbrake
53
64
  validate_patterns
54
65
  end
55
66
 
56
- FILTERABLE_KEYS.each { |key| filter_hash(notice[key]) }
67
+ FILTERABLE_KEYS.each do |key|
68
+ notice[key] = filter_hash(notice[key])
69
+ end
70
+
57
71
  FILTERABLE_CONTEXT_KEYS.each { |key| filter_context_key(notice, key) }
58
72
 
59
73
  return unless notice[:context][:url]
74
+
60
75
  filter_url(notice)
61
76
  end
62
77
 
@@ -70,26 +85,26 @@ module Airbrake
70
85
  def filter_hash(hash)
71
86
  return hash unless hash.is_a?(Hash)
72
87
 
88
+ hash_copy = hash.dup
89
+
73
90
  hash.each_key do |key|
74
91
  if should_filter?(key.to_s)
75
- hash[key] = FILTERED
76
- elsif hash[key].is_a?(Hash)
77
- filter_hash(hash[key])
92
+ hash_copy[key] = FILTERED
93
+ elsif hash_copy[key].is_a?(Hash)
94
+ hash_copy[key] = filter_hash(hash_copy[key])
78
95
  elsif hash[key].is_a?(Array)
79
- hash[key].each { |h| filter_hash(h) }
96
+ hash_copy[key].each_with_index do |h, i|
97
+ hash_copy[key][i] = filter_hash(h)
98
+ end
80
99
  end
81
100
  end
101
+
102
+ hash_copy
82
103
  end
83
104
 
84
105
  def filter_url_params(url)
85
106
  url.query = Hash[URI.decode_www_form(url.query)].map do |key, val|
86
- # Ruby < 2.2 raises InvalidComponentError if the query contains
87
- # invalid characters, so be sure to escape individual components.
88
- if should_filter?(key)
89
- "#{URI.encode_www_form_component(key)}=[Filtered]"
90
- else
91
- "#{URI.encode_www_form_component(key)}=#{URI.encode_www_form_component(val)}"
92
- end
107
+ should_filter?(key) ? "#{key}=[Filtered]" : "#{key}=#{val}"
93
108
  end.join('&')
94
109
 
95
110
  url.to_s
@@ -103,6 +118,7 @@ module Airbrake
103
118
  end
104
119
 
105
120
  return unless url.query
121
+
106
122
  notice[:context][:url] = filter_url_params(url)
107
123
  end
108
124
 
@@ -111,6 +127,7 @@ module Airbrake
111
127
 
112
128
  @patterns = @patterns.flat_map do |pattern|
113
129
  next(pattern) unless pattern.respond_to?(:call)
130
+
114
131
  pattern.call
115
132
  end
116
133
  end
@@ -124,14 +141,16 @@ module Airbrake
124
141
 
125
142
  logger.error(
126
143
  "#{LOG_LABEL} one of the patterns in #{self.class} is invalid. " \
127
- "Known patterns: #{@patterns}"
144
+ "Known patterns: #{@patterns}",
128
145
  )
129
146
  end
130
147
 
131
148
  def filter_context_key(notice, key)
132
149
  return unless notice[:context][key]
133
150
  return if notice[:context][key] == FILTERED
134
- return filter_hash(notice[:context][key]) unless should_filter?(key)
151
+ unless should_filter?(key)
152
+ return notice[:context][key] = filter_hash(notice[:context][key])
153
+ end
135
154
 
136
155
  notice[:context][key] = FILTERED
137
156
  end
@@ -19,6 +19,7 @@ module Airbrake
19
19
  notice[:errors].each do |error|
20
20
  error[:backtrace].each do |frame|
21
21
  next unless (file = frame[:file])
22
+
22
23
  file.sub!(/\A#{@root_directory}/, PROJECT_ROOT_LABEL)
23
24
  end
24
25
  end
@@ -23,7 +23,7 @@ module Airbrake
23
23
 
24
24
  # @return [Hash{Symbol=>Regexp}] matchers for certain features of SQL
25
25
  ALL_FEATURES = {
26
- # rubocop:disable Metrics/LineLength
26
+ # rubocop:disable Layout/LineLength
27
27
  single_quotes: /'(?:[^']|'')*?(?:\\'.*|'(?!'))/,
28
28
  double_quotes: /"(?:[^"]|"")*?(?:\\".*|"(?!"))/,
29
29
  dollar_quotes: /(\$(?!\d)[^$]*?\$).*?(?:\1|$)/,
@@ -33,13 +33,13 @@ module Airbrake
33
33
  hexadecimal_literals: /0x[0-9a-fA-F]+/,
34
34
  comments: /(?:#|--).*?(?=\r|\n|$)/i,
35
35
  multi_line_comments: %r{/\*(?:[^/]|/[^*])*?(?:\*/|/\*.*)},
36
- oracle_quoted_strings: /q'\[.*?(?:\]'|$)|q'\{.*?(?:\}'|$)|q'\<.*?(?:\>'|$)|q'\(.*?(?:\)'|$)/
37
- # rubocop:enable Metrics/LineLength
36
+ oracle_quoted_strings: /q'\[.*?(?:\]'|$)|q'\{.*?(?:\}'|$)|q'\<.*?(?:\>'|$)|q'\(.*?(?:\)'|$)/,
37
+ # rubocop:enable Layout/LineLength
38
38
  }.freeze
39
39
 
40
40
  # @return [Regexp] the regexp that is applied after the feature regexps
41
41
  # were used
42
- POST_FILTER = /(?<=[values|in ]\().+(?=\))/i
42
+ POST_FILTER = /(?<=[values|in ]\().+(?=\))/i.freeze
43
43
 
44
44
  # @return [Hash{Symbol=>Array<Symbol>}] a set of features that corresponds
45
45
  # to a certain dialect
@@ -64,7 +64,7 @@ module Airbrake
64
64
  cassandra: %i[
65
65
  single_quotes uuids numeric_literals boolean_literals
66
66
  hexadecimal_literals comments multi_line_comments
67
- ].freeze
67
+ ].freeze,
68
68
  }.freeze
69
69
 
70
70
  # @return [Hash{Symbol=>Regexp}] a set of regexps to check for unmatches
@@ -76,7 +76,7 @@ module Airbrake
76
76
  sqlite: %r{'|/\*|\*/},
77
77
  cassandra: %r{'|/\*|\*/},
78
78
  oracle: %r{'|/\*|\*/},
79
- oracle_enhanced: %r{'|/\*|\*/}
79
+ oracle_enhanced: %r{'|/\*|\*/},
80
80
  }.freeze
81
81
 
82
82
  # @return [Array<Regexp>] the list of queries to be ignored
@@ -89,7 +89,7 @@ module Airbrake
89
89
  /FROM pg_attribute/i,
90
90
  /FROM pg_index/i,
91
91
  /FROM pg_class/i,
92
- /FROM pg_type/i
92
+ /FROM pg_type/i,
93
93
  ].freeze
94
94
 
95
95
  def initialize(dialect)
@@ -16,6 +16,7 @@ module Airbrake
16
16
  # @macro call_filter
17
17
  def call(notice)
18
18
  return if notice[:errors].none? { |error| error[:type] == SYSTEM_EXIT_TYPE }
19
+
19
20
  notice.ignore!
20
21
  end
21
22
  end
@@ -16,7 +16,7 @@ module Airbrake
16
16
  String,
17
17
  Symbol,
18
18
  Regexp,
19
- Numeric
19
+ Numeric,
20
20
  ].freeze
21
21
 
22
22
  # Variables starting with this prefix are not attached to a notice.
@@ -41,8 +41,7 @@ module Airbrake
41
41
  thread_info[:fiber_variables] = vars
42
42
  end
43
43
 
44
- # Present in Ruby 2.3+.
45
- if th.respond_to?(:name) && (name = th.name)
44
+ if (name = th.name)
46
45
  thread_info[:name] = name
47
46
  end
48
47
 
@@ -56,6 +55,7 @@ module Airbrake
56
55
  def thread_variables(th)
57
56
  th.thread_variables.map.with_object({}) do |var, h|
58
57
  next if var.to_s.start_with?(IGNORE_PREFIX)
58
+
59
59
  h[var] = sanitize_value(th.thread_variable_get(var))
60
60
  end
61
61
  end
@@ -63,6 +63,7 @@ module Airbrake
63
63
  def fiber_variables(th)
64
64
  th.keys.map.with_object({}) do |key, h|
65
65
  next if key.to_s.start_with?(IGNORE_PREFIX)
66
+
66
67
  h[key] = sanitize_value(th[key])
67
68
  end
68
69
  end
@@ -72,7 +73,7 @@ module Airbrake
72
73
  thread_info[:group] = th.group.list.map(&:inspect)
73
74
  thread_info[:priority] = th.priority
74
75
 
75
- thread_info[:safe_level] = th.safe_level unless Airbrake::JRUBY
76
+ thread_info[:safe_level] = th.safe_level if Airbrake::HAS_SAFE_LEVEL
76
77
  end
77
78
 
78
79
  def sanitize_value(value)
@@ -0,0 +1,12 @@
1
+ module Airbrake
2
+ # Grouppable adds the `#groups` method, so that we don't need to define it in
3
+ # all of performance models every time we add a model without groups.
4
+ #
5
+ # @since v4.9.0
6
+ # @api private
7
+ module Grouppable
8
+ def groups
9
+ {}
10
+ end
11
+ end
12
+ end
@@ -38,6 +38,7 @@ module Airbrake
38
38
  # @raise [Airbrake::Error] when instance is ignored
39
39
  def raise_if_ignored
40
40
  return unless ignored?
41
+
41
42
  raise Airbrake::Error, "cannot access ignored #{self.class}"
42
43
  end
43
44
  end
@@ -21,7 +21,7 @@ module Airbrake
21
21
  project_id: @config.project_id,
22
22
  project_key: @config.project_key,
23
23
  host: @config.host,
24
- filter_chain: @filter_chain.inspect
24
+ filter_chain: @filter_chain.inspect,
25
25
  )
26
26
  end
27
27
 
@@ -30,7 +30,7 @@ module Airbrake
30
30
  q.text("#<#{self.class}:0x#{(object_id << 1).to_s(16).rjust(16, '0')} ")
31
31
  q.text(
32
32
  "project_id=\"#{@config.project_id}\" project_key=\"#{@config.project_key}\" " \
33
- "host=\"#{@config.host}\" filter_chain="
33
+ "host=\"#{@config.host}\" filter_chain=",
34
34
  )
35
35
  q.pp(@filter_chain)
36
36
  q.text('>')
@@ -22,7 +22,7 @@ module Airbrake
22
22
 
23
23
  # @return [Logger]
24
24
  def instance
25
- @instance ||= ::Logger.new(File::NULL)
25
+ @instance ||= ::Logger.new(File::NULL).tap { |l| l.level = ::Logger::WARN }
26
26
  end
27
27
  end
28
28
 
@@ -0,0 +1,12 @@
1
+ module Airbrake
2
+ # Mergeable adds the `#merge` method, so that we don't need to define it in
3
+ # all of performance models every time we add a model.
4
+ #
5
+ # @since v4.9.0
6
+ # @api private
7
+ module Mergeable
8
+ def merge(_other)
9
+ nil
10
+ end
11
+ end
12
+ end
@@ -16,6 +16,11 @@ module Airbrake
16
16
  time_in_nanoseconds / (10.0**6)
17
17
  end
18
18
 
19
+ # @return [Integer] current monotonic time in seconds
20
+ def time_in_s
21
+ time_in_nanoseconds / (10.0**9)
22
+ end
23
+
19
24
  private
20
25
 
21
26
  if defined?(Process::CLOCK_MONOTONIC)
@@ -4,19 +4,12 @@ module Airbrake
4
4
  #
5
5
  # @since v1.0.0
6
6
  class Notice
7
- # @return [Hash{Symbol=>String}] the information about the notifier library
8
- NOTIFIER = {
9
- name: 'airbrake-ruby'.freeze,
10
- version: Airbrake::AIRBRAKE_RUBY_VERSION,
11
- url: 'https://github.com/airbrake/airbrake-ruby'.freeze
12
- }.freeze
13
-
14
7
  # @return [Hash{Symbol=>String,Hash}] the information to be displayed in the
15
8
  # Context tab in the dashboard
16
9
  CONTEXT = {
17
10
  os: RUBY_PLATFORM,
18
11
  language: "#{RUBY_ENGINE}/#{RUBY_VERSION}".freeze,
19
- notifier: NOTIFIER
12
+ notifier: Airbrake::NOTIFIER_INFO,
20
13
  }.freeze
21
14
 
22
15
  # @return [Integer] the maxium size of the JSON payload in bytes
@@ -32,7 +25,7 @@ module Airbrake
32
25
  IOError,
33
26
  NotImplementedError,
34
27
  JSON::GeneratorError,
35
- Encoding::UndefinedConversionError
28
+ Encoding::UndefinedConversionError,
36
29
  ].freeze
37
30
 
38
31
  # @return [Array<Symbol>] the list of keys that can be be overwritten with
@@ -60,10 +53,10 @@ module Airbrake
60
53
  errors: NestedException.new(exception).as_json,
61
54
  context: context,
62
55
  environment: {
63
- program_name: $PROGRAM_NAME
56
+ program_name: $PROGRAM_NAME,
64
57
  },
65
58
  session: {},
66
- params: params
59
+ params: params,
67
60
  }
68
61
  @truncator = Airbrake::Truncator.new(PAYLOAD_MAX_SIZE)
69
62
 
@@ -76,7 +69,7 @@ module Airbrake
76
69
  #
77
70
  # @return [Hash{String=>String}, nil]
78
71
  # @api private
79
- def to_json
72
+ def to_json(*_args)
80
73
  loop do
81
74
  begin
82
75
  json = @payload.to_json
@@ -138,7 +131,7 @@ module Airbrake
138
131
  # Make sure we always send hostname.
139
132
  hostname: HOSTNAME,
140
133
 
141
- severity: DEFAULT_SEVERITY
134
+ severity: DEFAULT_SEVERITY,
142
135
  }.merge(CONTEXT).delete_if { |_key, val| val.nil? || val.empty? }
143
136
  end
144
137
 
@@ -152,7 +145,7 @@ module Airbrake
152
145
  logger.error(
153
146
  "#{LOG_LABEL} truncation failed. File an issue at " \
154
147
  "https://github.com/airbrake/airbrake-ruby " \
155
- "and attach the following payload: #{@payload}"
148
+ "and attach the following payload: #{@payload}",
156
149
  )
157
150
  end
158
151
 
@@ -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
@@ -55,7 +55,8 @@ module Airbrake
55
55
  def build_notice(exception, params = {})
56
56
  if @async_sender.closed?
57
57
  raise Airbrake::Error,
58
- "attempted to build #{exception} with closed Airbrake instance"
58
+ "Airbrake is closed; can't build exception: " \
59
+ "#{exception.class}: #{exception}"
59
60
  end
60
61
 
61
62
  if exception.is_a?(Airbrake::Notice)
@@ -81,6 +82,12 @@ module Airbrake
81
82
  @context.merge!(context)
82
83
  end
83
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
+
84
91
  private
85
92
 
86
93
  def convert_to_exception(ex)
@@ -116,7 +123,7 @@ module Airbrake
116
123
 
117
124
  logger.warn(
118
125
  "#{LOG_LABEL} falling back to sync delivery because there are no " \
119
- "running async workers"
126
+ "running async workers",
120
127
  )
121
128
  @sync_sender
122
129
  end
@@ -128,6 +135,7 @@ module Airbrake
128
135
  # If true, then it's likely an internal library error. In this case return
129
136
  # at least some backtrace to simplify debugging.
130
137
  return caller_copy if clean_bt.empty?
138
+
131
139
  clean_bt
132
140
  end
133
141
  end