airbrake-ruby 4.15.0 → 6.1.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/lib/airbrake-ruby/async_sender.rb +4 -2
  3. data/lib/airbrake-ruby/backtrace.rb +6 -5
  4. data/lib/airbrake-ruby/config/processor.rb +77 -0
  5. data/lib/airbrake-ruby/config/validator.rb +6 -0
  6. data/lib/airbrake-ruby/config.rb +44 -35
  7. data/lib/airbrake-ruby/context.rb +51 -0
  8. data/lib/airbrake-ruby/file_cache.rb +1 -1
  9. data/lib/airbrake-ruby/filter_chain.rb +3 -0
  10. data/lib/airbrake-ruby/filters/context_filter.rb +4 -5
  11. data/lib/airbrake-ruby/filters/dependency_filter.rb +1 -0
  12. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +1 -1
  13. data/lib/airbrake-ruby/filters/gem_root_filter.rb +1 -0
  14. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +3 -4
  15. data/lib/airbrake-ruby/filters/git_repository_filter.rb +11 -2
  16. data/lib/airbrake-ruby/filters/git_revision_filter.rb +3 -1
  17. data/lib/airbrake-ruby/filters/keys_filter.rb +23 -15
  18. data/lib/airbrake-ruby/filters/root_directory_filter.rb +1 -0
  19. data/lib/airbrake-ruby/filters/sql_filter.rb +11 -11
  20. data/lib/airbrake-ruby/filters/system_exit_filter.rb +1 -0
  21. data/lib/airbrake-ruby/filters/thread_filter.rb +4 -3
  22. data/lib/airbrake-ruby/grouppable.rb +1 -1
  23. data/lib/airbrake-ruby/ignorable.rb +1 -2
  24. data/lib/airbrake-ruby/mergeable.rb +1 -1
  25. data/lib/airbrake-ruby/monotonic_time.rb +1 -1
  26. data/lib/airbrake-ruby/notice.rb +1 -8
  27. data/lib/airbrake-ruby/notice_notifier.rb +4 -4
  28. data/lib/airbrake-ruby/performance_breakdown.rb +1 -6
  29. data/lib/airbrake-ruby/performance_notifier.rb +40 -54
  30. data/lib/airbrake-ruby/promise.rb +1 -0
  31. data/lib/airbrake-ruby/query.rb +1 -6
  32. data/lib/airbrake-ruby/queue.rb +1 -8
  33. data/lib/airbrake-ruby/remote_settings/callback.rb +44 -0
  34. data/lib/airbrake-ruby/remote_settings/settings_data.rb +116 -0
  35. data/lib/airbrake-ruby/remote_settings.rb +128 -0
  36. data/lib/airbrake-ruby/request.rb +1 -8
  37. data/lib/airbrake-ruby/stat.rb +2 -13
  38. data/lib/airbrake-ruby/sync_sender.rb +3 -2
  39. data/lib/airbrake-ruby/tdigest.rb +12 -9
  40. data/lib/airbrake-ruby/thread_pool.rb +9 -6
  41. data/lib/airbrake-ruby/time_truncate.rb +2 -2
  42. data/lib/airbrake-ruby/timed_trace.rb +1 -3
  43. data/lib/airbrake-ruby/truncator.rb +8 -2
  44. data/lib/airbrake-ruby/version.rb +11 -1
  45. data/lib/airbrake-ruby.rb +44 -54
  46. data/spec/airbrake_spec.rb +178 -92
  47. data/spec/async_sender_spec.rb +10 -8
  48. data/spec/backtrace_spec.rb +39 -36
  49. data/spec/benchmark_spec.rb +7 -5
  50. data/spec/code_hunk_spec.rb +26 -17
  51. data/spec/config/processor_spec.rb +167 -0
  52. data/spec/config/validator_spec.rb +23 -3
  53. data/spec/config_spec.rb +43 -55
  54. data/spec/context_spec.rb +54 -0
  55. data/spec/deploy_notifier_spec.rb +6 -4
  56. data/spec/file_cache_spec.rb +1 -0
  57. data/spec/filter_chain_spec.rb +29 -24
  58. data/spec/filters/context_filter_spec.rb +14 -5
  59. data/spec/filters/dependency_filter_spec.rb +3 -1
  60. data/spec/filters/exception_attributes_filter_spec.rb +5 -3
  61. data/spec/filters/gem_root_filter_spec.rb +9 -6
  62. data/spec/filters/git_last_checkout_filter_spec.rb +10 -12
  63. data/spec/filters/{git_repository_filter.rb → git_repository_filter_spec.rb} +26 -15
  64. data/spec/filters/git_revision_filter_spec.rb +20 -20
  65. data/spec/filters/keys_allowlist_spec.rb +26 -16
  66. data/spec/filters/keys_blocklist_spec.rb +35 -18
  67. data/spec/filters/root_directory_filter_spec.rb +7 -7
  68. data/spec/filters/sql_filter_spec.rb +28 -28
  69. data/spec/filters/system_exit_filter_spec.rb +4 -2
  70. data/spec/filters/thread_filter_spec.rb +16 -14
  71. data/spec/loggable_spec.rb +2 -2
  72. data/spec/monotonic_time_spec.rb +8 -6
  73. data/spec/nested_exception_spec.rb +46 -46
  74. data/spec/notice_notifier/options_spec.rb +25 -15
  75. data/spec/notice_notifier_spec.rb +54 -49
  76. data/spec/notice_spec.rb +7 -3
  77. data/spec/performance_breakdown_spec.rb +0 -12
  78. data/spec/performance_notifier_spec.rb +69 -87
  79. data/spec/promise_spec.rb +38 -32
  80. data/spec/query_spec.rb +1 -11
  81. data/spec/queue_spec.rb +1 -13
  82. data/spec/remote_settings/callback_spec.rb +162 -0
  83. data/spec/remote_settings/settings_data_spec.rb +348 -0
  84. data/spec/remote_settings_spec.rb +201 -0
  85. data/spec/request_spec.rb +1 -13
  86. data/spec/response_spec.rb +34 -12
  87. data/spec/spec_helper.rb +4 -4
  88. data/spec/stashable_spec.rb +5 -5
  89. data/spec/stat_spec.rb +7 -14
  90. data/spec/sync_sender_spec.rb +52 -17
  91. data/spec/tdigest_spec.rb +61 -56
  92. data/spec/thread_pool_spec.rb +67 -58
  93. data/spec/time_truncate_spec.rb +23 -6
  94. data/spec/timed_trace_spec.rb +32 -30
  95. data/spec/truncator_spec.rb +72 -43
  96. metadata +67 -51
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9737dfd8078f14a0150525e1c8b226bcaa5bb87e7627542d7a1a25a27d3c191d
4
- data.tar.gz: 653ff8929bc7199a77c59ead004167a38d214741157282d93e947c3d5ca8ecd6
3
+ metadata.gz: 2dfccc9b2cf013d06ad40e4b3726319c13bd09dcba70d9c1fa4b382c7d80b1b4
4
+ data.tar.gz: 63c8aa81c33948f26ead281e81769a274dbca9740880a420d62b4d951b352984
5
5
  SHA512:
