airbrake-ruby 2.9.0 → 2.10.0

Sign up to get free protection for your applications and to get access to all the features.
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