airbrake-ruby 2.9.0 → 2.10.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/airbrake-ruby.rb +40 -18
  3. data/lib/airbrake-ruby/async_sender.rb +0 -6
  4. data/lib/airbrake-ruby/backtrace.rb +0 -10
  5. data/lib/airbrake-ruby/code_hunk.rb +0 -4
  6. data/lib/airbrake-ruby/config.rb +23 -22
  7. data/lib/airbrake-ruby/config/validator.rb +0 -10
  8. data/lib/airbrake-ruby/file_cache.rb +0 -6
  9. data/lib/airbrake-ruby/filter_chain.rb +0 -5
  10. data/lib/airbrake-ruby/filters/context_filter.rb +1 -0
  11. data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
  12. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +45 -0
  13. data/lib/airbrake-ruby/filters/gem_root_filter.rb +2 -3
  14. data/lib/airbrake-ruby/filters/keys_blacklist.rb +1 -2
  15. data/lib/airbrake-ruby/filters/keys_filter.rb +9 -14
  16. data/lib/airbrake-ruby/filters/keys_whitelist.rb +0 -2
  17. data/lib/airbrake-ruby/filters/root_directory_filter.rb +2 -3
  18. data/lib/airbrake-ruby/filters/system_exit_filter.rb +2 -3
  19. data/lib/airbrake-ruby/filters/thread_filter.rb +2 -4
  20. data/lib/airbrake-ruby/nested_exception.rb +0 -2
  21. data/lib/airbrake-ruby/notice.rb +6 -44
  22. data/lib/airbrake-ruby/notifier.rb +4 -40
  23. data/lib/airbrake-ruby/promise.rb +0 -6
  24. data/lib/airbrake-ruby/response.rb +0 -4
  25. data/lib/airbrake-ruby/sync_sender.rb +0 -4
  26. data/lib/airbrake-ruby/version.rb +1 -3
  27. data/spec/airbrake_spec.rb +71 -140
  28. data/spec/async_sender_spec.rb +9 -0
  29. data/spec/config_spec.rb +4 -0
  30. data/spec/filters/dependency_filter_spec.rb +16 -0
  31. data/spec/filters/exception_attributes_filter_spec.rb +65 -0
  32. data/spec/filters/keys_whitelist_spec.rb +17 -23
  33. data/spec/notice_spec.rb +111 -69
  34. data/spec/notifier_spec.rb +304 -495
  35. data/spec/response_spec.rb +82 -0
  36. data/spec/sync_sender_spec.rb +31 -14
  37. metadata +10 -2
@@ -1,36 +1,29 @@
1
1
  module Airbrake
2
2
  class Config
3
- ##
4
3
  # Validates values of {Airbrake::Config} options.
5
4
  #
6
5
  # @api private
7
6
  # @since v1.5.0
8
7
  class Validator
9
- ##
10
8
  # @return [String]
11
9
  REQUIRED_KEY_MSG = ':project_key is required'.freeze
12
10
 
13
- ##
14
11
  # @return [String]
15
12
  REQUIRED_ID_MSG = ':project_id is required'.freeze
16
13
 
17
- ##
18
14
  # @return [String]
19
15
  WRONG_ENV_TYPE_MSG = "the 'environment' option must be configured " \
20
16
  "with a Symbol (or String), but '%s' was provided: " \
21
17
  '%s'.freeze
22
18
 
23
- ##
24
19
  # @return [Array<Class>] the list of allowed types to configure the
25
20
  # environment option
26
21
  VALID_ENV_TYPES = [NilClass, String, Symbol].freeze
27
22
 
28
- ##
29
23
  # @return [String] error message, if validator was able to find any errors
30
24
  # in the config
31
25
  attr_reader :error_message
32
26
 
33
- ##
34
27
  # Validates given config and stores error message, if any errors were
35
28
  # found.
36
29
  #
@@ -40,7 +33,6 @@ module Airbrake
40
33
  @error_message = nil
