airbrake-ruby 3.2.6 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/airbrake-ruby.rb +31 -138
  3. data/lib/airbrake-ruby/async_sender.rb +20 -8
  4. data/lib/airbrake-ruby/backtrace.rb +15 -13
  5. data/lib/airbrake-ruby/code_hunk.rb +2 -4
  6. data/lib/airbrake-ruby/config.rb +8 -38
  7. data/lib/airbrake-ruby/deploy_notifier.rb +4 -17
  8. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +5 -4
  9. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +6 -4
  10. data/lib/airbrake-ruby/filters/keys_blacklist.rb +0 -1
  11. data/lib/airbrake-ruby/filters/keys_filter.rb +4 -4
  12. data/lib/airbrake-ruby/filters/keys_whitelist.rb +0 -1
  13. data/lib/airbrake-ruby/loggable.rb +31 -0
  14. data/lib/airbrake-ruby/nested_exception.rb +2 -3
  15. data/lib/airbrake-ruby/notice.rb +6 -6
  16. data/lib/airbrake-ruby/notice_notifier.rb +11 -47
  17. data/lib/airbrake-ruby/performance_notifier.rb +6 -18
  18. data/lib/airbrake-ruby/response.rb +5 -2
  19. data/lib/airbrake-ruby/sync_sender.rb +8 -6
  20. data/lib/airbrake-ruby/version.rb +1 -1
  21. data/spec/airbrake_spec.rb +1 -143
  22. data/spec/async_sender_spec.rb +83 -90
  23. data/spec/backtrace_spec.rb +36 -47
  24. data/spec/code_hunk_spec.rb +12 -15
  25. data/spec/config_spec.rb +79 -96
  26. data/spec/deploy_notifier_spec.rb +3 -7
  27. data/spec/filter_chain_spec.rb +1 -3
  28. data/spec/filters/context_filter_spec.rb +1 -3
  29. data/spec/filters/dependency_filter_spec.rb +1 -3
  30. data/spec/filters/exception_attributes_filter_spec.rb +1 -14
  31. data/spec/filters/gem_root_filter_spec.rb +1 -4
  32. data/spec/filters/git_last_checkout_filter_spec.rb +3 -5
  33. data/spec/filters/git_revision_filter_spec.rb +1 -3
  34. data/spec/filters/keys_blacklist_spec.rb +14 -25
  35. data/spec/filters/keys_whitelist_spec.rb +14 -25
  36. data/spec/filters/root_directory_filter_spec.rb +1 -4
  37. data/spec/filters/system_exit_filter_spec.rb +2 -2
  38. data/spec/filters/thread_filter_spec.rb +1 -3
  39. data/spec/nested_exception_spec.rb +3 -5
  40. data/spec/notice_notifier_spec.rb +23 -20
  41. data/spec/notice_notifier_spec/options_spec.rb +20 -25
  42. data/spec/notice_spec.rb +13 -12
  43. data/spec/performance_notifier_spec.rb +19 -31
  44. data/spec/response_spec.rb +23 -17
  45. data/spec/sync_sender_spec.rb +26 -33
  46. metadata +2 -1
@@ -6,24 +6,12 @@ module Airbrake
6
6
  # @since v3.2.0
7
7
  class PerformanceNotifier
8
8
  include Inspectable
9
+ include Loggable
9
10
 
10
- # @param [Airbrake::Config] config
11
- def initialize(config)
12
- @config =
13
- if config.is_a?(Config)
14
- config
15
- else
16
- loc = caller_locations(1..1).first
17
- signature = "#{self.class.name}##{__method__}"
18
- warn(
19
- "#{loc.path}:#{loc.lineno}: warning: passing a Hash to #{signature} " \
20
- 'is deprecated. Pass `Airbrake::Config` instead'
21
- )
22
- Config.new(config)
23
- end
24
-
25
- @flush_period = @config.performance_stats_flush_period
26
- @sender = SyncSender.new(@config, :put)
11
+ def initialize
12
+ @config = Airbrake::Config.instance
13
+ @flush_period = Airbrake::Config.instance.performance_stats_flush_period
14
+ @sender = SyncSender.new(:put)
27
15
  @payload = {}
28
16
  @schedule_flush = nil
29
17
  @mutex = Mutex.new
@@ -92,7 +80,7 @@ module Airbrake
92
80
  signature = "#{self.class.name}##{__method__}"
93
81
  raise "#{signature}: payload (#{payload}) cannot be empty. Race?" if payload.none?
94
82
 