6
- metadata.gz: 21d60bd1415bec1e0ed3b89891f5b35e56f019b09dcb50c05f1e55b2176a2976a566c95e0a3fc4cdce2601a1b4b0e786b1ddbcc67188da6df51bd5c94188e54d
7
- data.tar.gz: 3c3b2312b7945685c6199ecd21f23601b752e1d4234d35461db2f4c7cf0318c24ee263df2536e9cffb7e107002a5a5be10af1cf7eecc16ac65e07cd948f2c68e
6
+ metadata.gz: 694a9114eb130976c052748a51e0490b3276bf32fe347bf466e64caf68e26c703941cd131bb43a0b7f3b6c6c9580067feffd5c1d443a46319a09ec6d0d1f6a37
7
+ data.tar.gz: 59bf1fd79c2467401aabc0c352397d82bae83805cb54ccac40c5ca132dfcfdefa5281a8ba11dc96f67d569405837af61aeb05a22ce9e2f3425d0ecc4a1c69251
@@ -7,16 +7,17 @@ module Airbrake
7
7
  class AsyncSender
8
8
  include Loggable
9
9
 
10
- def initialize(method = :post)
10
+ def initialize(method = :post, name = 'async-sender')
11
11
  @config = Airbrake::Config.instance
12
12
  @method = method
