airbrake-ruby 4.7.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 (101) hide show
  1. checksums.yaml +7 -0
  2. data/lib/airbrake-ruby.rb +515 -0
  3. data/lib/airbrake-ruby/async_sender.rb +80 -0
  4. data/lib/airbrake-ruby/backtrace.rb +196 -0
  5. data/lib/airbrake-ruby/benchmark.rb +39 -0
  6. data/lib/airbrake-ruby/code_hunk.rb +51 -0
  7. data/lib/airbrake-ruby/config.rb +229 -0
  8. data/lib/airbrake-ruby/config/validator.rb +91 -0
  9. data/lib/airbrake-ruby/deploy_notifier.rb +36 -0
  10. data/lib/airbrake-ruby/file_cache.rb +54 -0
  11. data/lib/airbrake-ruby/filter_chain.rb +95 -0
  12. data/lib/airbrake-ruby/filters/context_filter.rb +29 -0
  13. data/lib/airbrake-ruby/filters/dependency_filter.rb +31 -0
  14. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +46 -0
  15. data/lib/airbrake-ruby/filters/gem_root_filter.rb +33 -0
  16. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +92 -0
  17. data/lib/airbrake-ruby/filters/git_repository_filter.rb +64 -0
  18. data/lib/airbrake-ruby/filters/git_revision_filter.rb +66 -0
  19. data/lib/airbrake-ruby/filters/keys_blacklist.rb +49 -0
  20. data/lib/airbrake-ruby/filters/keys_filter.rb +140 -0
  21. data/lib/airbrake-ruby/filters/keys_whitelist.rb +48 -0
  22. data/lib/airbrake-ruby/filters/root_directory_filter.rb +28 -0
  23. data/lib/airbrake-ruby/filters/sql_filter.rb +125 -0
  24. data/lib/airbrake-ruby/filters/system_exit_filter.rb +23 -0
  25. data/lib/airbrake-ruby/filters/thread_filter.rb +92 -0
  26. data/lib/airbrake-ruby/hash_keyable.rb +37 -0
  27. data/lib/airbrake-ruby/ignorable.rb +44 -0
  28. data/lib/airbrake-ruby/inspectable.rb +39 -0
  29. data/lib/airbrake-ruby/loggable.rb +34 -0
  30. data/lib/airbrake-ruby/monotonic_time.rb +43 -0
  31. data/lib/airbrake-ruby/nested_exception.rb +38 -0
  32. data/lib/airbrake-ruby/notice.rb +162 -0
  33. data/lib/airbrake-ruby/notice_notifier.rb +134 -0
  34. data/lib/airbrake-ruby/performance_breakdown.rb +46 -0
  35. data/lib/airbrake-ruby/performance_notifier.rb +155 -0
  36. data/lib/airbrake-ruby/promise.rb +109 -0
  37. data/lib/airbrake-ruby/query.rb +54 -0
  38. data/lib/airbrake-ruby/request.rb +46 -0
  39. data/lib/airbrake-ruby/response.rb +74 -0
  40. data/lib/airbrake-ruby/stashable.rb +15 -0
  41. data/lib/airbrake-ruby/stat.rb +73 -0
  42. data/lib/airbrake-ruby/sync_sender.rb +113 -0
  43. data/lib/airbrake-ruby/tdigest.rb +393 -0
  44. data/lib/airbrake-ruby/thread_pool.rb +128 -0
  45. data/lib/airbrake-ruby/time_truncate.rb +17 -0
  46. data/lib/airbrake-ruby/timed_trace.rb +58 -0
  47. data/lib/airbrake-ruby/truncator.rb +115 -0
  48. data/lib/airbrake-ruby/version.rb +6 -0
  49. data/spec/airbrake_spec.rb +324 -0
  50. data/spec/async_sender_spec.rb +72 -0
  51. data/spec/backtrace_spec.rb +427 -0
  52. data/spec/benchmark_spec.rb +33 -0
  53. data/spec/code_hunk_spec.rb +115 -0
  54. data/spec/config/validator_spec.rb +184 -0
  55. data/spec/config_spec.rb +154 -0
  56. data/spec/deploy_notifier_spec.rb +48 -0
  57. data/spec/file_cache_spec.rb +34 -0
  58. data/spec/filter_chain_spec.rb +92 -0
  59. data/spec/filters/context_filter_spec.rb +23 -0
  60. data/spec/filters/dependency_filter_spec.rb +12 -0
  61. data/spec/filters/exception_attributes_filter_spec.rb +50 -0
  62. data/spec/filters/gem_root_filter_spec.rb +41 -0
  63. data/spec/filters/git_last_checkout_filter_spec.rb +46 -0
  64. data/spec/filters/git_repository_filter.rb +61 -0
  65. data/spec/filters/git_revision_filter_spec.rb +126 -0
  66. data/spec/filters/keys_blacklist_spec.rb +225 -0
  67. data/spec/filters/keys_whitelist_spec.rb +194 -0
  68. data/spec/filters/root_directory_filter_spec.rb +39 -0
  69. data/spec/filters/sql_filter_spec.rb +262 -0
  70. data/spec/filters/system_exit_filter_spec.rb +14 -0
  71. data/spec/filters/thread_filter_spec.rb +277 -0
  72. data/spec/fixtures/notroot.txt +7 -0
  73. data/spec/fixtures/project_root/code.rb +221 -0
  74. data/spec/fixtures/project_root/empty_file.rb +0 -0
  75. data/spec/fixtures/project_root/long_line.txt +1 -0
  76. data/spec/fixtures/project_root/short_file.rb +3 -0
  77. data/spec/fixtures/project_root/vendor/bundle/ignored_file.rb +5 -0
  78. data/spec/helpers.rb +9 -0
  79. data/spec/ignorable_spec.rb +14 -0
  80. data/spec/inspectable_spec.rb +45 -0
  81. data/spec/monotonic_time_spec.rb +12 -0
  82. data/spec/nested_exception_spec.rb +73 -0
  83. data/spec/notice_notifier/options_spec.rb +259 -0
  84. data/spec/notice_notifier_spec.rb +356 -0
  85. data/spec/notice_spec.rb +296 -0
  86. data/spec/performance_breakdown_spec.rb +12 -0
  87. data/spec/performance_notifier_spec.rb +491 -0
  88. data/spec/promise_spec.rb +197 -0
  89. data/spec/query_spec.rb +11 -0
  90. data/spec/request_spec.rb +11 -0
  91. data/spec/response_spec.rb +88 -0
  92. data/spec/spec_helper.rb +100 -0
  93. data/spec/stashable_spec.rb +23 -0
  94. data/spec/stat_spec.rb +47 -0
  95. data/spec/sync_sender_spec.rb +133 -0
  96. data/spec/tdigest_spec.rb +230 -0
  97. data/spec/thread_pool_spec.rb +158 -0
  98. data/spec/time_truncate_spec.rb +13 -0
  99. data/spec/timed_trace_spec.rb +125 -0
  100. data/spec/truncator_spec.rb +238 -0
  101. metadata +216 -0
