airbrake-ruby 1.0.0.rc.1
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.
- checksums.yaml +7 -0
- data/lib/airbrake-ruby.rb +292 -0
- data/lib/airbrake-ruby/async_sender.rb +90 -0
- data/lib/airbrake-ruby/backtrace.rb +75 -0
- data/lib/airbrake-ruby/config.rb +120 -0
- data/lib/airbrake-ruby/filter_chain.rb +86 -0
- data/lib/airbrake-ruby/filters.rb +10 -0
- data/lib/airbrake-ruby/filters/keys_blacklist.rb +37 -0
- data/lib/airbrake-ruby/filters/keys_filter.rb +65 -0
- data/lib/airbrake-ruby/filters/keys_whitelist.rb +37 -0
- data/lib/airbrake-ruby/notice.rb +207 -0
- data/lib/airbrake-ruby/notifier.rb +145 -0
- data/lib/airbrake-ruby/payload_truncator.rb +141 -0
- data/lib/airbrake-ruby/response.rb +53 -0
- data/lib/airbrake-ruby/sync_sender.rb +76 -0
- data/lib/airbrake-ruby/version.rb +7 -0
- data/spec/airbrake_spec.rb +177 -0
- data/spec/async_sender_spec.rb +121 -0
- data/spec/backtrace_spec.rb +77 -0
- data/spec/config_spec.rb +67 -0
- data/spec/filter_chain_spec.rb +157 -0
- data/spec/notice_spec.rb +190 -0
- data/spec/notifier_spec.rb +690 -0
- data/spec/notifier_spec/options_spec.rb +217 -0
- data/spec/payload_truncator_spec.rb +458 -0
- data/spec/spec_helper.rb +98 -0
- metadata +158 -0
@@ -0,0 +1,217 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Airbrake::Notifier do
|
4
|
+
let(:project_id) { 105138 }
|
5
|
+
let(:project_key) { 'fd04e13d806a90f96614ad8e529b2822' }
|
6
|
+
let(:localhost) { 'http://localhost:8080' }
|
7
|
+
|
8
|
+
let(:endpoint) do
|
9
|
+
"https://airbrake.io/api/v3/projects/#{project_id}/notices?key=#{project_key}"
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:airbrake_params) do
|
13
|
+
{ project_id: project_id,
|
14
|
+
project_key: project_key,
|
15
|
+
logger: Logger.new(StringIO.new) }
|
16
|
+
end
|
17
|
+
|
18
|
+
let(:ex) { AirbrakeTestError.new }
|
19
|
+
|
20
|
+
before do
|
21
|
+
stub_request(:post, endpoint).to_return(status: 201, body: '{}')
|
22
|
+
@airbrake = described_class.new(airbrake_params)
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "options" do
|
26
|
+
describe ":host" do
|
27
|
+
context "when custom" do
|
28
|
+
shared_examples 'endpoint' do |host, endpoint, title|
|
29
|
+
example(title) do
|
30
|
+
stub_request(:post, endpoint).to_return(status: 201, body: '{}')
|
31
|
+
@airbrake = described_class.new(airbrake_params.merge(host: host))
|
32
|
+
@airbrake.notify_sync(ex)
|
33
|
+
|
34
|
+
expect(a_request(:post, endpoint)).to have_been_made.once
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
path = '/api/v3/projects/105138/notices?key=fd04e13d806a90f96614ad8e529b2822'
|
39
|
+
|
40
|
+
context "given a full host" do
|
41
|
+
include_examples('endpoint', localhost = 'http://localhost:8080',
|
42
|
+
URI.join(localhost, path),
|
43
|
+
"sends notices to the specified host's endpoint")
|
44
|
+
end
|
45
|
+
|
46
|
+
context "given a full host" do
|
47
|
+
include_examples('endpoint', localhost = 'http://localhost',
|
48
|
+
URI.join(localhost, path),
|
49
|
+
"assumes port 80 by default")
|
50
|
+
end
|
51
|
+
|
52
|
+
context "given a host without scheme" do
|
53
|
+
include_examples 'endpoint', localhost = 'localhost:8080',
|
54
|
+
URI.join("https://#{localhost}", path),
|
55
|
+
"assumes https by default"
|
56
|
+
end
|
57
|
+
|
58
|
+
context "given only hostname" do
|
59
|
+
include_examples 'endpoint', localhost = 'localhost',
|
60
|
+
URI.join("https://#{localhost}", path),
|
61
|
+
"assumes https and port 80 by default"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe ":root_directory" do
|
67
|
+
it "filters out frames" do
|
68
|
+
params = airbrake_params.merge(root_directory: '/home/kyrylo/code')
|
69
|
+
airbrake = described_class.new(params)
|
70
|
+
airbrake.notify_sync(ex)
|
71
|
+
|
72
|
+
expect(
|
73
|
+
a_request(:post, endpoint).
|
74
|
+
with(body: %r|{"file":"\[PROJECT_ROOT\]/airbrake/ruby/spec/airbrake_spec.+|)
|
75
|
+
).to have_been_made.once
|
76
|
+
end
|
77
|
+
|
78
|
+
context "when present and is a" do
|
79
|
+
shared_examples 'root directory' do |dir|
|
80
|
+
it "being included into the notice's payload" do
|
81
|
+
params = airbrake_params.merge(root_directory: dir)
|
82
|
+
airbrake = described_class.new(params)
|
83
|
+
airbrake.notify_sync(ex)
|
84
|
+
|
85
|
+
expect(
|
86
|
+
a_request(:post, endpoint).
|
87
|
+
with(body: %r{"rootDirectory":"/bingo/bango"})
|
88
|
+
).to have_been_made.once
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context "String" do
|
93
|
+
include_examples 'root directory', '/bingo/bango'
|
94
|
+
end
|
95
|
+
|
96
|
+
context "Pathname" do
|
97
|
+
include_examples 'root directory', Pathname.new('/bingo/bango')
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe ":proxy" do
|
103
|
+
let(:proxy) do
|
104
|
+
WEBrick::HTTPServer.new(
|
105
|
+
Port: 0,
|
106
|
+
Logger: WEBrick::Log.new('/dev/null'),
|
107
|
+
AccessLog: []
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
let(:requests) { Queue.new }
|
112
|
+
|
113
|
+
let(:proxy_params) do
|
114
|
+
{ host: 'localhost',
|
115
|
+
port: proxy.config[:Port],
|
116
|
+
user: 'user',
|
117
|
+
password: 'password' }
|
118
|
+
end
|
119
|
+
|
120
|
+
before do
|
121
|
+
proxy.mount_proc '/' do |req, res|
|
122
|
+
requests << req
|
123
|
+
res.status = 201
|
124
|
+
res.body = "OK\n"
|
125
|
+
end
|
126
|
+
|
127
|
+
Thread.new { proxy.start }
|
128
|
+
|
129
|
+
params = airbrake_params.merge(
|
130
|
+
proxy: proxy_params,
|
131
|
+
host: "http://localhost:#{proxy.config[:Port]}"
|
132
|
+
)
|
133
|
+
|
134
|
+
@airbrake = described_class.new(params)
|
135
|
+
end
|
136
|
+
|
137
|
+
after { proxy.stop }
|
138
|
+
|
139
|
+
it "is being used if configured" do
|
140
|
+
@airbrake.notify_sync(ex)
|
141
|
+
|
142
|
+
proxied_request = requests.pop
|
143
|
+
|
144
|
+
expect(proxied_request.header['proxy-authorization'].first).
|
145
|
+
to eq('Basic dXNlcjpwYXNzd29yZA==')
|
146
|
+
|
147
|
+
# rubocop:disable Metrics/LineLength
|
148
|
+
expect(proxied_request.request_line).
|
149
|
+
to eq("POST http://localhost:#{proxy.config[:Port]}/api/v3/projects/105138/notices?key=fd04e13d806a90f96614ad8e529b2822 HTTP/1.1\r\n")
|
150
|
+
# rubocop:enable Metrics/LineLength
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
describe ":environment" do
|
155
|
+
context "when present" do
|
156
|
+
it "being included into the notice's payload" do
|
157
|
+
params = airbrake_params.merge(environment: :production)
|
158
|
+
airbrake = described_class.new(params)
|
159
|
+
airbrake.notify_sync(ex)
|
160
|
+
|
161
|
+
expect(
|
162
|
+
a_request(:post, endpoint).
|
163
|
+
with(body: /"context":{.*"environment":"production".*}/)
|
164
|
+
).to have_been_made.once
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
describe ":ignore_environments" do
|
170
|
+
shared_examples 'sent notice' do |params|
|
171
|
+
it "sends a notice" do
|
172
|
+
airbrake = described_class.new(airbrake_params.merge(params))
|
173
|
+
airbrake.notify_sync(ex)
|
174
|
+
|
175
|
+
expect(a_request(:post, endpoint)).to have_been_made
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
shared_examples 'ignored notice' do |params|
|
180
|
+
it "ignores exceptions occurring in envs that were not configured" do
|
181
|
+
airbrake = described_class.new(airbrake_params.merge(params))
|
182
|
+
airbrake.notify_sync(ex)
|
183
|
+
|
184
|
+
expect(a_request(:post, endpoint)).not_to have_been_made
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
context "when env is set and ignore_environments doesn't mention it" do
|
189
|
+
params = {
|
190
|
+
environment: :development,
|
191
|
+
ignore_environments: [:production]
|
192
|
+
}
|
193
|
+
|
194
|
+
include_examples 'sent notice', params
|
195
|
+
end
|
196
|
+
|
197
|
+
context "when the current env and notify envs are the same" do
|
198
|
+
params = {
|
199
|
+
environment: :development,
|
200
|
+
ignore_environments: [:production, :development]
|
201
|
+
}
|
202
|
+
|
203
|
+
include_examples 'ignored notice', params
|
204
|
+
end
|
205
|
+
|
206
|
+
context "when the current env is not set and notify envs are present" do
|
207
|
+
params = { ignore_environments: [:production, :development] }
|
208
|
+
|
209
|
+
include_examples 'sent notice', params
|
210
|
+
end
|
211
|
+
|
212
|
+
context "when the current env is set and notify envs aren't" do
|
213
|
+
include_examples 'sent notice', environment: :development
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
@@ -0,0 +1,458 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
RSpec.describe Airbrake::PayloadTruncator do
|
5
|
+
let(:max_size) { 1000 }
|
6
|
+
let(:truncated_len) { '[Truncated]'.length }
|
7
|
+
let(:max_len) { max_size + truncated_len }
|
8
|
+
|
9
|
+
before do
|
10
|
+
@truncator = described_class.new(max_size, Logger.new('/dev/null'))
|
11
|
+
end
|
12
|
+
|
13
|
+
describe ".truncate_error" do
|
14
|
+
let(:error) do
|
15
|
+
{ type: 'AirbrakeTestError', message: 'App crashed!', backtrace: [] }
|
16
|
+
end
|
17
|
+
|
18
|
+
before do
|
19
|
+
@stdout = StringIO.new
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "error backtrace" do
|
23
|
+
before do
|
24
|
+
backtrace = Array.new(size) do
|
25
|
+
{ file: 'foo.rb', line: 23, function: '<main>' }
|
26
|
+
end
|
27
|
+
|
28
|
+
@error = error.merge(backtrace: backtrace)
|
29
|
+
described_class.new(max_size, Logger.new(@stdout)).truncate_error(@error)
|
30
|
+
end
|
31
|
+
|
32
|
+
context "when long" do
|
33
|
+
let(:size) { 2003 }
|
34
|
+
|
35
|
+
it "truncates the backtrace to the max size" do
|
36
|
+
expect(@error[:backtrace].size).to eq(1000)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "logs the about the number of truncated frames" do
|
40
|
+
expect(@stdout.string).
|
41
|
+
to match(/INFO -- .+ dropped 1003 frame\(s\) from AirbrakeTestError/)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "when short" do
|
46
|
+
let(:size) { 999 }
|
47
|
+
|
48
|
+
it "does not truncate the backtrace" do
|
49
|
+
expect(@error[:backtrace].size).to eq(size)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "doesn't log anything" do
|
53
|
+
expect(@stdout.string).to be_empty
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "error message" do
|
59
|
+
before do
|
60
|
+
@error = error.merge(message: message)
|
61
|
+
described_class.new(max_size, Logger.new(@stdout)).truncate_error(@error)
|
62
|
+
end
|
63
|
+
|
64
|
+
context "when long" do
|
65
|
+
let(:message) { 'App crashed!' * 2000 }
|
66
|
+
|
67
|
+
it "truncates the message" do
|
68
|
+
expect(@error[:message].length).to eq(max_len)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "logs about the truncated string" do
|
72
|
+
expect(@stdout.string).
|
73
|
+
to match(/INFO -- .+ truncated the message of AirbrakeTestError/)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context "when short" do
|
78
|
+
let(:message) { 'App crashed!' }
|
79
|
+
let(:msg_len) { message.length }
|
80
|
+
|
81
|
+
it "doesn't truncate the message" do
|
82
|
+
expect(@error[:message].length).to eq(msg_len)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "doesn't log about the truncated string" do
|
86
|
+
expect(@stdout.string).to be_empty
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe ".truncate_object" do
|
93
|
+
describe "given a hash with short values" do
|
94
|
+
let(:params) do
|
95
|
+
{ bingo: 'bango', bongo: 'bish', bash: 'bosh' }
|
96
|
+
end
|
97
|
+
|
98
|
+
it "doesn't get truncated" do
|
99
|
+
@truncator.truncate_object(params)
|
100
|
+
expect(params).to eq(bingo: 'bango', bongo: 'bish', bash: 'bosh')
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "given a hash with a lot of elements" do
|
105
|
+
context "the elements of which are also hashes with a lot of elements" do
|
106
|
+
let(:params) do
|
107
|
+
Hash[(0...4124).each_cons(2).to_a].tap do |h|
|
108
|
+
h[0] = Hash[(0...4124).each_cons(2).to_a]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
it "truncates all the hashes to the max allowed size" do
|
113
|
+
expect(params.size).to eq(4123)
|
114
|
+
expect(params[0].size).to eq(4123)
|
115
|
+
|
116
|
+
@truncator.truncate_object(params)
|
117
|
+
|
118
|
+
expect(params.size).to eq(1000)
|
119
|
+
expect(params[0].size).to eq(1000)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "given a set with a lot of elements" do
|
125
|
+
context "the elements of which are also sets with a lot of elements" do
|
126
|
+
let(:params) do
|
127
|
+
row = (0...4124).each_cons(2)
|
128
|
+
set = Set.new(row.to_a.unshift(row.to_a))
|
129
|
+
{ bingo: set }
|
130
|
+
end
|
131
|
+
|
132
|
+
it "truncates all the sets to the max allowed size" do
|
133
|
+
expect(params[:bingo].size).to eq(4124)
|
134
|
+
expect(params[:bingo].to_a[0].size).to eq(4123)
|
135
|
+
|
136
|
+
@truncator.truncate_object(params)
|
137
|
+
|
138
|
+
expect(params[:bingo].size).to eq(1000)
|
139
|
+
expect(params[:bingo].to_a[0].size).to eq(1000)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
context "including recursive sets" do
|
144
|
+
let(:params) do
|
145
|
+
a = Set.new
|
146
|
+
a << a << :bango
|
147
|
+
{ bingo: a }
|
148
|
+
end
|
149
|
+
|
150
|
+
it "prevents recursion" do
|
151
|
+
@truncator.truncate_object(params)
|
152
|
+
|
153
|
+
expect(params).to eq(bingo: Set.new(['[Circular]', :bango]))
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
describe "given an array with a lot of elements" do
|
159
|
+
context "the elements of which are also arrays with a lot of elements" do
|
160
|
+
let(:params) do
|
161
|
+
row = (0...4124).each_cons(2)
|
162
|
+
{ bingo: row.to_a.unshift(row.to_a) }
|
163
|
+
end
|
164
|
+
|
165
|
+
it "truncates all the arrays to the max allowed size" do
|
166
|
+
expect(params[:bingo].size).to eq(4124)
|
167
|
+
expect(params[:bingo][0].size).to eq(4123)
|
168
|
+
|
169
|
+
@truncator.truncate_object(params)
|
170
|
+
|
171
|
+
expect(params[:bingo].size).to eq(1000)
|
172
|
+
expect(params[:bingo][0].size).to eq(1000)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
describe "given a hash with long values" do
|
178
|
+
context "which are strings" do
|
179
|
+
let(:params) do
|
180
|
+
{ bingo: 'bango' * 2000, bongo: 'bish', bash: 'bosh' * 1000 }
|
181
|
+
end
|
182
|
+
|
183
|
+
it "truncates only long strings" do
|
184
|
+
expect(params[:bingo].length).to eq(10_000)
|
185
|
+
expect(params[:bongo].length).to eq(4)
|
186
|
+
expect(params[:bash].length).to eq(4000)
|
187
|
+
|
188
|
+
@truncator.truncate_object(params)
|
189
|
+
|
190
|
+
expect(params[:bingo].length).to eq(max_len)
|
191
|
+
expect(params[:bongo].length).to eq(4)
|
192
|
+
expect(params[:bash].length).to eq(max_len)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
context "which are arrays" do
|
197
|
+
context "of long strings" do
|
198
|
+
let(:params) do
|
199
|
+
{ bingo: ['foo', 'bango' * 2000, 'bar', 'piyo' * 2000, 'baz'],
|
200
|
+
bongo: 'bish',
|
201
|
+
bash: 'bosh' * 1000 }
|
202
|
+
end
|
203
|
+
|
204
|
+
it "truncates long strings in the array, but not short ones" do
|
205
|
+
expect(params[:bingo].map(&:length)).to eq([3, 10_000, 3, 8_000, 3])
|
206
|
+
expect(params[:bongo].length).to eq(4)
|
207
|
+
expect(params[:bash].length).to eq(4000)
|
208
|
+
|
209
|
+
@truncator.truncate_object(params)
|
210
|
+
|
211
|
+
expect(params[:bingo].map(&:length)).to eq([3, max_len, 3, max_len, 3])
|
212
|
+
expect(params[:bongo].length).to eq(4)
|
213
|
+
expect(params[:bash].length).to eq(max_len)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
context "of short strings" do
|
218
|
+
let(:params) do
|
219
|
+
{ bingo: %w(foo bar baz), bango: 'bongo', bish: 'bash' }
|
220
|
+
end
|
221
|
+
|
222
|
+
it "truncates long strings in the array, but not short ones" do
|
223
|
+
@truncator.truncate_object(params)
|
224
|
+
expect(params).
|
225
|
+
to eq(bingo: %w(foo bar baz), bango: 'bongo', bish: 'bash')
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
context "of hashes" do
|
230
|
+
context "with long strings" do
|
231
|
+
let(:params) do
|
232
|
+
{ bingo: [{}, { bango: 'bongo', hoge: { fuga: 'piyo' * 2000 } }],
|
233
|
+
bish: 'bash',
|
234
|
+
bosh: 'foo' }
|
235
|
+
end
|
236
|
+
|
237
|
+
it "truncates the long string" do
|
238
|
+
expect(params[:bingo][1][:hoge][:fuga].length).to eq(8000)
|
239
|
+
|
240
|
+
@truncator.truncate_object(params)
|
241
|
+
|
242
|
+
expect(params[:bingo][0]).to eq({})
|
243
|
+
expect(params[:bingo][1][:bango]).to eq('bongo')
|
244
|
+
expect(params[:bingo][1][:hoge][:fuga].length).to eq(max_len)
|
245
|
+
expect(params[:bish]).to eq('bash')
|
246
|
+
expect(params[:bosh]).to eq('foo')
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
context "with short strings" do
|
251
|
+
let(:params) do
|
252
|
+
{ bingo: [{}, { bango: 'bongo', hoge: { fuga: 'piyo' } }],
|
253
|
+
bish: 'bash',
|
254
|
+
bosh: 'foo' }
|
255
|
+
end
|
256
|
+
|
257
|
+
it "doesn't truncate the short string" do
|
258
|
+
expect(params[:bingo][1][:hoge][:fuga].length).to eq(4)
|
259
|
+
|
260
|
+
@truncator.truncate_object(params)
|
261
|
+
|
262
|
+
expect(params[:bingo][0]).to eq({})
|
263
|
+
expect(params[:bingo][1][:bango]).to eq('bongo')
|
264
|
+
expect(params[:bingo][1][:hoge][:fuga].length).to eq(4)
|
265
|
+
expect(params[:bish]).to eq('bash')
|
266
|
+
expect(params[:bosh]).to eq('foo')
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
context "with strings that equal to max_size" do
|
271
|
+
before do
|
272
|
+
@truncator = described_class.new(max_size, Logger.new('/dev/null'))
|
273
|
+
end
|
274
|
+
|
275
|
+
let(:params) { { unicode: '1111' } }
|
276
|
+
let(:max_size) { params[:unicode].size }
|
277
|
+
|
278
|
+
it "is doesn't truncate the string" do
|
279
|
+
@truncator.truncate_object(params)
|
280
|
+
|
281
|
+
expect(params[:unicode].length).to eq(max_size)
|
282
|
+
expect(params[:unicode]).to match(/\A1{#{max_size}}\z/)
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
context "of recursive hashes" do
|
288
|
+
let(:params) do
|
289
|
+
a = { bingo: {} }
|
290
|
+
a[:bingo][:bango] = a
|
291
|
+
end
|
292
|
+
|
293
|
+
it "prevents recursion" do
|
294
|
+
@truncator.truncate_object(params)
|
295
|
+
|
296
|
+
expect(params).to eq(bingo: { bango: '[Circular]' })
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
context "of arrays" do
|
301
|
+
context "with long strings" do
|
302
|
+
let(:params) do
|
303
|
+
{ bingo: ['bango', ['bongo', ['bish' * 2000]]],
|
304
|
+
bish: 'bash',
|
305
|
+
bosh: 'foo' }
|
306
|
+
end
|
307
|
+
|
308
|
+
it "truncates only the long string" do
|
309
|
+
expect(params[:bingo][1][1][0].length).to eq(8000)
|
310
|
+
|
311
|
+
@truncator.truncate_object(params)
|
312
|
+
|
313
|
+
expect(params[:bingo][1][1][0].length).to eq(max_len)
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
context "of recursive arrays" do
|
319
|
+
let(:params) do
|
320
|
+
a = []
|
321
|
+
a << a << :bango
|
322
|
+
{ bingo: a }
|
323
|
+
end
|
324
|
+
|
325
|
+
it "prevents recursion" do
|
326
|
+
@truncator.truncate_object(params)
|
327
|
+
|
328
|
+
expect(params).to eq(bingo: ['[Circular]', :bango])
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
context "which are arbitrary objects" do
|
334
|
+
context "with default #to_s" do
|
335
|
+
let(:params) { { bingo: Object.new } }
|
336
|
+
|
337
|
+
it "converts the object to a safe string" do
|
338
|
+
@truncator.truncate_object(params)
|
339
|
+
|
340
|
+
expect(params[:bingo]).to include('Object')
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
context "with redefined #to_s" do
|
345
|
+
let(:params) do
|
346
|
+
obj = Object.new
|
347
|
+
|
348
|
+
def obj.to_s
|
349
|
+
'bango' * 2000
|
350
|
+
end
|
351
|
+
|
352
|
+
{ bingo: obj }
|
353
|
+
end
|
354
|
+
|
355
|
+
it "truncates the string if it's too long" do
|
356
|
+
@truncator.truncate_object(params)
|
357
|
+
|
358
|
+
expect(params[:bingo].length).to eq(max_len)
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
context "with other owner than Kernel" do
|
363
|
+
let(:params) do
|
364
|
+
mod = Module.new do
|
365
|
+
def to_s
|
366
|
+
"I am a fancy object" * 2000
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
klass = Class.new { include mod }
|
371
|
+
|
372
|
+
{ bingo: klass.new }
|
373
|
+
end
|
374
|
+
|
375
|
+
it "truncates the string it if it's long" do
|
376
|
+
@truncator.truncate_object(params)
|
377
|
+
|
378
|
+
expect(params[:bingo].length).to eq(max_len)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
context "multiple copies of the same object" do
|
384
|
+
let(:params) do
|
385
|
+
bingo = []
|
386
|
+
bango = ['bongo']
|
387
|
+
bingo << bango << bango
|
388
|
+
{ bish: bingo }
|
389
|
+
end
|
390
|
+
|
391
|
+
it "are not being truncated" do
|
392
|
+
@truncator.truncate_object(params)
|
393
|
+
|
394
|
+
expect(params).to eq(bish: [['bongo'], ['bongo']])
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
describe "unicode payload" do
|
400
|
+
before do
|
401
|
+
@truncator = described_class.new(max_size - 1, Logger.new('/dev/null'))
|
402
|
+
end
|
403
|
+
|
404
|
+
describe "truncation" do
|
405
|
+
let(:params) { { unicode: "€€€€" } }
|
406
|
+
let(:max_size) { params[:unicode].length }
|
407
|
+
|
408
|
+
it "is performed correctly" do
|
409
|
+
@truncator.truncate_object(params)
|
410
|
+
|
411
|
+
expect(params[:unicode].length).to eq(max_len - 1)
|
412
|
+
|
413
|
+
if RUBY_VERSION == '1.9.2'
|
414
|
+
expect(params[:unicode]).to match(/\A?{#{max_size - 1}}\[Truncated\]\z/)
|
415
|
+
else
|
416
|
+
expect(params[:unicode]).to match(/\A€{#{max_size - 1}}\[Truncated\]\z/)
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
describe "string encoding conversion" do
|
422
|
+
let(:params) { { unicode: "bad string€\xAE" } }
|
423
|
+
let(:max_size) { 100 }
|
424
|
+
|
425
|
+
it "converts strings to valid UTF-8" do
|
426
|
+
@truncator.truncate_object(params)
|
427
|
+
|
428
|
+
if RUBY_VERSION == '1.9.2'
|
429
|
+
expect(params[:unicode]).to eq('bad string??')
|
430
|
+
else
|
431
|
+
expect(params[:unicode]).to match(/\Abad string€[�\?]\z/)
|
432
|
+
end
|
433
|
+
|
434
|
+
expect { params.to_json }.not_to raise_error
|
435
|
+
end
|
436
|
+
|
437
|
+
it "converts ASCII-8BIT strings with invalid characters to UTF-8 correctly" do
|
438
|
+
# Shenanigans to get a bad ASCII-8BIT string. Direct conversion raises error.
|
439
|
+
encoded = Base64.encode64("\xD3\xE6\xBC\x9D\xBA").encode!('ASCII-8BIT')
|
440
|
+
bad_string = Base64.decode64(encoded)
|
441
|
+
|
442
|
+
params = { unicode: bad_string }
|
443
|
+
|
444
|
+
@truncator.truncate_object(params)
|
445
|
+
|
446
|
+
expect(params[:unicode]).to match(/[�\?]{4}/)
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
describe "given a non-recursible object" do
|
452
|
+
it "raises error" do
|
453
|
+
expect { @truncator.truncate_object(:bingo) }.
|
454
|
+
to raise_error(Airbrake::Error, /cannot truncate object/)
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|