41
34
  end
42
35
 
43
- ##
44
36
  # @return [Boolean]
45
37
  def valid_project_id?
46
38
  valid = @config.project_id.to_i > 0
@@ -48,7 +40,6 @@ module Airbrake
48
40
  valid
49
41
  end
50
42
 
51
- ##
52
43
  # @return [Boolean]
53
44
  def valid_project_key?
54
45
  valid = @config.project_key.is_a?(String) && !@config.project_key.empty?
@@ -56,7 +47,6 @@ module Airbrake
56
47
  valid
57
48
  end
58
49
 
59
- ##
60
50
  # @return [Boolean]
61
51
  def valid_environment?
62
52
  environment = @config.environment
@@ -1,19 +1,15 @@
1
1
  module Airbrake
2
- ##
3
2
  # Extremely simple global cache.
4
3
  #
5
4
  # @api private
6
5
  # @since v2.4.1
7
6
  module FileCache
8
- ##
9
7
  # @return [Integer]
10
8
  MAX_SIZE = 50
11
9
 
12
- ##
13
10
  # @return [Mutex]
14
11
  MUTEX = Mutex.new
15
12
 
16
- ##
17
13
  # Associates the value given by +value+ with the key given by +key+. Deletes
18
14
  # entries that exceed +MAX_SIZE+.
19
15
  #
@@ -27,7 +23,6 @@ module Airbrake
27
23
  end
28
24
  end
29
25
 
30
- ##
31
26
  # Retrieve an object from the cache.
32
27
  #
33
28
  # @param [Object] key
@@ -38,7 +33,6 @@ module Airbrake
38
33
  end
39
34
  end
40
35
 
41
- ##
42
36
  # Checks whether the cache is empty. Needed only for the test suite.
43
37
  #
44
38
  # @return [Boolean]
@@ -1,5 +1,4 @@
1
1
  module Airbrake
2
- ##
3
2
  # Represents the mechanism for filtering notices. Defines a few default
4
3
  # filters.
5
4
  #
@@ -7,7 +6,6 @@ module Airbrake
7
6
  # @api private
8
7
  # @since v1.0.0
9
8
  class FilterChain
10
- ##
11
9
  # @return [Array<Class>] filters to be executed first
12
10
  DEFAULT_FILTERS = [
13
11
  Airbrake::Filters::SystemExitFilter,
@@ -17,7 +15,6 @@ module Airbrake
17
15
  # Airbrake::Filters::ThreadFilter
18
16
  ].freeze
19
17
 
20
- ##
21
18
  # @return [Integer]
22
19
  DEFAULT_WEIGHT = 0
23
20
 
@@ -26,7 +23,6 @@ module Airbrake
26
23
  DEFAULT_FILTERS.each { |f| add_filter(f.new) }
27
24
  end
28
25
 
29
- ##
30
26
  # Adds a filter to the filter chain. Sorts filters by weight.
31
27
  #
32
28
  # @param [#call] filter The filter object (proc, class, module, etc)
@@ -37,7 +33,6 @@ module Airbrake
37
33
  end.reverse!
38
34
  end
39
35
 
40
- ##
41
36
  # Applies all the filters in the filter chain to the given notice. Does not
42
37
  # filter ignored notices.
43
38
  #
@@ -15,6 +15,7 @@ module Airbrake
15
15
  @mutex = Mutex.new
16
16
  end
17
17
 
18
+ # @macro call_filter
18
19
  def call(notice)
19
20
  @mutex.synchronize do
20
21
  return if @context.empty?