13
+ @name = name
13
14
  end
14
15
 
15
16
  # Asynchronously sends a notice to Airbrake.
16
17
  #
17
18
  # @param [Hash] payload Whatever needs to be sent
18
19
  # @return [Airbrake::Promise]
19
- def send(payload, promise, endpoint = @config.endpoint)
20
+ def send(payload, promise, endpoint = @config.error_endpoint)
20
21
  unless thread_pool << [payload, promise, endpoint]
21
22
  return promise.reject(
22
23
  "AsyncSender has reached its capacity of #{@config.queue_size}",
@@ -47,6 +48,7 @@ module Airbrake
47
48
  @thread_pool ||= begin
48
49
  sender = SyncSender.new(@method)
49
50
  ThreadPool.new(
51
+ name: @name,
50
52
  worker_size: @config.workers,
51
53
  queue_size: @config.queue_size,
52
54
  block: proc { |args| sender.send(*args) },
@@ -22,7 +22,7 @@ module Airbrake
22
22
  (?<line>\d+) # Matches '43'
23
23
  :in\s
24
24
  `(?<function>.*)' # Matches "`block (3 levels) in <top (required)>'"
25
- \z}x
25
+ \z}x.freeze
26
26
 
27
27
  # @return [Regexp] the pattern that matches JRuby Java stack frames, such
28
28
  # as org.jruby.ast.NewlineNode.interpret(NewlineNode.java:105)
@@ -39,7 +39,7 @@ module Airbrake
39
39
  :?
40
40
  (?<line>\d+)? # Matches '105'
41
41
  \)
42
- \z}x
42
+ \z}x.freeze
43
43
 
44
44
  # @return [Regexp] the pattern that tries to assume what a generic stack
45
45
  # frame might look like, when exception's backtrace is set manually.
@@ -53,7 +53,7 @@ module Airbrake
53
53
  |
54
54
  :in\s(?<function>.+) # Matches ":in func"
55
55
  )? # ... or nothing
56
- \z}x
56
+ \z}x.freeze
57
57
 
58
58
  # @return [Regexp] the pattern that matches exceptions from PL/SQL such as
59
59
  # ORA-06512: at "STORE.LI_LICENSES_PACK", line 1945
@@ -67,7 +67,7 @@ module Airbrake
67
67
  |
68
68
  #{GENERIC}
69
69
  )
70
- \z/x
70
+ \z/x.freeze
71
71
 
72
72
  # @return [Regexp] the pattern that matches CoffeeScript backtraces
73
73
  # usually coming from Rails & ExecJS
@@ -82,7 +82,7 @@ module Airbrake
82
82
  # Matches the Ruby part of the backtrace
83
83
  #{RUBY}
84
84
  )
85
- \z/x
85
+ \z/x.freeze
86
86
  end
87
87
 
88
88
  # @return [Integer] how many first frames should include code hunks
@@ -95,6 +95,7 @@ module Airbrake
95
95
  # @return [Array<Hash{Symbol=>String,Integer}>] the parsed backtrace
96
96
  def self.parse(exception)
97
97
  return [] if exception.backtrace.nil? || exception.backtrace.none?
98
+
98
99
  parse_backtrace(exception)
99
100
  end
100
101
 