@@ -0,0 +1,7 @@
1
+ This
2
+ file
3
+ is
4
+ not
5
+ inside
6
+ root
7
+ directory
@@ -0,0 +1,221 @@
1
+ module Airbrake
2
+ ##
3
+ # Represents a chunk of information that is meant to be either sent to
4
+ # Airbrake or ignored completely.
5
+ #
6
+ # @since v1.0.0
7
+ class Notice
8
+ ##
9
+ # @return [Hash{Symbol=>String}] the information about the notifier library
10
+ NOTIFIER = {
11
+ name: 'airbrake-ruby'.freeze,
12
+ version: Airbrake::AIRBRAKE_RUBY_VERSION,
13
+ url: 'https://github.com/airbrake/airbrake-ruby'.freeze
14
+ }.freeze
15
+
16
+ ##
17
+ # @return [Hash{Symbol=>String,Hash}] the information to be displayed in the
18
+ # Context tab in the dashboard
19
+ CONTEXT = {
20
+ os: RUBY_PLATFORM,
21
+ language: "#{RUBY_ENGINE}/#{RUBY_VERSION}".freeze,
22
+ notifier: NOTIFIER
23
+ }.freeze
24
+
25
+ ##
26
+ # @return [Integer] the maxium size of the JSON payload in bytes
27
+ MAX_NOTICE_SIZE = 64000
28
+
29
+ ##
30
+ # @return [Integer] the maximum size of hashes, arrays and strings in the
31
+ # notice.
32
+ PAYLOAD_MAX_SIZE = 10000
33
+
34
+ ##
35
+ # @return [Array<StandardError>] the list of possible exceptions that might
36
+ # be raised when an object is converted to JSON
37
+ JSON_EXCEPTIONS = [
38
+ IOError,
39
+ NotImplementedError,
40
+ JSON::GeneratorError,
41
+ Encoding::UndefinedConversionError
42
+ ].freeze
43
+
44
+ # @return [Array<Symbol>] the list of keys that can be be overwritten with
45
+ # {Airbrake::Notice#[]=}
46
+ WRITABLE_KEYS = %i[notifier context environment session params].freeze
47
+
48
+ ##
49
+ # @return [Array<Symbol>] parts of a Notice's payload that can be modified
50
+ # by the truncator
51
+ TRUNCATABLE_KEYS = %i[errors environment session params].freeze
52
+
53
+ ##
54
+ # @return [String] the name of the host machine
55
+ HOSTNAME = Socket.gethostname.freeze
56
+
57
+ ##
58
+ # @return [String]
59
+ DEFAULT_SEVERITY = 'error'.freeze
60
+
61
+ ##
62
+ # @since v1.7.0
63
+ # @return [Hash{Symbol=>Object}] the hash with arbitrary objects to be used
64
+ # in filters
65
+ attr_reader :stash
66
+
67
+ def initialize(config, exception, params = {})
68
+ @config = config
69
+
70
+ @payload = {
71
+ errors: NestedException.new(config, exception).as_json,
72
+ context: context,
73
+ environment: {
74
+ program_name: $PROGRAM_NAME
75
+ },
76
+ session: {},
77
+ params: params
78
+ }
79
+ @stash = { exception: exception }
80
+ @truncator = Airbrake::Truncator.new(PAYLOAD_MAX_SIZE)
81
+
82
+ extract_custom_attributes(exception)
83
+ end
84
+
85
+ ##
86
+ # Converts the notice to JSON. Calls +to_json+ on each object inside
87
+ # notice's payload. Truncates notices, JSON representation of which is
88
+ # bigger than {MAX_NOTICE_SIZE}.
89
+ #
90
+ # @return [Hash{String=>String}, nil]
91
+ def to_json
92
+ loop do
93
+ begin
94
+ json = @payload.to_json
95
+ rescue *JSON_EXCEPTIONS => ex
96
+ @config.logger.debug("#{LOG_LABEL} `notice.to_json` failed: #{ex.class}: #{ex}")
97
+ else
98
+ return json if json && json.bytesize <= MAX_NOTICE_SIZE
99
+ end
100
+
101
+ break if truncate == 0
102
+ end
103
+ end
104
+
105
+ ##
106
+ # Ignores a notice. Ignored notices never reach the Airbrake dashboard.
107
+ #
108
+ # @return [void]
109
+ # @see #ignored?
110
+ # @note Ignored noticed can't be unignored
111
+ def ignore!
112
+ @payload = nil
113
+ end
114
+
115
+ ##
116
+ # Checks whether the notice was ignored.
117
+ #
118
+ # @return [Boolean]
119
+ # @see #ignore!
120
+ def ignored?
121
+ @payload.nil?
122
+ end
123
+
124
+ ##
125
+ # Reads a value from notice's payload.
126
+ # @return [Object]
127
+ #
128
+ # @raise [Airbrake::Error] if the notice is ignored
129
+ def [](key)
130
+ raise_if_ignored
131
+ @payload[key]
132
+ end
133
+
134
+ ##
135
+ # Writes a value to the payload hash. Restricts unrecognized
136
+ # writes.
137
+ # @example
138
+ # notice[:params][:my_param] = 'foobar'
139
+ #
140
+ # @return [void]
141
+ # @raise [Airbrake::Error] if the notice is ignored
142
+ # @raise [Airbrake::Error] if the +key+ is not recognized
143
+ # @raise [Airbrake::Error] if the root value is not a Hash
144
+ def []=(key, value)
145
+ raise_if_ignored
146
+
147
+ unless WRITABLE_KEYS.include?(key)
148
+ raise Airbrake::Error,
149
+ ":#{key} is not recognized among #{WRITABLE_KEYS}"
150
+ end
151
+
152
+ unless value.respond_to?(:to_hash)
153
+ raise Airbrake::Error, "Got #{value.class} value, wanted a Hash"
154
+ end
155
+
156
+ @payload[key] = value.to_hash
157
+ end
158
+
159
+ private
160
+
161
+ def context
162
+ {
163
+ version: @config.app_version,
164
+ # We ensure that root_directory is always a String, so it can always be
165
+ # converted to JSON in a predictable manner (when it's a Pathname and in
166
+ # Rails environment, it converts to unexpected JSON).
167
+ rootDirectory: @config.root_directory.to_s,
168
+ environment: @config.environment,
169
+
170
+ # Make sure we always send hostname.
171
+ hostname: HOSTNAME,
172
+
173
+ severity: DEFAULT_SEVERITY
174
+ }.merge(CONTEXT).delete_if { |_key, val| val.nil? || val.empty? }
175
+ end
176
+
177
+ def raise_if_ignored
178
+ return unless ignored?
179
+ raise Airbrake::Error, 'cannot access ignored notice'
180
+ end
181
+
182
+ def truncate
183
+ TRUNCATABLE_KEYS.each { |key| @truncator.truncate(self[key]) }
184
+
185
+ new_max_size = @truncator.reduce_max_size
186
+ if new_max_size == 0
187
+ @config.logger.error(
188
+ "#{LOG_LABEL} truncation failed. File an issue at " \
189
+ "https://github.com/airbrake/airbrake-ruby " \
190
+ "and attach the following payload: #{@payload}"
191
+ )
192
+ end
193
+
194
+ new_max_size
195
+ end
196
+
197
+ def extract_custom_attributes(exception)
198
+ return unless exception.respond_to?(:to_airbrake)
199
+ attributes = nil
200
+
201
+ begin
202
+ attributes = exception.to_airbrake
203
+ rescue StandardError => ex
204
+ @config.logger.error(
205
+ "#{LOG_LABEL} #{exception.class}#to_airbrake failed: #{ex.class}: #{ex}"
206
+ )
207
+ end
208
+
209
+ return unless attributes
210
+
211
+ begin
212
+ @payload.merge!(attributes)
213
+ rescue TypeError
214
+ @config.logger.error(
215
+ "#{LOG_LABEL} #{exception.class}#to_airbrake failed:" \
216
+ " #{attributes} must be a Hash"
217
+ )
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1 @@
1
+ loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong line
@@ -0,0 +1,3 @@
1
+ module Banana
2
+ attr_reader :bingo
3
+ end
@@ -0,0 +1,5 @@
1
+ module IgnoredFile
2
+ def ignore_me
3
+ puts 'Anybody here?'
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module Helpers
2
+ def fixture_path(filename)
3
+ File.expand_path(File.join('spec', 'fixtures', filename))
4
+ end
5
+
6
+ def project_root_path(filename)
7
+ fixture_path(File.join('project_root', filename))
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ RSpec.describe Airbrake::Ignorable do
2
+ let(:klass) do
3
+ mod = subject
4
+ Class.new { include(mod) }
5
+ end
6
+
7
+ it "ignores includee" do
8
+ instance = klass.new
9
+ expect(instance).not_to be_ignored
10
+
11
+ instance.ignore!
12
+ expect(instance).to be_ignored
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ RSpec.describe Airbrake::Inspectable do
2
+ let(:klass) do
3
+ mod = subject
4
+ Class.new do
5
+ include(mod)
6
+
7
+ def initialize
8
+ @config = Airbrake::Config.new
9
+ @filter_chain = nil
10
+ end
11
+ end
12
+ end
13
+
14
+ describe "#inspect" do
15
+ it "displays object information" do
16
+ instance = klass.new
17
+ expect(instance.inspect).to match(/
18
+ #<:0x\w+\s
19
+ project_id=""\s
20
+ project_key=""\s
21
+ host="http.+"\s
22
+ filter_chain=nil>
23
+ /x)
24
+ end
25
+ end
26
+
27
+ describe "#pretty_print" do
28
+ it "displays object information in a beautiful way" do
29
+ q = PP.new
30
+
31
+ instance = klass.new
32
+ # Guarding is needed to fix JRuby failure:
33
+ # NoMethodError: undefined method `[]' for nil:NilClass
34
+ q.guard_inspect_key { instance.pretty_print(q) }
35
+
36
+ expect(q.output).to match(/
37
+ #<:0x\w+\s
38
+ project_id=""\s
39
+ project_key=""\s
40
+ host="http.+"\s
41
+ filter_chain=nil
42
+ /x)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,12 @@
1
+ RSpec.describe Airbrake::MonotonicTime do
2
+ describe ".time_in_ms" do
3
+ it "returns monotonic time in milliseconds" do
4
+ expect(subject.time_in_ms).to be_a(Float)
5
+ end
6
+
7
+ it "always returns time in the future" do
8
+ old_time = subject.time_in_ms
9
+ expect(subject.time_in_ms).to be > old_time
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,73 @@
1
+ RSpec.describe Airbrake::NestedException do
2
+ describe "#as_json" do
3
+ context "given exceptions with backtraces" do
4
+ it "unwinds nested exceptions" do
5
+ begin
6
+ begin
7
+ raise AirbrakeTestError
8
+ rescue AirbrakeTestError
9
+ Ruby21Error.raise_error('bingo')
10
+ end
11
+ rescue Ruby21Error => ex
12
+ nested_exception = described_class.new(ex)
13
+ exceptions = nested_exception.as_json
14
+
15
+ expect(exceptions.size).to eq(2)
16
+ expect(exceptions[0][:message]).to eq('bingo')
17
+ expect(exceptions[1][:message]).to eq('App crashed!')
18
+ expect(exceptions[0][:backtrace]).not_to be_empty
19
+ expect(exceptions[1][:backtrace]).not_to be_empty
20
+ end
21
+ end
22
+
23
+ it "unwinds no more than 3 nested exceptions" do
24
+ begin
25
+ begin
26
+ raise AirbrakeTestError
27
+ rescue AirbrakeTestError
28
+ begin
29
+ Ruby21Error.raise_error('bongo')
30
+ rescue Ruby21Error
31
+ begin
32
+ Ruby21Error.raise_error('bango')
33
+ rescue Ruby21Error
34
+ Ruby21Error.raise_error('bingo')
35
+ end
36
+ end
37
+ end
38
+ rescue Ruby21Error => ex
39
+ nested_exception = described_class.new(ex)
40
+ exceptions = nested_exception.as_json
41
+
42
+ expect(exceptions.size).to eq(3)
43
+ expect(exceptions[0][:message]).to eq('bingo')
44
+ expect(exceptions[1][:message]).to eq('bango')
45
+ expect(exceptions[2][:message]).to eq('bongo')
46
+ expect(exceptions[0][:backtrace]).not_to be_empty
47
+ expect(exceptions[1][:backtrace]).not_to be_empty
48
+ end
49
+ end
50
+ end
51
+
52
+ context "given exceptions without backtraces" do
53
+ it "sets backtrace to nil" do
54
+ begin
55
+ begin
56
+ raise AirbrakeTestError
57
+ rescue AirbrakeTestError => ex2
58
+ ex2.set_backtrace([])
59
+ Ruby21Error.raise_error('bingo')
60
+ end
61
+ rescue Ruby21Error => ex1
62
+ ex1.set_backtrace([])
63
+ nested_exception = described_class.new(ex1)
64
+ exceptions = nested_exception.as_json
65
+
66
+ expect(exceptions.size).to eq(2)
67
+ expect(exceptions[0][:backtrace]).to be_empty
68
+ expect(exceptions[1][:backtrace]).to be_empty
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,259 @@
1
+ RSpec.describe Airbrake::NoticeNotifier do
2
+ let(:project_id) { 105138 }
3
+ let(:project_key) { 'fd04e13d806a90f96614ad8e529b2822' }
4
+ let(:localhost) { 'http://localhost:8080' }
5
+
6
+ let(:endpoint) do
7
+ "https://api.airbrake.io/api/v3/projects/#{project_id}/notices"
8
+ end
9
+
10
+ let(:params) { {} }
11
+ let(:ex) { AirbrakeTestError.new }
12
+
13
+ before do
14
+ stub_request(:post, endpoint).to_return(status: 201, body: '{}')
15
+
16
+ Airbrake::Config.instance = Airbrake::Config.new(
17
+ project_id: project_id,
18
+ project_key: project_key
19
+ )
20
+ end
21
+
22
+ describe "options" do
23
+ describe ":host" do
24
+ context "when custom" do
25
+ shared_examples 'endpoint' do |host, endpoint, title|
26
+ before { Airbrake::Config.instance.merge(host: host) }
27
+
28
+ example(title) do
29
+ stub_request(:post, endpoint).to_return(status: 201, body: '{}')
30
+ subject.notify_sync(ex)
31
+
32
+ expect(a_request(:post, endpoint)).to have_been_made.once
33
+ end
34
+ end
35
+
36
+ path = '/api/v3/projects/105138/notices'
37
+
38
+ context "given a full host" do
39
+ include_examples('endpoint', localhost = 'http://localhost:8080',
40
+ URI.join(localhost, path),
41
+ "sends notices to the specified host's endpoint")
42
+ end
43
+
44
+ context "given a full host" do
45
+ include_examples('endpoint', localhost = 'http://localhost',
46
+ URI.join(localhost, path),
47
+ "assumes port 80 by default")
48
+ end
49
+
50
+ context "given a host without scheme" do
51
+ include_examples 'endpoint', localhost = 'localhost:8080',
52
+ URI.join("https://#{localhost}", path),
53
+ "assumes https by default"
54
+ end
55
+
56
+ context "given only hostname" do
57
+ include_examples 'endpoint', localhost = 'localhost',
58
+ URI.join("https://#{localhost}", path),
59
+ "assumes https and port 80 by default"
60
+ end
61
+ end
62
+ end
63
+
64
+ describe ":root_directory" do
65
+ before do
66
+ subject.add_filter(
67
+ Airbrake::Filters::RootDirectoryFilter.new('/home/kyrylo/code')
68
+ )
69
+ end
70
+
71
+ it "filters out frames" do
72
+ subject.notify_sync(ex)
73
+
74
+ expect(
75
+ a_request(:post, endpoint)
76
+ .with(body: %r|{"file":"/PROJECT_ROOT/airbrake/ruby/spec/airbrake_spec.+|)
77
+ ).to have_been_made.once
78
+ end
79
+
80
+ context "when present and is a" do
81
+ shared_examples 'root directory' do |dir|
82
+ before { Airbrake::Config.instance.merge(root_directory: dir) }
83
+
84
+ it "being included into the notice's payload" do
85
+ subject.notify_sync(ex)
86
+ expect(
87
+ a_request(:post, endpoint)
88
+ .with(body: %r{"rootDirectory":"/bingo/bango"})
89
+ ).to have_been_made.once
90
+ end
91
+ end
92
+
93
+ context "String" do
94
+ include_examples 'root directory', '/bingo/bango'
95
+ end
96
+
97
+ context "Pathname" do
98
+ include_examples 'root directory', Pathname.new('/bingo/bango')
99
+ end
100
+ end
101
+ end
102
+
103
+ describe ":proxy" do
104
+ let(:proxy) do
105
+ WEBrick::HTTPServer.new(
106
+ Port: 0,
107
+ Logger: WEBrick::Log.new('/dev/null'),
108
+ AccessLog: []
109
+ )
110
+ end
111
+
112
+ let(:requests) { Queue.new }
113
+
114
+ let(:proxy_params) do
115
+ { host: 'localhost',
116
+ port: proxy.config[:Port],
117
+ user: 'user',
118
+ password: 'password' }
119
+ end
120
+
121
+ before do
122
+ Airbrake::Config.instance.merge(
123
+ proxy: proxy_params,
124
+ host: "http://localhost:#{proxy.config[:Port]}"
125
+ )
126
+
127
+ proxy.mount_proc '/' do |req, res|
128
+ requests << req
129
+ res.status = 201
130
+ res.body = "OK\n"
131
+ end
132
+
133
+ Thread.new { proxy.start }
134
+ end
135
+
136
+ after { proxy.stop }
137
+
138
+ it "is being used if configured" do
139
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6.0")
140
+ skip(
141
+ "We use Webmock 2, which doesn't support Ruby 2.6+. It's " \
142
+ "safe to run this test on 2.6+ once we upgrade to Webmock 3.5+"
143
+ )
144
+ end
145
+ subject.notify_sync(ex)
146
+
147
+ proxied_request = requests.pop(true)
148
+
149
+ expect(proxied_request.header['proxy-authorization'].first)
150
+ .to eq('Basic dXNlcjpwYXNzd29yZA==')
151
+
152
+ # rubocop:disable Metrics/LineLength
153
+ expect(proxied_request.request_line)
154
+ .to eq("POST http://localhost:#{proxy.config[:Port]}/api/v3/projects/105138/notices HTTP/1.1\r\n")
155
+ # rubocop:enable Metrics/LineLength
156
+ end
157
+ end
158
+
159
+ describe ":environment" do
160
+ context "when present" do
161
+ before { Airbrake::Config.instance.merge(environment: :production) }
162
+
163
+ it "being included into the notice's payload" do
164
+ subject.notify_sync(ex)
165
+ expect(
166
+ a_request(:post, endpoint)
167
+ .with(body: /"context":{.*"environment":"production".*}/)
168
+ ).to have_been_made.once
169
+ end
170
+ end
171
+ end
172
+
173
+ describe ":ignore_environments" do
174
+ shared_examples 'sent notice' do |params|
175
+ before { Airbrake::Config.instance.merge(params) }
176
+
177
+ it "sends a notice" do
178
+ subject.notify_sync(ex)
179
+ expect(a_request(:post, endpoint)).to have_been_made
180
+ end
181
+ end
182
+
183
+ shared_examples 'ignored notice' do |params|
184
+ before { Airbrake::Config.instance.merge(params) }
185
+
186
+ it "ignores exceptions occurring in envs that were not configured" do
187
+ subject.notify_sync(ex)
188
+ expect(a_request(:post, endpoint)).not_to have_been_made
189
+ end
190
+ end
191
+
192
+ context "when env is set and ignore_environments doesn't mention it" do
193
+ params = {
194
+ environment: :development,
195
+ ignore_environments: [:production]
196
+ }
197
+
198
+ include_examples 'sent notice', params
199
+ end
200
+
201
+ context "when the current env and notify envs are the same" do
202
+ params = {
203
+ environment: :development,
204
+ ignore_environments: %i[production development]
205
+ }
206
+
207
+ include_examples 'ignored notice', params
208
+
209
+ it "returns early and doesn't try to parse the given exception" do
210
+ expect(Airbrake::Notice).not_to receive(:new)
211
+ expect(subject.notify_sync(ex))
212
+ .to eq('error' => "current environment 'development' is ignored")
213
+ expect(a_request(:post, endpoint)).not_to have_been_made
214
+ end
215
+ end
216
+
217
+ context "when the current env is not set and notify envs are present" do
218
+ params = { ignore_environments: %i[production development] }
219
+
220
+ include_examples 'sent notice', params
221
+ end
222
+
223
+ context "when the current env is set and notify envs aren't" do
224
+ include_examples 'sent notice', environment: :development
225
+ end
226
+
227
+ context "when ignore_environments specifies a Regexp pattern" do
228
+ params = {
229
+ environment: :testing,
230
+ ignore_environments: ['staging', /test.+/]
231
+ }
232
+
233
+ include_examples 'ignored notice', params
234
+ end
235
+ end
236
+
237
+ describe ":blacklist_keys" do
238
+ # Fixes https://github.com/airbrake/airbrake-ruby/issues/276
239
+ context "when specified along with :whitelist_keys" do
240
+ context "and when context payload is present" do
241
+ before do
242
+ Airbrake::Config.instance.merge(
243
+ blacklist_keys: %i[password password_confirmation],
244
+ whitelist_keys: [:email, /user/i, 'account_id']
245
+ )
246
+ end
247
+
248
+ it "sends a notice" do
249
+ notice = subject.build_notice(ex)
250
+ notice[:context][:headers] = 'banana'
251
+ subject.notify_sync(notice)
252
+
253
+ expect(a_request(:post, endpoint)).to have_been_made
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end