95
- @config.logger.debug("#{LOG_LABEL} #{signature}: #{payload}")
83
+ logger.debug("#{LOG_LABEL} #{signature}: #{payload}")
96
84
 
97
85
  payload.group_by { |k, _v| k.name }.each do |resource_name, data|
98
86
  data = { resource_name => data.map { |k, v| k.to_h.merge!(v.to_h) } }
@@ -11,13 +11,16 @@ module Airbrake
11
11
  # @return [Integer] HTTP code returned when an IP sends over 10k/min notices
12
12
  TOO_MANY_REQUESTS = 429
13
13
 
14
+ class << self
15
+ include Loggable
16
+ end
17
+
14
18
  # Parses HTTP responses from the Airbrake API.
15
19
  #
16
20
  # @param [Net::HTTPResponse] response
17
- # @param [Logger] logger
18
21
  # @return [Hash{String=>String}] parsed response
19
22
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
20
- def self.parse(response, logger)
23
+ def self.parse(response)
21
24
  code = response.code.to_i
22
25
  body = response.body
23
26
 
@@ -9,9 +9,11 @@ module Airbrake
9
9
  # @return [String] body for HTTP requests
10
10
  CONTENT_TYPE = 'application/json'.freeze
11
11
 
12
- # @param [Airbrake::Config] config
13
- def initialize(config, method = :post)
14
- @config = config
12
+ include Loggable
13
+
14
+ # @param [Symbol] method HTTP method to use to send payload
15
+ def initialize(method = :post)
16
+ @config = Airbrake::Config.instance
15
17
  @method = method
16
18
  @rate_limit_reset = Time.now
17
19
  end
@@ -35,11 +37,11 @@ module Airbrake
35
37
  response = https.request(req)
36
38
  rescue StandardError => ex
37
39
  reason = "#{LOG_LABEL} HTTP error: #{ex}"
38
- @config.logger.error(reason)
40
+ logger.error(reason)
39
41
  return promise.reject(reason)
40
42
  end
41
43
 
42
- parsed_resp = Response.parse(response, @config.logger)
44
+ parsed_resp = Response.parse(response)
43
45
  if parsed_resp.key?('rate_limit_reset')
44
46
  @rate_limit_reset = parsed_resp['rate_limit_reset']
45
47
  end
@@ -101,7 +103,7 @@ module Airbrake
101
103
 
102
104
  if missing
103
105
  reason = "#{LOG_LABEL} data was not sent because of missing body"
104
- @config.logger.error(reason)
106
+ logger.error(reason)
105
107
  promise.reject(reason)
106
108
  end
107
109
 
@@ -2,5 +2,5 @@
2
2
  # More information: http://semver.org/
3
3
  module Airbrake
4
4
  # @return [String] the library version
5
- AIRBRAKE_RUBY_VERSION = '3.2.6'.freeze
5
+ AIRBRAKE_RUBY_VERSION = '4.0.0'.freeze
6
6
  end
@@ -1,31 +1,7 @@
1
1
  RSpec.describe Airbrake do
2
- describe ".[]" do
3
- it "returns a NilNotifier" do
4
- expect(described_class[:test]).to be_an(Airbrake::NilNoticeNotifier)
5
- end
6
- end
7
-
8
- describe ".notifiers" do
9
- it "returns a Hash of notifiers" do
10
- expect(described_class.notifiers).to eq(
11
- notice: {}, performance: {}, deploy: {}
12
- )
13
- end
14
- end
15
-
16
- let(:default_notifier) do
17
- described_class[:default]
18
- end
2
+ before { Airbrake::Config.instance = Airbrake::Config.new }
19
3
 
20
4
  describe ".configure" do
21
- let(:config_params) { { project_id: 1, project_key: 'abc' } }
22
-
23
- after do
24
- described_class.instance_variable_get(:@notice_notifiers).clear
25
- described_class.instance_variable_get(:@performance_notifiers).clear
26
- described_class.instance_variable_get(:@deploy_notifiers).clear
27
- end
28
-
29
5
  it "yields the config" do
30
6
  expect do |b|
31
7
  begin
@@ -36,31 +12,6 @@ RSpec.describe Airbrake do
36
12
  end.to yield_with_args(Airbrake::Config)
37
13
  end
38
14
 