@@ -0,0 +1,31 @@
1
+ module Airbrake
2
+ module Filters
3
+ # Attaches loaded dependencies to the notice object.
4
+ #
5
+ # @api private
6
+ # @since v2.10.0
7
+ class DependencyFilter
8
+ def initialize
9
+ @weight = 117
10
+ end
11
+
12
+ # @macro call_filter
13
+ def call(notice)
14
+ deps = {}
15
+ Gem.loaded_specs.map.with_object(deps) do |(name, spec), h|
16
+ h[name] = "#{spec.version}#{git_version(spec)}"
17
+ end
18
+
19
+ notice[:context][:versions] = {} unless notice[:context].key?(:versions)
20
+ notice[:context][:versions][:dependencies] = deps
21
+ end
22
+
23
+ private
24
+
25
+ def git_version(spec)
26
+ return unless spec.respond_to?(:git_version) || spec.git_version
27
+ spec.git_version.to_s
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,45 @@
1
+ module Airbrake
2
+ module Filters
3
+ # ExceptionAttributesFilter attempts to call `#to_airbrake` on the stashed
4
+ # exception and attaches returned data to the notice object.
5
+ #
6
+ # @api private
7
+ # @since v2.10.0
8
+ class ExceptionAttributesFilter
9
+ def initialize(logger)
10
+ @logger = logger
11
+ @weight = 118
12
+ end
13
+
14
+ # @macro call_filter
15
+ def call(notice)
16
+ exception = notice.stash[:exception]
17
+ return unless exception.respond_to?(:to_airbrake)
18
+
19
+ attributes = nil
20
+ begin
21
+ attributes = exception.to_airbrake
22
+ rescue StandardError => ex
23
+ @logger.error(
24
+ "#{LOG_LABEL} #{exception.class}#to_airbrake failed. #{ex.class}: #{ex}"
25
+ )
26
+ end
27
+
28
+ unless attributes.is_a?(Hash)
29
+ @logger.error(
30
+ "#{LOG_LABEL} #{self.class}: wanted Hash, got #{attributes.class}"
31
+ )
32
+ return
33
+ end
34
+
35
+ attributes.each do |key, attrs|
36
+ if notice[key]
37
+ notice[key].merge!(attrs)
38
+ else
39
+ notice[key] = attrs
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,13 +1,11 @@
1
1
  module Airbrake
2
2
  module Filters
3
- ##
4
3
  # Replaces paths to gems with a placeholder.
4
+ # @api private
5
5
  class GemRootFilter
6
- ##
7
6
  # @return [String]
8
7
  GEM_ROOT_LABEL = '/GEM_ROOT'.freeze
9
8
 
10
- ##
11
9
  # @return [Integer]
12
10
  attr_reader :weight
13
11
 
@@ -15,6 +13,7 @@ module Airbrake
15
13
  @weight = 120
16
14
  end
17
15
 
16
+ # @macro call_filter
18
17
  def call(notice)
19
18
  return unless defined?(Gem)
20
19
 
@@ -1,6 +1,5 @@
1
1
  module Airbrake
2
2
  module Filters
3
- ##
4
3
  # A default Airbrake notice filter. Filters only specific keys listed in the
5
4
  # list of parameters in the payload of a notice.
6
5
  #
@@ -26,6 +25,7 @@ module Airbrake
26
25
  #
27
26
  # @see KeysWhitelist
28
27
  # @see KeysFilter
28
+ # @api private
29
29
  class KeysBlacklist
30
30
  include KeysFilter
31
31
 
@@ -34,7 +34,6 @@ module Airbrake
34
34
  @weight = -110
35
35
  end
36
36
 
37
- ##
38
37
  # @return [Boolean] true if the key matches at least one pattern, false
39
38
  # otherwise
40
39
  def should_filter?(key)
@@ -1,6 +1,7 @@
1
1
  module Airbrake
2
+ # Namespace for all standard filters. Custom filters can also go under this
3
+ # namespace.
2
4
  module Filters
3
- ##
4
5
  # This is a filter helper that endows a class ability to filter notices'
5
6
  # payload based on the return value of the +should_filter?+ method that a
6
7
  # class that includes this module must implement.
@@ -8,31 +9,26 @@ module Airbrake
8
9
  # @see Notice
9
10
  # @see KeysWhitelist
10
11
  # @see KeysBlacklist
