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
@@ -2,7 +2,7 @@ module Airbrake
2
2
  # Benchmark benchmarks Ruby code.
3
3
  #
4
4
  # @since v4.2.4
5
- # @api private
5
+ # @api public
6
6
  class Benchmark
7
7
  # Measures monotonic time for the given operation.
8
8
  #
@@ -30,7 +30,7 @@ module Airbrake
30
30
  Airbrake::FileCache[file] ||= File.foreach(file)
31
31
  rescue StandardError => ex
32
32
  logger.error(
33
- "#{self.class.name}: can't read code hunk for #{file}: #{ex}"
33
+ "#{self.class.name}: can't read code hunk for #{file}: #{ex}",
34
34
  )
35
35
  nil
36
36
  end
@@ -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
@@ -68,14 +79,14 @@ module Airbrake
68
79
  # @return [Array<String, Symbol, Regexp>] the keys, which should be
69
80
  # filtered
70
81
  # @api public
71
- # @since v1.2.0
72
- attr_accessor :blacklist_keys
82
+ # @since v4.15.0
83
+ attr_accessor :allowlist_keys
73
84
 
74
- # @return [Array<String, Symbol, Regexp>] the keys, which shouldn't be
85
+ # @return [Array<String, Symbol, Regexp>] the keys, which should be
75
86
  # filtered
76
87
  # @api public
77
- # @since v1.2.0
78
- attr_accessor :whitelist_keys
88
+ # @since v4.15.0
89
+ attr_accessor :blocklist_keys
79
90
 
80
91
  # @return [Boolean] true if the library should attach code hunks to each
81
92
  # frame in a backtrace, false otherwise
@@ -101,6 +112,30 @@ module Airbrake
101
112
  # @since v4.6.0
102
113
  attr_accessor :query_stats
103
114
 
115
+ # @return [Boolean] true if the library should send job/queue/worker stats
116
+ # to Airbrake, false otherwise
117
+ # @api public
118
+ # @since v4.12.0
119
+ attr_accessor :job_stats
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
+
104
139
  class << self
105
140
  # @return [Config]
106
141
  attr_writer :instance
@@ -113,44 +148,51 @@ module Airbrake
113
148
 
114
149
  # @param [Hash{Symbol=>Object}] user_config the hash to be used to build the
115
150
  # config
151
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
116
152
  def initialize(user_config = {})
117
153
  self.proxy = {}
118
154
  self.queue_size = 100
119
155
  self.workers = 1
120
156
  self.code_hunks = true
121
- self.logger = ::Logger.new(File::NULL)
157
+ self.logger = ::Logger.new(File::NULL).tap { |l| l.level = Logger::WARN }
122
158
  self.project_id = user_config[:project_id]
123
159
  self.project_key = user_config[:project_key]
124
- 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'
125
163
 
126
164
  self.ignore_environments = []
127
165
 
128
166
  self.timeout = user_config[:timeout]
129
167
 
130
- self.blacklist_keys = []
131
- self.whitelist_keys = []
168
+ self.blocklist_keys = []
169
+ self.allowlist_keys = []
132
170
 
133
171
  self.root_directory = File.realpath(
134
172
  (defined?(Bundler) && Bundler.root) ||
135
- Dir.pwd
173
+ Dir.pwd,
136
174
  )
137
175
 
138
176
  self.versions = {}
139
177
  self.performance_stats = true
140
178
  self.performance_stats_flush_period = 15
141
179
  self.query_stats = true
180
+ self.job_stats = true
181
+ self.error_notifications = true
182
+ self.remote_config = true
142
183
 
143
184
  merge(user_config)
144
185
  end
186
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
145
187
 
146
- # 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.
147
189
  # @return [URI] the endpoint address
148
- def endpoint
149
- @endpoint ||=
190
+ def error_endpoint
191
+ @error_endpoint ||=
150
192
  begin