39
- context "when invoked with a notice notifier name" do
40
- it "sets notice notifier name to the provided name" do
41
- described_class.configure(:test) { |c| c.merge(config_params) }
42
- expect(described_class[:test]).to be_an(Airbrake::NoticeNotifier)
43
- end
44
- end
45
-
46
- context "when invoked without a notifier name" do
47
- it "defaults to the :default notifier name" do
48
- described_class.configure { |c| c.merge(config_params) }
49
- expect(described_class[:default]).to be_an(Airbrake::NoticeNotifier)
50
- end
51
- end
52
-
53
- context "when invoked twice with the same notifier name" do
54
- it "raises Airbrake::Error" do
55
- described_class.configure { |c| c.merge(config_params) }
56
- expect do
57
- described_class.configure { |c| c.merge(config_params) }
58
- end.to raise_error(
59
- Airbrake::Error, "the 'default' notifier was already configured"
60
- )
61
- end
62
- end
63
-
64
15
  context "when user config doesn't contain a project id" do
65
16
  it "raises error" do
66
17
  expect { described_class.configure { |c| c.project_key = '1' } }.
@@ -75,97 +26,4 @@ RSpec.describe Airbrake do
75
26
  end
76
27
  end
77
28
  end
78
-
79
- describe ".configured?" do
80
- it "forwards 'configured?' to the notifier" do
81
- expect(default_notifier).to receive(:configured?)
82
- described_class.configured?
83
- end
84
- end
85
-
86
- describe ".notify" do
87
- it "forwards 'notify' to the notifier" do
88
- block = proc {}
89
- expect(default_notifier).to receive(:notify).with('ex', foo: 'bar', &block)
90
- described_class.notify('ex', foo: 'bar', &block)
91
- end
92
- end
93
-
94
- describe ".notify_sync" do
95
- it "forwards 'notify_sync' to the notifier" do
96
- block = proc {}
97
- expect(default_notifier).to receive(:notify).with('ex', foo: 'bar', &block)
98
- described_class.notify('ex', foo: 'bar', &block)
99
- end
100
- end
101
-
102
- describe ".add_filter" do
103
- it "forwards 'add_filter' to the notifier" do
104
- block = proc {}
105
- expect(default_notifier).to receive(:add_filter).with(nil, &block)
106
- described_class.add_filter(&block)
107
- end
108
- end
109
-
110
- describe ".build_notice" do
111
- it "forwards 'build_notice' to the notifier" do
112
- expect(default_notifier).to receive(:build_notice).with('ex', foo: 'bar')
113
- described_class.build_notice('ex', foo: 'bar')
114
- end
115
- end
116
-
117
- describe ".close" do
118
- it "forwards 'close' to the notifier" do
119
- expect(default_notifier).to receive(:close)
120
- described_class.close
121
- end
122
- end
123
-
124
- describe ".notify_deploy" do
125
- let(:default_notifier) { described_class.notifiers[:deploy][:default] }
126
-
127
- it "calls 'notify' on the deploy notifier" do
128
- expect(default_notifier).to receive(:notify).with(foo: 'bar')
129
- described_class.notify_deploy(foo: 'bar')
130
- end
131
- end
132
-
133
- describe ".merge_context" do
134
- it "forwards 'merge_context' to the notifier" do
135
- expect(default_notifier).to receive(:merge_context).with(foo: 'bar')
136
- described_class.merge_context(foo: 'bar')
137
- end
138
- end
139
-
140
- describe ".notify_request" do
141
- let(:default_notifier) { described_class.notifiers[:performance][:default] }
142
-
143
- it "calls 'notify' on the route notifier" do
144
- params = {
145
- method: 'GET',
146
- route: '/foo',
147
- status_code: 200,
148
- start_time: Time.new(2018, 1, 1, 0, 20, 0, 0),
149
- end_time: Time.new(2018, 1, 1, 0, 19, 0, 0)
150
- }
151
- expect(default_notifier).to receive(:notify).with(Airbrake::Request.new(params))
152
- described_class.notify_request(params)
153
- end
154
- end
155
-
156
- describe ".notify_query" do
157
- let(:default_notifier) { described_class.notifiers[:performance][:default] }
158
-
159
- it "calls 'notify' on the query notifier" do
160
- params = {
161
- method: 'GET',
162
- route: '/foo',
163
- query: 'SELECT * FROM foos',
164
- start_time: Time.new(2018, 1, 1, 0, 20, 0, 0),
165
- end_time: Time.new(2018, 1, 1, 0, 19, 0, 0)
166
- }
167
- expect(default_notifier).to receive(:notify).with(Airbrake::Query.new(params))
168
- described_class.notify_query(params)
169
- end
170
- end
171
29
  end
@@ -1,154 +1,147 @@
1
1
  RSpec.describe Airbrake::AsyncSender do