12
+ # @api private
11
13
  module KeysFilter
12
- ##
13
14
  # @return [String] The label to replace real values of filtered payload
14
15
  FILTERED = '[Filtered]'.freeze
15
16
 
16
- ##
17
17
  # @return [Array<String,Symbol,Regexp>] the array of classes instances of
18
18
  # which can compared with payload keys
19
19
  VALID_PATTERN_CLASSES = [String, Symbol, Regexp].freeze
20
20
 
21
- ##
22
21
  # @return [Array<Symbol>] parts of a Notice's payload that can be modified
23
22
  # by blacklist/whitelist filters
24
23
  FILTERABLE_KEYS = %i[environment session params].freeze
25
24
 
26
- ##
27
25
  # @return [Array<Symbol>] parts of a Notice's *context* payload that can
28
26
  # be modified by blacklist/whitelist filters
29
27
  FILTERABLE_CONTEXT_KEYS = %i[user headers].freeze
30
28
 
31
- ##
32
29
  # @return [Integer]
33
30
  attr_reader :weight
34
31
 
35
- ##
36
32
  # Creates a new KeysBlacklist or KeysWhitelist filter that uses the given
37
33
  # +patterns+ for filtering a notice's payload.
38
34
  #
@@ -44,13 +40,13 @@ module Airbrake
44
40
  @valid_patterns = false
45
41
  end
46
42
 
47
- ##
48
- # This is a mandatory method required by any filter integrated with
49
- # FilterChain.
43
+ # @!macro call_filter
44
+ # This is a mandatory method required by any filter integrated with
45
+ # FilterChain.
50
46
  #
51
- # @param [Notice] notice the notice to be filtered
52
- # @return [void]
53
- # @see FilterChain
47
+ # @param [Notice] notice the notice to be filtered
48
+ # @return [void]
49
+ # @see FilterChain
54
50
  def call(notice)
55
51
  unless @valid_patterns
56
52
  eval_proc_patterns!
@@ -64,7 +60,6 @@ module Airbrake
64
60
  filter_url(notice)
65
61
  end
66
62
 
67
- ##
68
63
  # @raise [NotImplementedError] if called directly
69
64
  def should_filter?(_key)
70
65
  raise NotImplementedError, 'method must be implemented in the included class'
@@ -1,6 +1,5 @@
1
1
  module Airbrake
2
2
  module Filters
3
- ##
4
3
  # A default Airbrake notice filter. Filters everything in the payload of a
5
4
  # notice, but specified keys.
6
5
  #
@@ -34,7 +33,6 @@ module Airbrake
34
33
  @weight = -100
35
34
  end
36
35
 
37
- ##
38
36
  # @return [Boolean] true if the key doesn't match any pattern, false
39
37
  # otherwise.
40
38
  def should_filter?(key)
@@ -1,13 +1,11 @@
1
1
  module Airbrake
2
2
  module Filters
3
- ##
4
3
  # Replaces root directory with a label.
4
+ # @api private
5
5
  class RootDirectoryFilter
6
- ##
7
6
  # @return [String]
8
7
  PROJECT_ROOT_LABEL = '/PROJECT_ROOT'.freeze
9
8
 
10
- ##
11
9
  # @return [Integer]
12
10
  attr_reader :weight
13
11
 
@@ -16,6 +14,7 @@ module Airbrake
16
14
  @weight = 100
17
15
  end
18
16
 
17
+ # @macro call_filter
19
18
  def call(notice)
20
19
  notice[:errors].each do |error|
21
20
  error[:backtrace].each do |frame|
@@ -1,13 +1,11 @@
1
1
  module Airbrake
2
2
  module Filters
3
- ##
4
3
  # Skip over SystemExit exceptions, because they're just noise.
4
+ # @api private
5
5
  class SystemExitFilter
6
- ##
7
6
  # @return [String]
8
7
  SYSTEM_EXIT_TYPE = 'SystemExit'.freeze
9
8
 