151
- self.host = ('https://' << host) if host !~ %r{\Ahttps?://}
193
+ self.error_host = ('https://' << error_host) if error_host !~ %r{\Ahttps?://}
152
194
  api = "api/v3/projects/#{project_id}/notices"
153
- URI.join(host, api)
195
+ URI.join(error_host, api)
154
196
  end
155
197
  end
156
198
 
@@ -213,6 +255,8 @@ module Airbrake
213
255
  promise.reject("The Performance Stats feature is disabled")
214
256
  elsif resource.is_a?(Airbrake::Query) && !query_stats
215
257
  promise.reject("The Query Stats feature is disabled")
258
+ elsif resource.is_a?(Airbrake::Queue) && !job_stats
259
+ promise.reject("The Job Stats feature is disabled")
216
260
  else
217
261
  promise
218
262
  end
@@ -0,0 +1,71 @@
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
+ return if @config.environment == 'test'
47
+
48
+ RemoteSettings.poll(@project_id, @config.remote_config_host) do |data|
49
+ @poll_callback.call(data)
50
+ end
51
+ end
52
+
53
+ # @param [Airbrake::NoticeNotifier] notifier
54
+ # @return [void]
55
+ def add_filters(notifier)
56
+ return unless @config.root_directory
57
+
58
+ [
59
+ Airbrake::Filters::RootDirectoryFilter,
60
+ Airbrake::Filters::GitRevisionFilter,
61
+ Airbrake::Filters::GitRepositoryFilter,
62
+ Airbrake::Filters::GitLastCheckoutFilter,
63
+ ].each do |filter|
64
+ next if notifier.has_filter?(filter)
65
+
66
+ notifier.add_filter(filter.new(@config.root_directory))
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -29,7 +29,7 @@ module Airbrake
29
29
  return promise.reject(
30
30
  "the 'environment' option must be configured " \
31
31
  "with a Symbol (or String), but '#{config.environment.class}' was " \
32
- "provided: #{config.environment}"
32
+ "provided: #{config.environment}",
33
33
  )
34
34
  end
35
35
 
@@ -44,9 +44,13 @@ 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
- "current environment '#{config.environment}' is ignored"
53
+ "current environment '#{config.environment}' is ignored",
50
54
  )
51
55
  end
52
56
 
@@ -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
 
@@ -74,7 +80,7 @@ module Airbrake
74
80
  if config.ignore_environments.any? && config.environment.nil?
75
81
  config.logger.warn(
76
82
  "#{LOG_LABEL} the 'environment' option is not set, " \
77
- "'ignore_environments' has no effect"
83
+ "'ignore_environments' has no effect",
78
84
  )
79
85
  end
80
86
 
@@ -27,7 +27,7 @@ module Airbrake
27
27
  @sender.send(
28
28
  deploy_info,
29
29
  promise,
30
- URI.join(@config.host, "api/v4/projects/#{@config.project_id}/deploys")
30
+ URI.join(@config.host, "api/v4/projects/#{@config.project_id}/deploys"),
31
31
  )
32
32
 
33
33
  promise
@@ -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 = {}
@@ -70,13 +70,14 @@ module Airbrake
70
70
  def refine(notice)
71
71
  @filters.each do |filter|
72
72
  break if notice.ignored?
73
+
73
74
  filter.call(notice)
74
75
  end
75
76
  end
76
77
 
77
78
  # @return [String] customized inspect to lessen the amount of clutter
78
79
  def inspect
79
- @filters.map(&:class).to_s
80
+ filter_classes.to_s
80
81
  end
81
82
 
82
83
  # @return [String] {#inspect} for PrettyPrint
@@ -91,5 +92,19 @@ module Airbrake
91
92
  end
92
93
  q.text(']')
93
94
  end
95
+
96
+ # @param [Class] filter_class
97
+ # @return [Boolean] true if the current chain has an instance of the given
98
+ # class, false otherwise
99
+ # @since v4.14.0
100
+ def includes?(filter_class)
101
+ filter_classes.include?(filter_class)
102
+ end
103
+
104
+ private
105
+
106
+ def filter_classes
107
+ @filters.map(&:class)
108
+ end
94
109
  end
95
110
  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
@@ -22,13 +22,13 @@ module Airbrake
22
22
  attributes = exception.to_airbrake
23
23
  rescue StandardError => ex
24
24
  logger.error(
25
- "#{LOG_LABEL} #{exception.class}#to_airbrake failed. #{ex.class}: #{ex}"
25
+ "#{LOG_LABEL} #{exception.class}#to_airbrake failed. #{ex.class}: #{ex}",
26
26
  )
27
27
  end
28
28
 
29
29
  unless attributes.is_a?(Hash)
30
30
  logger.error(
31
- "#{LOG_LABEL} #{self.class}: wanted Hash, got #{attributes.class}"
31
+ "#{LOG_LABEL} #{self.class}: wanted Hash, got #{attributes.class}",
32
32
  )
33
33
  return
34
34
  end
@@ -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
@@ -27,6 +27,7 @@ module Airbrake
27
27
  @git_path = File.join(root_directory, '.git')
28
28
  @weight = 116
29
29
  @last_checkout = nil
30
+ @deploy_username = ENV['AIRBRAKE_DEPLOY_USERNAME']
30
31
  end
31
32
 
32
33
  # @macro call_filter
@@ -40,32 +41,31 @@ module Airbrake
40
41
 
41
42
  return unless File.exist?(@git_path)
42
43
  return unless (checkout = last_checkout)
44
+
43
45
  notice[:context][:lastCheckout] = checkout
44
46
  end
45
47
 
46
48
  private
47
49
 
48
- # rubocop:disable Metrics/AbcSize
49
50
  def last_checkout
50
51
  return unless (line = last_checkout_line)
51
52
 
52
53
  parts = line.chomp.split("\t").first.split(' ')
53
54
  if parts.size < MIN_HEAD_COLS
54
55
  logger.error(
55
- "#{LOG_LABEL} Airbrake::#{self.class.name}: can't parse line: #{line}"
56
+ "#{LOG_LABEL} Airbrake::#{self.class.name}: can't parse line: #{line}",
56
57
  )
57
58
  return
58
59
  end
59
60
 
60
61
  author = parts[2..-4]
61
62
  @last_checkout = {
62
- username: author[0..1].join(' '),
63
+ username: @deploy_username || author[0..1].join(' '),
63
64
  email: parts[-3][1..-2],
64
65
  revision: parts[1],
65
- time: timestamp(parts[-2].to_i)
66
+ time: timestamp(parts[-2].to_i),
66
67
  }
67
68
  end
68
- # rubocop:enable Metrics/AbcSize
69
69
 
70
70
  def last_checkout_line
71
71
  head_path = File.join(@git_path, 'logs', 'HEAD')
@@ -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
 
@@ -39,6 +40,7 @@ module Airbrake
39
40
  end
40
41
 
41
42
  return unless @repository
43
+
42
44
  notice[:context][:repository] = @repository
43
45
  end
44
46
 
@@ -46,6 +48,7 @@ module Airbrake
46
48
 
47
49
  def detect_git_version
48
50
  return unless which('git')
51
+
49
52
  Gem::Version.new(`git --version`.split[2])
50
53
  end
51
54
 
@@ -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)