Skip to content

Commit e2f679b

Browse files
authored
Check envelope size before sending it (#1747)
* Add Transport#serialize_envelope to control envelope serialization Because we now need to check if a serialized envelope item is oversized (see #1603 (comment)), envelope serialization is better performed by Transport to have more control to filter oversized items. * Avoid mutating the envelope's items * Update changelog * Resize changelog image * Update changelog * Update CHANGELOG.md
1 parent e727167 commit e2f679b

File tree

6 files changed

+226
-24
lines changed

6 files changed

+226
-24
lines changed

CHANGELOG.md

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,46 @@
1-
## Unreleased
1+
## 5.2.0
22

33
### Features
44

55
- Log Redis command arguments when sending PII is enabled [#1726](https://github.com/getsentry/sentry-ruby/pull/1726)
66
- Add request env to sampling context [#1749](https://github.com/getsentry/sentry-ruby/pull/1749)
77

8+
**Example**
9+
10+
```rb
11+
Sentry.init do |config|
12+
config.traces_sampler = lambda do |sampling_context|
13+
env = sampling_context[:env]
14+
15+
if env["REQUEST_METHOD"] == "GET"
16+
0.01
17+
else
18+
0.1
19+
end
20+
end
21+
end
22+
```
23+
24+
- Check envelope size before sending it [#1747](https://github.com/getsentry/sentry-ruby/pull/1747)
25+
26+
The SDK will now check if the envelope's event items are oversized before sending the envelope. It goes like this:
27+
28+
1. If an event is oversized (200kb), the SDK will remove its breadcrumbs (which in our experience is the most common cause).
29+
2. If the event size now falls within the limit, it'll be sent.
30+
3. Otherwise, the event will be thrown away. The SDK will also log a debug message about the event's attributes size (in bytes) breakdown. For example,
31+
32+
```
33+
{event_id: 34, level: 7, timestamp: 22, environment: 13, server_name: 14, modules: 935, message: 5, user: 2, tags: 2, contexts: 820791, extra: 2, fingerprint: 2, platform: 6, sdk: 40, threads: 51690}
34+
```
35+
36+
This will help users report size-related issues in the future.
37+
38+
839
- Automatic session tracking [#1715](https://github.com/getsentry/sentry-ruby/pull/1715)
940

1041
**Example**:
11-
12-
![image](https://user-images.githubusercontent.com/6536764/157057827-2893527e-7973-4901-a070-bd78a720574a.png)
1342

43+
<img width="80%" src="https://user-images.githubusercontent.com/6536764/157057827-2893527e-7973-4901-a070-bd78a720574a.png">
1444

1545
The SDK now supports [automatic session tracking / release health](https://docs.sentry.io/product/releases/health/) by default in Rack based applications.
1646
Aggregate statistics on successful / errored requests are collected and sent to the server every minute.

sentry-ruby/lib/sentry/envelope.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@ def add_item(headers, payload)
3434
@items << Item.new(headers, payload)
3535
end
3636

37-
def to_s
38-
[JSON.generate(@headers), *@items.map(&:to_s)].join("\n")
39-
end
40-
4137
def item_types
4238
@items.map(&:type)
4339
end

sentry-ruby/lib/sentry/event.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class Event
2323
WRITER_ATTRIBUTES = SERIALIZEABLE_ATTRIBUTES - %i(type timestamp level)
2424

2525
MAX_MESSAGE_SIZE_IN_BYTES = 1024 * 8
26+
MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 200
2627

2728
SKIP_INSPECTION_ATTRIBUTES = [:@modules, :@stacktrace_builder, :@send_default_pii, :@trusted_proxies, :@rack_env_whitelist]
2829

sentry-ruby/lib/sentry/transport.rb

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,43 @@ def send_envelope(envelope)
5757

5858
return if envelope.items.empty?
5959

60-
log_info("[Transport] Sending envelope with items [#{envelope.item_types.join(', ')}] #{envelope.event_id} to Sentry")
61-
send_data(envelope.to_s)
60+
data, serialized_items = serialize_envelope(envelope)
61+
62+
if data
63+
log_info("[Transport] Sending envelope with items [#{serialized_items.map(&:type).join(', ')}] #{envelope.event_id} to Sentry")
64+
send_data(data)
65+
end
66+
end
67+
68+
def serialize_envelope(envelope)
69+
serialized_items = []
70+
serialized_results = []
71+
72+
envelope.items.each do |item|
73+
result = item.to_s
74+
75+
if result.bytesize > Event::MAX_SERIALIZED_PAYLOAD_SIZE
76+
item.payload.delete(:breadcrumbs)
77+
result = item.to_s
78+
end
79+
80+
if result.bytesize > Event::MAX_SERIALIZED_PAYLOAD_SIZE
81+
size_breakdown = item.payload.map do |key, value|
82+
"#{key}: #{JSON.generate(value).bytesize}"
83+
end.join(", ")
84+
85+
log_debug("Envelope item [#{item.type}] is still oversized without breadcrumbs: {#{size_breakdown}}")
86+
87+
next
88+
end
89+
90+
serialized_results << result
91+
serialized_items << item
92+
end
93+
94+
data = [JSON.generate(envelope.headers), *serialized_results].join("\n") unless serialized_results.empty?
95+
96+
[data, serialized_items]
6297
end
6398

6499
def is_rate_limited?(item_type)

sentry-ruby/spec/sentry/transport/http_transport_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
let(:client) { Sentry::Client.new(configuration) }
1414
let(:event) { client.event_from_message("foobarbaz") }
1515
let(:data) do
16-
subject.envelope_from_event(event.to_hash).to_s
16+
subject.serialize_envelope(subject.envelope_from_event(event.to_hash)).first
1717
end
1818

1919
subject { client.transport }

sentry-ruby/spec/sentry/transport_spec.rb

Lines changed: 154 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,13 @@
1818

1919
subject { client.transport }
2020

21-
describe "#envelope_from_event" do
22-
23-
before do
24-
Sentry.init do |config|
25-
config.dsn = DUMMY_DSN
26-
end
27-
end
28-
21+
describe "#serialize_envelope" do
2922
context "normal event" do
3023
let(:event) { client.event_from_exception(ZeroDivisionError.new("divided by 0")) }
24+
let(:envelope) { subject.envelope_from_event(event) }
25+
3126
it "generates correct envelope content" do
32-
result = subject.envelope_from_event(event.to_hash).to_s
27+
result, _ = subject.serialize_envelope(envelope)
3328

3429
envelope_header, item_header, item = result.split("\n")
3530

@@ -51,12 +46,11 @@
5146
let(:transaction) do
5247
Sentry::Transaction.new(name: "test transaction", op: "rack.request", hub: hub)
5348
end
54-
let(:event) do
55-
client.event_from_transaction(transaction)
56-
end
49+
let(:event) { client.event_from_transaction(transaction) }
50+
let(:envelope) { subject.envelope_from_event(event) }
5751

5852
it "generates correct envelope content" do
59-
result = subject.envelope_from_event(event.to_hash).to_s
53+
result, _ = subject.serialize_envelope(envelope)
6054

6155
envelope_header, item_header, item = result.split("\n")
6256

@@ -76,14 +70,15 @@
7670

7771
context "client report" do
7872
let(:event) { client.event_from_exception(ZeroDivisionError.new("divided by 0")) }
73+
let(:envelope) { subject.envelope_from_event(event) }
7974
before do
8075
5.times { subject.record_lost_event(:ratelimit_backoff, 'error') }
8176
3.times { subject.record_lost_event(:queue_overflow, 'transaction') }
8277
end
8378

8479
it "incudes client report in envelope" do
8580
Timecop.travel(Time.now + 90) do
86-
result = subject.envelope_from_event(event.to_hash).to_s
81+
result, _ = subject.serialize_envelope(envelope)
8782

8883
client_report_header, client_report_payload = result.split("\n").last(2)
8984

@@ -103,6 +98,151 @@
10398
end
10499
end
105100
end
101+
102+
context "oversized event" do
103+
let(:event) { client.event_from_message("foo") }
104+
let(:envelope) { subject.envelope_from_event(event) }
105+
106+
before do
107+
event.breadcrumbs = Sentry::BreadcrumbBuffer.new(100)
108+
100.times do |i|
109+
event.breadcrumbs.record Sentry::Breadcrumb.new(category: i.to_s, message: "x" * Sentry::Event::MAX_MESSAGE_SIZE_IN_BYTES)
110+
end
111+
serialized_result = JSON.generate(event.to_hash)
112+
expect(serialized_result.bytesize).to be > Sentry::Event::MAX_SERIALIZED_PAYLOAD_SIZE
113+
end
114+
115+
it "removes breadcrumbs and carry on" do
116+
data, _ = subject.serialize_envelope(envelope)
117+
expect(data.bytesize).to be < Sentry::Event::MAX_SERIALIZED_PAYLOAD_SIZE
118+
119+
expect(envelope.items.count).to eq(1)
120+
121+
event_item = envelope.items.first
122+
expect(event_item.payload[:breadcrumbs]).to be_nil
123+
end
124+
125+
context "if it's still oversized" do
126+
before do
127+
100.times do |i|
128+
event.contexts["context_#{i}"] = "s" * Sentry::Event::MAX_MESSAGE_SIZE_IN_BYTES
129+
end
130+
end
131+
132+
it "rejects the item and logs attributes size breakdown" do
133+
data, _ = subject.serialize_envelope(envelope)
134+
expect(data).to be_nil
135+
expect(io.string).not_to match(/Sending envelope with items \[event\]/)
136+
expect(io.string).to match(/tags: 2, contexts: 820791, extra: 2/)
137+
end
138+
end
139+
end
140+
end
141+
142+
describe "#send_envelope" do
143+
context "normal event" do
144+
let(:event) { client.event_from_exception(ZeroDivisionError.new("divided by 0")) }
145+
let(:envelope) { subject.envelope_from_event(event) }
146+
147+
it "sends the event and logs the action" do
148+
expect(subject).to receive(:send_data)
149+
150+
subject.send_envelope(envelope)
151+
152+
expect(io.string).to match(/Sending envelope with items \[event\]/)
153+
end
154+
end
155+
156+
context "transaction event" do
157+
let(:transaction) do
158+
Sentry::Transaction.new(name: "test transaction", op: "rack.request", hub: hub)
159+
end
160+
let(:event) { client.event_from_transaction(transaction) }
161+
let(:envelope) { subject.envelope_from_event(event) }
162+
163+
it "sends the event and logs the action" do
164+
expect(subject).to receive(:send_data)
165+
166+
subject.send_envelope(envelope)
167+
168+
expect(io.string).to match(/Sending envelope with items \[transaction\]/)
169+
end
170+
end
171+
172+
context "client report" do
173+
let(:event) { client.event_from_exception(ZeroDivisionError.new("divided by 0")) }
174+
let(:envelope) { subject.envelope_from_event(event) }
175+
before do
176+
5.times { subject.record_lost_event(:ratelimit_backoff, 'error') }
177+
3.times { subject.record_lost_event(:queue_overflow, 'transaction') }
178+
end
179+
180+
it "sends the event and logs the action" do
181+
Timecop.travel(Time.now + 90) do
182+
expect(subject).to receive(:send_data)
183+
184+
subject.send_envelope(envelope)
185+
186+
expect(io.string).to match(/Sending envelope with items \[event, client_report\]/)
187+
end
188+
end
189+
end
190+
191+
context "oversized event" do
192+
let(:event) { client.event_from_message("foo") }
193+
let(:envelope) { subject.envelope_from_event(event) }
194+
195+
before do
196+
event.breadcrumbs = Sentry::BreadcrumbBuffer.new(100)
197+
100.times do |i|
198+
event.breadcrumbs.record Sentry::Breadcrumb.new(category: i.to_s, message: "x" * Sentry::Event::MAX_MESSAGE_SIZE_IN_BYTES)
199+
end
200+
serialized_result = JSON.generate(event.to_hash)
201+
expect(serialized_result.bytesize).to be > Sentry::Event::MAX_SERIALIZED_PAYLOAD_SIZE
202+
end
203+
204+
it "sends the event and logs the action" do
205+
expect(subject).to receive(:send_data)
206+
207+
subject.send_envelope(envelope)
208+
209+
expect(io.string).to match(/Sending envelope with items \[event\]/)
210+
end
211+
212+
context "if it's still oversized" do
213+
before do
214+
100.times do |i|
215+
event.contexts["context_#{i}"] = "s" * Sentry::Event::MAX_MESSAGE_SIZE_IN_BYTES
216+
end
217+
end
218+
219+
it "rejects the event item and doesn't send the envelope" do
220+
expect(subject).not_to receive(:send_data)
221+
222+
subject.send_envelope(envelope)
223+
224+
expect(io.string).to match(/tags: 2, contexts: 820791, extra: 2/)
225+
expect(io.string).not_to match(/Sending envelope with items \[event\]/)
226+
end
227+
228+
context "with other types of items" do
229+
before do
230+
5.times { subject.record_lost_event(:ratelimit_backoff, 'error') }
231+
3.times { subject.record_lost_event(:queue_overflow, 'transaction') }
232+
end
233+
234+
it "excludes oversized event and sends the rest" do
235+
Timecop.travel(Time.now + 90) do
236+
expect(subject).to receive(:send_data)
237+
238+
subject.send_envelope(envelope)
239+
240+
expect(io.string).to match(/Sending envelope with items \[client_report\]/)
241+
end
242+
end
243+
end
244+
end
245+
end
106246
end
107247

108248
describe "#send_event" do

0 commit comments

Comments
 (0)