10
- ##
11
9
  # @return [Integer]
12
10
  attr_reader :weight
13
11
 
@@ -15,6 +13,7 @@ module Airbrake
15
13
  @weight = 130
16
14
  end
17
15
 
16
+ # @macro call_filter
18
17
  def call(notice)
19
18
  return if notice[:errors].none? { |error| error[:type] == SYSTEM_EXIT_TYPE }
20
19
  notice.ignore!
@@ -1,14 +1,12 @@
1
1
  module Airbrake
2
2
  module Filters
3
- ##
4
3
  # Attaches thread & fiber local variables along with general thread
5
4
  # information.
5
+ # @api private
6
6
  class ThreadFilter
7
- ##
8
7
  # @return [Integer]
9
8
  attr_reader :weight
10
9
 
11
- ##
12
10
  # @return [Array<Class>] the list of classes that can be safely converted
13
11
  # to JSON
14
12
  SAFE_CLASSES = [
@@ -21,7 +19,6 @@ module Airbrake
21
19
  Numeric
22
20
  ].freeze
23
21
 
24
- ##
25
22
  # Variables starting with this prefix are not attached to a notice.
26
23
  # @see https://github.com/airbrake/airbrake-ruby/issues/229
27
24
  # @return [String]
@@ -31,6 +28,7 @@ module Airbrake
31
28
  @weight = 110
32
29
  end
33
30
 
31
+ # @macro call_filter
34
32
  def call(notice)
35
33
  th = Thread.current
36
34
  thread_info = {}
@@ -1,12 +1,10 @@
1
1
  module Airbrake
2
- ##
3
2
  # A class that is capable of unwinding nested exceptions and representing them
4
3
  # as JSON-like hash.
5
4
  #
6
5
  # @api private
7
6
  # @since v1.0.4
8
7
  class NestedException
9
- ##
10
8
  # @return [Integer] the maximum number of nested exceptions that a notice
11
9
  # can unwrap. Exceptions that have a longer cause chain will be ignored
12
10
  MAX_NESTED_EXCEPTIONS = 3
@@ -1,11 +1,9 @@
1
1
  module Airbrake
2
- ##
3
2
  # Represents a chunk of information that is meant to be either sent to
4
3
  # Airbrake or ignored completely.
5
4
  #
6
5
  # @since v1.0.0
7
6
  class Notice
8
- ##
9
7
  # @return [Hash{Symbol=>String}] the information about the notifier library
10
8
  NOTIFIER = {
11
9
  name: 'airbrake-ruby'.freeze,
@@ -13,7 +11,6 @@ module Airbrake
13
11
  url: 'https://github.com/airbrake/airbrake-ruby'.freeze
14
12
  }.freeze
15
13
 
16
- ##
17
14
  # @return [Hash{Symbol=>String,Hash}] the information to be displayed in the
18
15
  # Context tab in the dashboard
19
16
  CONTEXT = {
@@ -22,16 +19,13 @@ module Airbrake
22
19
  notifier: NOTIFIER
23
20
  }.freeze
24
21
 
25
- ##
26
22
  # @return [Integer] the maxium size of the JSON payload in bytes
27
23
  MAX_NOTICE_SIZE = 64000
28
24
 
29
- ##
30
25
  # @return [Integer] the maximum size of hashes, arrays and strings in the
31
26
  # notice.
32
27
  PAYLOAD_MAX_SIZE = 10000
33
28
 
34
- ##
35
29
  # @return [Array<StandardError>] the list of possible exceptions that might
36
30
  # be raised when an object is converted to JSON