@@ -0,0 +1,77 @@
1
+ module Airbrake
2
+ class Config
3
+ # Processor is a helper class, which is responsible for setting default
4
+ # config values, default notifier filters and remote configuration changes.
5
+ #
6
+ # @since v5.0.0
7
+ # @api private
8
+ class Processor
9
+ # @param [Airbrake::Config] config
10
+ # @return [Airbrake::Config::Processor]
11
+ def self.process(config)
12
+ new(config).process
13
+ end
14
+
15
+ # @param [Airbrake::Config] config
16
+ def initialize(config)
17
+ @config = config
18
+ @blocklist_keys = @config.blocklist_keys
19
+ @allowlist_keys = @config.allowlist_keys
20
+ @project_id = @config.project_id
21
+ @poll_callback = Airbrake::RemoteSettings::Callback.new(config)
22
+ end
23
+
24
+ # @param [Airbrake::NoticeNotifier] notifier
25
+ # @return [void]
26
+ def process_blocklist(notifier)
27
+ return if @blocklist_keys.none?
28
+
29
+ blocklist = Airbrake::Filters::KeysBlocklist.new(@blocklist_keys)
30
+ notifier.add_filter(blocklist)
31
+ end
32
+
33
+ # @param [Airbrake::NoticeNotifier] notifier
34
+ # @return [void]
35
+ def process_allowlist(notifier)
36
+ return if @allowlist_keys.none?
37
+
38
+ allowlist = Airbrake::Filters::KeysAllowlist.new(@allowlist_keys)
39
+ notifier.add_filter(allowlist)
40
+ end
41
+
42
+ # @return [Airbrake::RemoteSettings]
43
+ def process_remote_configuration
44
+ return unless @config.remote_config
45
+ return unless @project_id
46
+
47
+ # Never poll remote configuration in the test environment.
48
+ return if @config.environment == 'test'
49
+
50
+ # If the current environment is ignored, don't try to poll remote
51
+ # configuration.
52
+ return if @config.ignore_environments.include?(@config.environment)
53
+
54
+ RemoteSettings.poll(@project_id, @config.remote_config_host) do |data|
55
+ @poll_callback.call(data)
56
+ end
57
+ end
58
+
59
+ # @param [Airbrake::NoticeNotifier] notifier
60
+ # @return [void]
61
+ def add_filters(notifier)
62
+ return unless @config.root_directory
63
+
64
+ [
65
+ Airbrake::Filters::RootDirectoryFilter,
66
+ Airbrake::Filters::GitRevisionFilter,
67
+ Airbrake::Filters::GitRepositoryFilter,
68
+ Airbrake::Filters::GitLastCheckoutFilter,
69
+ ].each do |filter|
70
+ next if notifier.has_filter?(filter)
71
+
72
+ notifier.add_filter(filter.new(@config.root_directory))
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -44,6 +44,10 @@ module Airbrake
44
44
  def check_notify_ability(config)
45
45
  promise = Airbrake::Promise.new
46
46
 
47
+ unless config.error_notifications
48
+ return promise.reject('error notifications are disabled')
49
+ end
50
+
47
51
  if ignored_environment?(config)
