Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check envelope size before sending it #1747

Merged
merged 6 commits into from
Mar 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,46 @@
## Unreleased
## 5.2.0

### Features

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

**Example**

```rb
Sentry.init do |config|
config.traces_sampler = lambda do |sampling_context|
env = sampling_context[:env]

if env["REQUEST_METHOD"] == "GET"
0.01
else
0.1
end
end
end
```

- Check envelope size before sending it [#1747](https://github.com/getsentry/sentry-ruby/pull/1747)

The SDK will now check if the envelope's event items are oversized before sending the envelope. It goes like this:

1. If an event is oversized (200kb), the SDK will remove its breadcrumbs (which in our experience is the most common cause).
2. If the event size now falls within the limit, it'll be sent.
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,

```
{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}
```

This will help users report size-related issues in the future.


- Automatic session tracking [#1715](https://github.com/getsentry/sentry-ruby/pull/1715)

**Example**:

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

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

The SDK now supports [automatic session tracking / release health](https://docs.sentry.io/product/releases/health/) by default in Rack based applications.
Aggregate statistics on successful / errored requests are collected and sent to the server every minute.
Expand Down
4 changes: 0 additions & 4 deletions sentry-ruby/lib/sentry/envelope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ def add_item(headers, payload)
@items << Item.new(headers, payload)
end

def to_s
[JSON.generate(@headers), *@items.map(&:to_s)].join("\n")
end

def item_types
@items.map(&:type)
end
Expand Down
1 change: 1 addition & 0 deletions sentry-ruby/lib/sentry/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Event
WRITER_ATTRIBUTES = SERIALIZEABLE_ATTRIBUTES - %i(type timestamp level)

MAX_MESSAGE_SIZE_IN_BYTES = 1024 * 8
MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 200

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

Expand Down
39 changes: 37 additions & 2 deletions sentry-ruby/lib/sentry/transport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,43 @@ def send_envelope(envelope)

return if envelope.items.empty?

log_info("[Transport] Sending envelope with items [#{envelope.item_types.join(', ')}] #{envelope.event_id} to Sentry")
send_data(envelope.to_s)
data, serialized_items = serialize_envelope(envelope)

if data
log_info("[Transport] Sending envelope with items [#{serialized_items.map(&:type).join(', ')}] #{envelope.event_id} to Sentry")
send_data(data)
end
end

def serialize_envelope(envelope)
serialized_items = []
serialized_results = []

envelope.items.each do |item|
result = item.to_s

if result.bytesize > Event::MAX_SERIALIZED_PAYLOAD_SIZE
item.payload.delete(:breadcrumbs)
result = item.to_s
end

if result.bytesize > Event::MAX_SERIALIZED_PAYLOAD_SIZE
size_breakdown = item.payload.map do |key, value|
"#{key}: #{JSON.generate(value).bytesize}"
end.join(", ")

log_debug("Envelope item [#{item.type}] is still oversized without breadcrumbs: {#{size_breakdown}}")

next
end

serialized_results << result
serialized_items << item
end

data = [JSON.generate(envelope.headers), *serialized_results].join("\n") unless serialized_results.empty?

[data, serialized_items]
end

def is_rate_limited?(item_type)
Expand Down
2 changes: 1 addition & 1 deletion sentry-ruby/spec/sentry/transport/http_transport_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
let(:client) { Sentry::Client.new(configuration) }
let(:event) { client.event_from_message("foobarbaz") }
let(:data) do
subject.envelope_from_event(event.to_hash).to_s
subject.serialize_envelope(subject.envelope_from_event(event.to_hash)).first
end

subject { client.transport }
Expand Down
168 changes: 154 additions & 14 deletions sentry-ruby/spec/sentry/transport_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,13 @@

subject { client.transport }

describe "#envelope_from_event" do

before do
Sentry.init do |config|
config.dsn = DUMMY_DSN
end
end

describe "#serialize_envelope" do
context "normal event" do
let(:event) { client.event_from_exception(ZeroDivisionError.new("divided by 0")) }
let(:envelope) { subject.envelope_from_event(event) }

it "generates correct envelope content" do
result = subject.envelope_from_event(event.to_hash).to_s
result, _ = subject.serialize_envelope(envelope)

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

Expand All @@ -51,12 +46,11 @@
let(:transaction) do
Sentry::Transaction.new(name: "test transaction", op: "rack.request", hub: hub)
end
let(:event) do
client.event_from_transaction(transaction)
end
let(:event) { client.event_from_transaction(transaction) }
let(:envelope) { subject.envelope_from_event(event) }

it "generates correct envelope content" do
result = subject.envelope_from_event(event.to_hash).to_s
result, _ = subject.serialize_envelope(envelope)

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

Expand All @@ -76,14 +70,15 @@

context "client report" do
let(:event) { client.event_from_exception(ZeroDivisionError.new("divided by 0")) }
let(:envelope) { subject.envelope_from_event(event) }
before do
5.times { subject.record_lost_event(:ratelimit_backoff, 'error') }
3.times { subject.record_lost_event(:queue_overflow, 'transaction') }
end

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

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

Expand All @@ -103,6 +98,151 @@
end
end
end

context "oversized event" do
let(:event) { client.event_from_message("foo") }
let(:envelope) { subject.envelope_from_event(event) }

before do
event.breadcrumbs = Sentry::BreadcrumbBuffer.new(100)
100.times do |i|
event.breadcrumbs.record Sentry::Breadcrumb.new(category: i.to_s, message: "x" * Sentry::Event::MAX_MESSAGE_SIZE_IN_BYTES)
end
serialized_result = JSON.generate(event.to_hash)
expect(serialized_result.bytesize).to be > Sentry::Event::MAX_SERIALIZED_PAYLOAD_SIZE
end

it "removes breadcrumbs and carry on" do
data, _ = subject.serialize_envelope(envelope)
expect(data.bytesize).to be < Sentry::Event::MAX_SERIALIZED_PAYLOAD_SIZE

expect(envelope.items.count).to eq(1)

event_item = envelope.items.first
expect(event_item.payload[:breadcrumbs]).to be_nil
end

context "if it's still oversized" do
before do
100.times do |i|
event.contexts["context_#{i}"] = "s" * Sentry::Event::MAX_MESSAGE_SIZE_IN_BYTES
end
end

it "rejects the item and logs attributes size breakdown" do
data, _ = subject.serialize_envelope(envelope)
expect(data).to be_nil
expect(io.string).not_to match(/Sending envelope with items \[event\]/)
expect(io.string).to match(/tags: 2, contexts: 820791, extra: 2/)
end
end
end
end

describe "#send_envelope" do
context "normal event" do
let(:event) { client.event_from_exception(ZeroDivisionError.new("divided by 0")) }
let(:envelope) { subject.envelope_from_event(event) }

it "sends the event and logs the action" do
expect(subject).to receive(:send_data)

subject.send_envelope(envelope)

expect(io.string).to match(/Sending envelope with items \[event\]/)
end
end

context "transaction event" do
let(:transaction) do
Sentry::Transaction.new(name: "test transaction", op: "rack.request", hub: hub)
end
let(:event) { client.event_from_transaction(transaction) }
let(:envelope) { subject.envelope_from_event(event) }

it "sends the event and logs the action" do
expect(subject).to receive(:send_data)

subject.send_envelope(envelope)

expect(io.string).to match(/Sending envelope with items \[transaction\]/)
end
end

context "client report" do
let(:event) { client.event_from_exception(ZeroDivisionError.new("divided by 0")) }
let(:envelope) { subject.envelope_from_event(event) }
before do
5.times { subject.record_lost_event(:ratelimit_backoff, 'error') }
3.times { subject.record_lost_event(:queue_overflow, 'transaction') }
end

it "sends the event and logs the action" do
Timecop.travel(Time.now + 90) do
expect(subject).to receive(:send_data)

subject.send_envelope(envelope)

expect(io.string).to match(/Sending envelope with items \[event, client_report\]/)
end
end
end

context "oversized event" do
let(:event) { client.event_from_message("foo") }
let(:envelope) { subject.envelope_from_event(event) }

before do
event.breadcrumbs = Sentry::BreadcrumbBuffer.new(100)
100.times do |i|
event.breadcrumbs.record Sentry::Breadcrumb.new(category: i.to_s, message: "x" * Sentry::Event::MAX_MESSAGE_SIZE_IN_BYTES)
end
serialized_result = JSON.generate(event.to_hash)
expect(serialized_result.bytesize).to be > Sentry::Event::MAX_SERIALIZED_PAYLOAD_SIZE
end

it "sends the event and logs the action" do
expect(subject).to receive(:send_data)

subject.send_envelope(envelope)

expect(io.string).to match(/Sending envelope with items \[event\]/)
end

context "if it's still oversized" do
before do
100.times do |i|
event.contexts["context_#{i}"] = "s" * Sentry::Event::MAX_MESSAGE_SIZE_IN_BYTES
end
end

it "rejects the event item and doesn't send the envelope" do
expect(subject).not_to receive(:send_data)

subject.send_envelope(envelope)

expect(io.string).to match(/tags: 2, contexts: 820791, extra: 2/)
expect(io.string).not_to match(/Sending envelope with items \[event\]/)
end

context "with other types of items" do
before do
5.times { subject.record_lost_event(:ratelimit_backoff, 'error') }
3.times { subject.record_lost_event(:queue_overflow, 'transaction') }
end

it "excludes oversized event and sends the rest" do
Timecop.travel(Time.now + 90) do
expect(subject).to receive(:send_data)

subject.send_envelope(envelope)

expect(io.string).to match(/Sending envelope with items \[client_report\]/)
end
end
end
end
end
end

describe "#send_event" do
Expand Down