37
31
  JSON_EXCEPTIONS = [
@@ -45,25 +39,22 @@ module Airbrake
45
39
  # {Airbrake::Notice#[]=}
46
40
  WRITABLE_KEYS = %i[notifier context environment session params].freeze
47
41
 
48
- ##
49
42
  # @return [Array<Symbol>] parts of a Notice's payload that can be modified
50
43
  # by the truncator
51
44
  TRUNCATABLE_KEYS = %i[errors environment session params].freeze
52
45
 
53
- ##
54
46
  # @return [String] the name of the host machine
55
47
  HOSTNAME = Socket.gethostname.freeze
56
48
 
57
- ##
58
49
  # @return [String]
59
50
  DEFAULT_SEVERITY = 'error'.freeze
60
51
 
61
- ##
62
52
  # @since v1.7.0
63
53
  # @return [Hash{Symbol=>Object}] the hash with arbitrary objects to be used
64
54
  # in filters
65
55
  attr_reader :stash
66
56
 
57
+ # @api private
67
58
  def initialize(config, exception, params = {})
68
59
  @config = config
69
60
 
@@ -78,16 +69,14 @@ module Airbrake
78
69
  }
79
70
  @stash = { exception: exception }
80
71
  @truncator = Airbrake::Truncator.new(PAYLOAD_MAX_SIZE)
81
-
82
- extract_custom_attributes(exception)
83
72
  end
84
73
 
85
- ##
86
74
  # Converts the notice to JSON. Calls +to_json+ on each object inside
87
75
  # notice's payload. Truncates notices, JSON representation of which is
88
76
  # bigger than {MAX_NOTICE_SIZE}.
89
77
  #
90
78
  # @return [Hash{String=>String}, nil]
79
+ # @api private
91
80
  def to_json
92
81
  loop do
93
82
  begin
@@ -102,7 +91,6 @@ module Airbrake
102
91
  end
103
92
  end
104
93
 
105
- ##
106
94
  # Ignores a notice. Ignored notices never reach the Airbrake dashboard.
107
95
  #
108
96
  # @return [void]
@@ -112,7 +100,6 @@ module Airbrake
112
100
  @payload = nil
113
101
  end
114
102
 
115
- ##
116
103
  # Checks whether the notice was ignored.
117
104
  #
118
105
  # @return [Boolean]
@@ -121,19 +108,17 @@ module Airbrake
121
108
  @payload.nil?
122
109
  end
123
110
 
124
- ##
125
111
  # Reads a value from notice's payload.
126
- # @return [Object]
127
112
  #
113
+ # @return [Object]
128
114
  # @raise [Airbrake::Error] if the notice is ignored
129
115
  def [](key)
130
116
  raise_if_ignored
131
117
  @payload[key]
132
118
  end
133
119
 
134
- ##
135
- # Writes a value to the payload hash. Restricts unrecognized
136
- # writes.
120
+ # Writes a value to the payload hash. Restricts unrecognized writes.
121
+ #
137
122
  # @example
138
123
  # notice[:params][:my_param] = 'foobar'
139
124
  #
@@ -161,6 +146,7 @@ module Airbrake
161
146
  def context
162
147
  {
163
148
  version: @config.app_version,
149
+ versions: @config.versions,
164
150
  # We ensure that root_directory is always a String, so it can always be
165
151
  # converted to JSON in a predictable manner (when it's a Pathname and in
166
152
  # Rails environment, it converts to unexpected JSON).
@@ -195,29 +181,5 @@ module Airbrake
195
181
 
196
182
  new_max_size
197
183
  end
198
-
199
- def extract_custom_attributes(exception)
200
- return unless exception.respond_to?(:to_airbrake)
201
- attributes = nil
202
-
203
- begin
204
- attributes = exception.to_airbrake
205
- rescue StandardError => ex
206
- @config.logger.error(
207
- "#{LOG_LABEL} #{exception.class}#to_airbrake failed: #{ex.class}: #{ex}"
208
- )
209
- end
210
-
211
- return unless attributes
212
-
213
- begin
214
- @payload.merge!(attributes)
215
- rescue TypeError
216
- @config.logger.error(
217
- "#{LOG_LABEL} #{exception.class}#to_airbrake failed:" \
218
- " #{attributes} must be a Hash"
219
- )
220
- end
221
- end
222
184
  end
223
185
  end