48
52
  return promise.reject(
49
53
  "current environment '#{config.environment}' is ignored",
@@ -57,12 +61,14 @@ module Airbrake
57
61
 
58
62
  def valid_project_id?(config)
59
63
  return true if config.project_id.to_i > 0
64
+
60
65
  false
61
66
  end
62
67
 
63
68
  def valid_project_key?(config)
64
69
  return false unless config.project_key.is_a?(String)
65
70
  return false if config.project_key.empty?
71
+
66
72
  true
67
73
  end
68
74
 
@@ -46,6 +46,17 @@ module Airbrake
46
46
  # @api public
47
47
  attr_accessor :host
48
48
 
49
+ # @since v5.0.0
50
+ alias error_host host
51
+ # @since v5.0.0
52
+ alias error_host= host=
53
+
54
+ # @return [String] the host, which provides the API endpoint to which
55
+ # APM data should be sent
56
+ # @api public
57
+ # @since v5.0.0
58
+ attr_accessor :apm_host
59
+
49
60
  # @return [String, Pathname] the working directory of your project
50
61
  # @api public
51
62
  attr_accessor :root_directory
@@ -71,18 +82,12 @@ module Airbrake
71
82
  # @since v4.15.0
72
83
  attr_accessor :allowlist_keys
73
84
 
74
- # @deprecated Use allowlist_keys instead
75
- alias whitelist_keys allowlist_keys
76
-
77
85
  # @return [Array<String, Symbol, Regexp>] the keys, which should be
78
86
  # filtered
79
87
  # @api public
80
88
  # @since v4.15.0
81
89
  attr_accessor :blocklist_keys
82
90
 
83
- # @deprecated Use blocklist_keys instead
84
- alias blacklist_keys blocklist_keys
85
-
86
91
  # @return [Boolean] true if the library should attach code hunks to each
87
92
  # frame in a backtrace, false otherwise
88
93
  # @api public
@@ -113,6 +118,24 @@ module Airbrake
113
118
  # @since v4.12.0
114
119
  attr_accessor :job_stats
115
120
 
121
+ # @return [Boolean] true if the library should send error reports to
122
+ # Airbrake, false otherwise
123
+ # @api public
124
+ # @since v5.0.0
125
+ attr_accessor :error_notifications
126
+
127
+ # @return [String] the host which should be used for fetching remote
128
+ # configuration options
129
+ # @api public
130
+ # @since v5.0.0
131
+ attr_accessor :remote_config_host
132
+
133
+ # @return [String] true if notifier should periodically fetch remote
134
+ # configuration, false otherwise
135
+ # @api public
136
+ # @since v5.2.0
137
+ attr_accessor :remote_config
138
+
116
139
  class << self
117
140
  # @return [Config]
118
141
  attr_writer :instance
@@ -125,7 +148,7 @@ module Airbrake
125
148
 
126
149
  # @param [Hash{Symbol=>Object}] user_config the hash to be used to build the
127
150
  # config
128
- # rubocop:disable Metrics/AbcSize
151
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
129
152
  def initialize(user_config = {})
130
153
  self.proxy = {}
131
154
  self.queue_size = 100
@@ -134,7 +157,9 @@ module Airbrake
134
157
  self.logger = ::Logger.new(File::NULL).tap { |l| l.level = Logger::WARN }
135
158
  self.project_id = user_config[:project_id]
136
159
  self.project_key = user_config[:project_key]
137
- self.host = 'https://api.airbrake.io'
160
+ self.error_host = 'https://api.airbrake.io'
161
+ self.apm_host = 'https://api.airbrake.io'
162
+ self.remote_config_host = 'https://notifier-configs.airbrake.io'
138
163
 
139
164
  self.ignore_environments = []
140
165
 
@@ -153,37 +178,21 @@ module Airbrake
153
178
  self.performance_stats_flush_period = 15
154
179
  self.query_stats = true
155
180
  self.job_stats = true
181
+ self.error_notifications = true
182
+ self.remote_config = true
156
183
 
157
184
  merge(user_config)
158
185
  end
159
- # rubocop:enable Metrics/AbcSize
160
-
161
- def blacklist_keys=(keys)
162
- loc = caller_locations(1..1).first
163
- Kernel.warn(
164
- "#{loc.path}:#{loc.lineno}: warning: blacklist_keys= is deprecated " \
165
- "use blocklist_keys= instead",
166
- )
167
- self.blocklist_keys = keys
168
- end
169
-
170
- def whitelist_keys=(keys)
171
- loc = caller_locations(1..1).first
172
- Kernel.warn(
173
- "#{loc.path}:#{loc.lineno}: warning: whitelist_keys= is deprecated " \
174
- "use allowlist_keys= instead",
175
- )
176
- self.allowlist_keys = keys
177
- end
186
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
178
187
 
179
- # The full URL to the Airbrake Notice API. Based on the +:host+ option.
188
+ # The full URL to the Airbrake Notice API. Based on the +:error_host+ option.
180
189
  # @return [URI] the endpoint address
181
- def endpoint
182
- @endpoint ||=
190
+ def error_endpoint
191
+ @error_endpoint ||=
183
192
  begin
184
- self.host = ('https://' << host) if host !~ %r{\Ahttps?://}
193
+ self.error_host = ('https://' << error_host) if error_host !~ %r{\Ahttps?://}
185
194
  api = "api/v3/projects/#{project_id}/notices"
186
- URI.join(host, api)
195
+ URI.join(error_host, api)
187
196
  end
188
197
  end
189
198
 
@@ -239,14 +248,14 @@ module Airbrake
239
248
 
240
249
  # @return [Promise] resolved promise if neither of the performance options
241
250
  # reject it, false otherwise
242
- def check_performance_options(resource)
251
+ def check_performance_options(metric)
243
252
  promise = Airbrake::Promise.new
244
253
 
245
254
  if !performance_stats
246
255
  promise.reject("The Performance Stats feature is disabled")
247
- elsif resource.is_a?(Airbrake::Query) && !query_stats
256
+ elsif metric.is_a?(Airbrake::Query) && !query_stats
248
257
  promise.reject("The Query Stats feature is disabled")
249
- elsif resource.is_a?(Airbrake::Queue) && !job_stats
258
+ elsif metric.is_a?(Airbrake::Queue) && !job_stats
250
259
  promise.reject("The Job Stats feature is disabled")
251
260
  else
252
261
  promise
@@ -0,0 +1,51 @@
1
+ module Airbrake
2
+ # Represents a thread-safe Airbrake context object, which carries arbitrary
3
+ # information added via {Airbrake.merge_context} calls.
4
+ #
5
+ # @example
6
+ # Airbrake::Context.current.merge!(foo: 'bar')
7
+ #
8
+ # @api private
9
+ # @since v5.2.1
10
+ class Context
11
+ # Returns current, thread-local, context.
12
+ # @return [self]
13
+ def self.current
14
+ Thread.current[:airbrake_context] ||= new
15
+ end
16
+
17
+ def initialize
18
+ @mutex = Mutex.new
19
+ @context = {}
20
+ end
21
+
22
+ # Merges the given context with the current one.
23
+ #
24
+ # @param [Hash{Object=>Object}] other
25
+ # @return [void]
26
+ def merge!(other)
27
+ @mutex.synchronize do
28
+ @context.merge!(other)
29
+ end
30
+ end
31
+
32
+ # @return [Hash] duplicated Hash context
33
+ def to_h
34
+ @mutex.synchronize do
35
+ @context.dup
36
+ end
37
+ end
38
+
39
+ # @return [Hash] clears (resets) the current context
40
+ def clear
41
+ @mutex.synchronize do
42
+ @context.clear
43
+ end
44
+ end
45
+
46
+ # @return [Boolean] checks whether the context has any data
47
+ def empty?
48
+ @context.empty?
49
+ end
50
+ end
51
+ end
@@ -40,7 +40,7 @@ module Airbrake
40
40
  data.empty?
41
41
  end
42
42
 
43
- # @since ?.?.?
43
+ # @since v4.7.0
44
44
  # @return [void]
45
45
  def self.reset
46
46
  @data = {}
@@ -57,7 +57,9 @@ module Airbrake
57
57
  # @return [void]
58
58
  # @since v3.1.0
59
59
  def delete_filter(filter_class)
60
+ # rubocop:disable Style/ClassEqualityComparison
60
61
  index = @filters.index { |f| f.class.name == filter_class.name }
62
+ # rubocop:enable Style/ClassEqualityComparison
61
63
  @filters.delete_at(index) if index
62
64
  end
63
65
 
@@ -70,6 +72,7 @@ module Airbrake
70
72
  def refine(notice)
71
73
  @filters.each do |filter|
72
74
  break if notice.ignored?
75
+
73
76
  filter.call(notice)
74
77
  end
75
78
  end
@@ -9,8 +9,7 @@ module Airbrake
9
9
  # @return [Integer]
10
10
  attr_reader :weight
11
11
 
12
- def initialize(context)
13
- @context = context
12
+ def initialize
14
13
  @weight = 119
15
14
  @mutex = Mutex.new
16
15
  end
@@ -18,10 +17,10 @@ module Airbrake
18
17
  # @macro call_filter
19
18
  def call(notice)
20
19
  @mutex.synchronize do
21
- return if @context.empty?
20
+ return if Airbrake::Context.current.empty?
22
21
 
23
- notice[:params][:airbrake_context] = @context.dup
24
- @context.clear
22
+ notice[:params][:airbrake_context] = Airbrake::Context.current.to_h
23
+ Airbrake::Context.current.clear
25
24
  end
26
25
  end
27
26
  end
@@ -24,6 +24,7 @@ module Airbrake
24
24
 
25
25
  def git_version(spec)
26
26
  return unless spec.respond_to?(:git_version) || spec.git_version
27
+
27
28
  spec.git_version.to_s
28
29
  end
29
30
  end
@@ -13,7 +13,7 @@ module Airbrake
13
13
  end
14
14
 
15
15
  # @macro call_filter
16
- def call(notice)
16
+ def call(notice) # rubocop:disable Metrics/AbcSize
17
17
  exception = notice.stash[:exception]
18
18
  return unless exception.respond_to?(:to_airbrake)
19
19
 
@@ -23,6 +23,7 @@ module Airbrake
23
23
  # If the frame is unparseable, then 'file' is nil, thus nothing to
24
24
  # filter (all frame's data is in 'function' instead).
25
25
  next unless (file = frame[:file])
26
+
26
27
  frame[:file] = file.sub(/\A#{gem_path}/, GEM_ROOT_LABEL)
27
28
  end
28
29
  end
@@ -41,16 +41,16 @@ module Airbrake
41
41
 
42
42
  return unless File.exist?(@git_path)
43
43
  return unless (checkout = last_checkout)
44
+
44
45
  notice[:context][:lastCheckout] = checkout
45
46
  end
46
47
 
47
48
  private
48
49
 
49
- # rubocop:disable Metrics/AbcSize
50
50
  def last_checkout
51
51
  return unless (line = last_checkout_line)
52
52
 
53
- parts = line.chomp.split("\t").first.split(' ')
53
+ parts = line.chomp.split("\t").first.split
54
54
  if parts.size < MIN_HEAD_COLS
55
55
  logger.error(
56
56
  "#{LOG_LABEL} Airbrake::#{self.class.name}: can't parse line: #{line}",
@@ -66,14 +66,13 @@ module Airbrake
66
66
  time: timestamp(parts[-2].to_i),
67
67
  }
68
68
  end
69
- # rubocop:enable Metrics/AbcSize
70
69
 
71
70
  def last_checkout_line
72
71
  head_path = File.join(@git_path, 'logs', 'HEAD')
73
72
  return unless File.exist?(head_path)
74
73
 
75
74
  last_line = nil
76
- IO.foreach(head_path) do |line|
75
+ File.foreach(head_path) do |line|
77
76
  last_line = line if checkout_line?(line)
78
77
  end
79
78
  last_line
@@ -18,6 +18,7 @@ module Airbrake
18
18
  # @macro call_filter
19
19
  def call(notice)
20
20
  return if notice[:context].key?(:repository)
21
+
21
22
  attach_repository(notice)
22
23
  end
23
24
 
@@ -35,10 +36,11 @@ module Airbrake
35
36
  `cd #{@git_path} && git config --get remote.origin.url`.chomp
36
37
  else
37
38
  "`git remote get-url` is unsupported in git #{@git_version}. " \
38
- 'Consider an upgrade to 2.7+'
39
+ 'Consider an upgrade to 2.7+'
39
40
  end
40
41
 
41
42
  return unless @repository
43
+
42
44
  notice[:context][:repository] = @repository
43
45
  end
44
46
 
@@ -46,7 +48,14 @@ module Airbrake
46
48
 
47
49
  def detect_git_version
48
50
  return unless which('git')
49
- Gem::Version.new(`git --version`.split[2])
51
+
52
+ begin
53
+ Gem::Version.new(`git --version`.split[2])
54
+ rescue Errno::EAGAIN
55
+ # Bugfix for the case when the system cannot allocate memory for
56
+ # a fork() call: https://github.com/airbrake/airbrake-ruby/issues/680
57
+ nil
58
+ end
50
59
  end
51
60
 
52
61
  # Cross-platform way to tell if an executable is accessible.
@@ -30,6 +30,7 @@ module Airbrake
30
30
 
31
31
  @revision = find_revision
32
32
  return unless @revision
33
+
33
34
  notice[:context][:revision] = @revision
34
35
  end
35
36
 
@@ -41,6 +42,7 @@ module Airbrake
41
42
 
42
43
  head = File.read(head_path)
43
44
  return head unless head.start_with?(PREFIX)
45
+
44
46
  head = head.chomp[PREFIX.size..-1]
45
47
 
46
48
  ref_path = File.join(@git_path, head)
@@ -55,7 +57,7 @@ module Airbrake
55
57
 
56
58
  File.readlines(packed_refs_path).each do |line|
57
59
  next if %w[# ^].include?(line[0])
58
- next unless (parts = line.split(' ')).size == 2
60
+ next unless (parts = line.split).size == 2
59
61
  return parts.first if parts.last == head
60
62
  end
61
63
 
@@ -64,10 +64,14 @@ module Airbrake
64
64
  validate_patterns
65
65
  end
66
66
 
67
- FILTERABLE_KEYS.each { |key| filter_hash(notice[key]) }
67
+ FILTERABLE_KEYS.each do |key|
68
+ notice[key] = filter_hash(notice[key])
69
+ end
70
+
68
71
  FILTERABLE_CONTEXT_KEYS.each { |key| filter_context_key(notice, key) }
69
72
 
70
73
  return unless notice[:context][:url]
74
+
71
75
  filter_url(notice)
72
76
  end
73
77
 
@@ -78,29 +82,29 @@ module Airbrake
78
82
 
79
83
  private
80
84
 
81
- def filter_hash(hash)
85
+ def filter_hash(hash) # rubocop:disable Metrics/AbcSize
82
86
  return hash unless hash.is_a?(Hash)
83
87
 
88
+ hash_copy = hash.dup
89
+
84
90
  hash.each_key do |key|
85
91
  if should_filter?(key.to_s)
86
- hash[key] = FILTERED
87
- elsif hash[key].is_a?(Hash)
88
- 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])
89
95
  elsif hash[key].is_a?(Array)
90
- 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
91
99
  end
92
100
  end
101
+
102
+ hash_copy
93
103
  end
94
104
 
95
105
  def filter_url_params(url)
96
- url.query = Hash[URI.decode_www_form(url.query)].map do |key, val|
97
- # Ruby < 2.2 raises InvalidComponentError if the query contains
98
- # invalid characters, so be sure to escape individual components.
99
- if should_filter?(key)
100
- "#{URI.encode_www_form_component(key)}=[Filtered]"
101
- else
102
- "#{URI.encode_www_form_component(key)}=#{URI.encode_www_form_component(val)}"
103
- end
106
+ url.query = URI.decode_www_form(url.query).to_h.map do |key, val|
107
+ should_filter?(key) ? "#{key}=[Filtered]" : "#{key}=#{val}"
104
108
  end.join('&')
105
109
 
106
110
  url.to_s
@@ -114,6 +118,7 @@ module Airbrake
114
118
  end
115
119
 
116
120
  return unless url.query
121
+
117
122
  notice[:context][:url] = filter_url_params(url)
118
123
  end
119
124
 
@@ -122,6 +127,7 @@ module Airbrake
122
127
 
123
128
  @patterns = @patterns.flat_map do |pattern|
124
129
  next(pattern) unless pattern.respond_to?(:call)
130
+
125
131
  pattern.call
126
132
  end
127
133
  end
@@ -142,7 +148,9 @@ module Airbrake
142
148
  def filter_context_key(notice, key)
143
149
  return unless notice[:context][key]
144
150
  return if notice[:context][key] == FILTERED
145
- 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
146
154
 
147
155
  notice[:context][key] = FILTERED
148
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