airbrake-ruby 4.8.0

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