2
+ let(:endpoint) { 'https://api.airbrake.io/api/v3/projects/1/notices' }
3
+ let(:queue_size) { 10 }
4
+ let(:notice) { Airbrake::Notice.new(AirbrakeTestError.new) }
5
+
2
6
  before do
3
- stub_request(:post, /.*/).to_return(status: 201, body: '{}')
7
+ stub_request(:post, endpoint).to_return(status: 201, body: '{}')
8
+ Airbrake::Config.instance = Airbrake::Config.new(
9
+ project_id: '1',
10
+ workers: 3,
11
+ queue_size: queue_size
12
+ )
13
+
14
+ allow(Airbrake::Loggable.instance).to receive(:debug)
15
+ expect(subject).to have_workers
4
16
  end
5
17
 
6
18
  describe "#send" do
7
- it "limits the size of the queue" do
8
- stdout = StringIO.new
9
- notices_count = 1000
10
- queue_size = 10
11
- config = Airbrake::Config.new(
12
- logger: Logger.new(stdout), workers: 3, queue_size: queue_size
13
- )
14
- sender = described_class.new(config)
15
- expect(sender).to have_workers
16
-
17
- notice = Airbrake::Notice.new(config, AirbrakeTestError.new)
18
- notices_count.times { sender.send(notice, Airbrake::Promise.new) }
19
- sender.close
20
-
21
- log = stdout.string.split("\n")
22
- notices_sent = log.grep(/\*\*Airbrake: Airbrake::Response \(201\): \{\}/).size
23
- notices_dropped = log.grep(/\*\*Airbrake:.*not.*delivered/).size
24
- expect(notices_sent).to be >= queue_size
25
- expect(notices_sent + notices_dropped).to eq(notices_count)
19
+ it "sends payload to Airbrake" do
20
+ 2.times do
21
+ subject.send(notice, Airbrake::Promise.new)
22
+ end
23
+ subject.close
24
+
25
+ expect(a_request(:post, endpoint)).to have_been_made.twice
26
26
  end
27
- end
28
27
 
29
- describe "#close" do
30
- before do
31
- @stderr = StringIO.new
32
- config = Airbrake::Config.new(logger: Logger.new(@stderr))
33
- @sender = described_class.new(config)
34
- expect(@sender).to have_workers
28
+ context "when the queue is full" do
29
+ before do
30
+ allow(subject.unsent).to receive(:size).and_return(queue_size)
31
+ end
32
+
33
+ it "discards payload" do
34
+ 200.times do
35
+ subject.send(notice, Airbrake::Promise.new)
36
+ end
37
+ subject.close
38
+
39
+ expect(a_request(:post, endpoint)).not_to have_been_made
40
+ end
41
+
42
+ it "logs discarded payload" do
43
+ expect(Airbrake::Loggable.instance).to receive(:error).with(
44
+ /reached its capacity/
45
+ ).exactly(15).times
46
+
47
+ 15.times do
48
+ subject.send(notice, Airbrake::Promise.new)
49
+ end
50
+ subject.close
51
+ end
35
52
  end
53
+ end
36
54
 
55
+ describe "#close" do
37
56
  context "when there are no unsent notices" do
38
57
  it "joins the spawned thread" do
39
- workers = @sender.instance_variable_get(:@workers).list
40
-
58
+ workers = subject.workers.list
41
59
  expect(workers).to all(be_alive)
42
- @sender.close
60
+
61
+ subject.close
43
62
  expect(workers).to all(be_stop)
44
63
  end
45
64
  end
46
65
 
47
66
  context "when there are some unsent notices" do
48
- before do
49
- notice = Airbrake::Notice.new(Airbrake::Config.new, AirbrakeTestError.new)
50
- 300.times { @sender.send(notice, Airbrake::Promise.new) }
51
- expect(@sender.instance_variable_get(:@unsent).size).not_to be_zero
52
- @sender.close
53
- end
54
-
55
- it "warns about the number of notices" do
56
- expect(@stderr.string).to match(/waiting to send \d+ unsent notice/)
57
- end
58
-
59
- it "prints the correct number of log messages" do
60
- log = @stderr.string.split("\n")
61
- notices_sent = log.grep(/\*\*Airbrake: Airbrake::Response \(201\): \{\}/).size
62
- notices_dropped = log.grep(/\*\*Airbrake:.*not.*delivered/).size
63
- expect(notices_sent).to be >= @sender.instance_variable_get(:@unsent).max
64
- expect(notices_sent + notices_dropped).to eq(300)
67
+ it "logs how many notices are left to send" do
68
+ expect(Airbrake::Loggable.instance).to receive(:debug).with(
69
+ /waiting to send \d+ unsent notice\(s\)/
70
+ )
71
+ expect(Airbrake::Loggable.instance).to receive(:debug).with(/closed/)
72
+
73
+ 300.times { subject.send(notice, Airbrake::Promise.new) }
74
+ subject.close
65
75
  end
