airbrake-ruby 4.8.0 → 5.2.0

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