66
76
 
67
77
  it "waits until the unsent notices queue is empty" do
68
- expect(@sender.instance_variable_get(:@unsent).size).to be_zero
78
+ subject.close
79
+ expect(subject.unsent.size).to be_zero
69
80
  end
70
81
  end
71
82
 
72
83
  context "when it was already closed" do
73
84
  it "doesn't increase the unsent queue size" do
74
85
  begin
75
- @sender.close
86
+ subject.close
76
87
  rescue Airbrake::Error
77
88
  nil
78
89
  end
79
90
 
80
- expect(@sender.instance_variable_get(:@unsent).size).to be_zero
91
+ expect(subject.unsent.size).to be_zero
81
92
  end
82
93
 
83
94
  it "raises error" do
84
- @sender.close
95
+ subject.close
85
96
 
86
- expect(@sender).to be_closed
87
- expect { @sender.close }.
88
- to raise_error(Airbrake::Error, 'attempted to close already closed sender')
97
+ expect(subject).to be_closed
98
+ expect { subject.close }.to raise_error(
99
+ Airbrake::Error, 'attempted to close already closed sender'
100
+ )
89
101
  end
90
102
  end
91
103
 
92
104
  context "when workers were not spawned" do
93
105
  it "correctly closes the notifier nevertheless" do
94
- sender = described_class.new(Airbrake::Config.new)
95
- sender.close
96
-
97
- expect(sender).to be_closed
106
+ subject.close
107
+ expect(subject).to be_closed
98
108
  end
99
109
  end
100
110
  end
101
111
 
102
112
  describe "#has_workers?" do
103
- before do
104
- @sender = described_class.new(Airbrake::Config.new)
105
- expect(@sender).to have_workers
106
- end
107
-
108
113
  it "returns false when the sender is not closed, but has 0 workers" do
109
- @sender.instance_variable_get(:@workers).list.each(&:kill)
110
- sleep 1
111
- expect(@sender).not_to have_workers
114
+ subject.workers.list.each do |worker|
115
+ worker.kill.join
116
+ end
117
+ expect(subject).not_to have_workers
112
118
  end
113
119
 
114
120
  it "returns false when the sender is closed" do
115
- @sender.close
116
- expect(@sender).not_to have_workers
121
+ subject.close
122
+ expect(subject).not_to have_workers
117
123
  end
118
124
 
119
125
  it "respawns workers on fork()", skip: %w[jruby rbx].include?(RUBY_ENGINE) do
120
- pid = fork do
121
- expect(@sender).to have_workers
122
- end
126
+ pid = fork { expect(subject).to have_workers }
123
127
  Process.wait(pid)
124
- @sender.close
125
- expect(@sender).not_to have_workers
128
+ subject.close
129
+ expect(subject).not_to have_workers
126
130
  end
127
131
  end
128
132
 
129
133
  describe "#spawn_workers" do
130
134
  it "spawns alive threads in an enclosed ThreadGroup" do
131
- sender = described_class.new(Airbrake::Config.new)
132
- expect(sender).to have_workers
135
+ expect(subject.workers).to be_a(ThreadGroup)
136
+ expect(subject.workers.list).to all(be_alive)
137
+ expect(subject.workers).to be_enclosed
133
138
 
134
- workers = sender.instance_variable_get(:@workers)
135
-
136
- expect(workers).to be_a(ThreadGroup)
137
- expect(workers.list).to all(be_alive)
138
- expect(workers).to be_enclosed
139
-
140
- sender.close
139
+ subject.close
141
140
  end
142
141
 
143
142
  it "spawns exactly config.workers workers" do
144
- workers_count = 5
145
- sender = described_class.new(Airbrake::Config.new(workers: workers_count))
146
- expect(sender).to have_workers
147
-
148
- workers = sender.instance_variable_get(:@workers)
149
-
150
- expect(workers.list.size).to eq(workers_count)
151
- sender.close
143
+ expect(subject.workers.list.size).to eq(Airbrake::Config.instance.workers)
144
+ subject.close
152
145
  end
153
146
  end
154
147
  end