Skip to content

Commit cb8c015

Browse files
authored
Fix: Handle exception with large stacktrace without dropping entire item - by keeping N frames (#1807)
* Fix: Handle exception with large stacktrace without dropping entire item Certain exception type such as `SystemStackError` has long backtrace (thus the stack error) The whole envelope item was dropped due to payload size limit logic This ensures it tries to remove most of the stacktrace frames (except first 10) when payload is too large, so that the envelope item won't be dropped = exception still reported * Update frame truncating to keep first & last N/2 frames instead of just last N frames * Update CHANGELOG
1 parent 76a83cf commit cb8c015

File tree

3 files changed

+131
-23
lines changed

3 files changed

+131
-23
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## Unreleased
2+
3+
### Features
4+
5+
- Handle exception with large stacktrace without dropping entire item [#1807](https://github.com/getsentry/sentry-ruby/pull/1807)
6+
17
## 5.3.1
28

39
### Bug Fixes

sentry-ruby/lib/sentry/transport.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class Transport
99
PROTOCOL_VERSION = '7'
1010
USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
1111
CLIENT_REPORT_INTERVAL = 30
12+
STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD = 500
1213

1314
# https://develop.sentry.dev/sdk/client-reports/#envelope-item-payload
1415
CLIENT_REPORT_REASONS = [
@@ -82,6 +83,32 @@ def serialize_envelope(envelope)
8283
result = item.to_s
8384
end
8485

86+
if result.bytesize > Event::MAX_SERIALIZED_PAYLOAD_SIZE
87+
if single_exceptions = item.payload.dig(:exception, :values)
88+
single_exceptions.each do |single_exception|
89+
traces = single_exception.dig(:stacktrace, :frames)
90+
if traces && traces.size > STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD
91+
size_on_both_ends = STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD / 2
92+
traces.replace(
93+
traces[0..(size_on_both_ends - 1)] + traces[-size_on_both_ends..-1],
94+
)
95+
end
96+
end
97+
elsif single_exceptions = item.payload.dig("exception", "values")
98+
single_exceptions.each do |single_exception|
99+
traces = single_exception.dig("stacktrace", "frames")
100+
if traces && traces.size > STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD
101+
size_on_both_ends = STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD / 2
102+
traces.replace(
103+
traces[0..(size_on_both_ends - 1)] + traces[-size_on_both_ends..-1],
104+
)
105+
end
106+
end
107+
end
108+
109+
result = item.to_s
110+
end
111+
85112
if result.bytesize > Event::MAX_SERIALIZED_PAYLOAD_SIZE
86113
size_breakdown = item.payload.map do |key, value|
87114
"#{key}: #{JSON.generate(value).bytesize}"

sentry-ruby/spec/sentry/transport_spec.rb

Lines changed: 98 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -100,40 +100,115 @@
100100
end
101101

102102
context "oversized event" do
103-
let(:event) { client.event_from_message("foo") }
104-
let(:envelope) { subject.envelope_from_event(event) }
103+
context "due to breadcrumb" do
104+
let(:event) { client.event_from_message("foo") }
105+
let(:envelope) { subject.envelope_from_event(event) }
105106

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)
107+
before do
108+
event.breadcrumbs = Sentry::BreadcrumbBuffer.new(100)
109+
100.times do |i|
110+
event.breadcrumbs.record Sentry::Breadcrumb.new(category: i.to_s, message: "x" * Sentry::Event::MAX_MESSAGE_SIZE_IN_BYTES)
111+
end
112+
serialized_result = JSON.generate(event.to_hash)
113+
expect(serialized_result.bytesize).to be > Sentry::Event::MAX_SERIALIZED_PAYLOAD_SIZE
110114
end
111-
serialized_result = JSON.generate(event.to_hash)
112-
expect(serialized_result.bytesize).to be > Sentry::Event::MAX_SERIALIZED_PAYLOAD_SIZE
113-
end
114115

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
116+
it "removes breadcrumbs and carry on" do
117+
data, _ = subject.serialize_envelope(envelope)
118+
expect(data.bytesize).to be < Sentry::Event::MAX_SERIALIZED_PAYLOAD_SIZE
119+
120+
expect(envelope.items.count).to eq(1)
121+
122+
event_item = envelope.items.first
123+
expect(event_item.payload[:breadcrumbs]).to be_nil
124+
end
118125

119-
expect(envelope.items.count).to eq(1)
126+
context "if it's still oversized" do
127+
before do
128+
100.times do |i|
129+
event.contexts["context_#{i}"] = "s" * Sentry::Event::MAX_MESSAGE_SIZE_IN_BYTES
130+
end
131+
end
120132

121-
event_item = envelope.items.first
122-
expect(event_item.payload[:breadcrumbs]).to be_nil
133+
it "rejects the item and logs attributes size breakdown" do
134+
data, _ = subject.serialize_envelope(envelope)
135+
expect(data).to be_nil
136+
expect(io.string).not_to match(/Sending envelope with items \[event\]/)
137+
expect(io.string).to match(/tags: 2, contexts: 820791, extra: 2/)
138+
end
139+
end
123140
end
124141

125-
context "if it's still oversized" do
142+
context "due to stacktrace frames" do
143+
let(:event) { client.event_from_exception(SystemStackError.new("stack level too deep")) }
144+
let(:envelope) { subject.envelope_from_event(event) }
145+
146+
let(:in_app_pattern) do
147+
project_root = "/fake/project_root"
148+
Regexp.new("^(#{project_root}/)?#{Sentry::Backtrace::APP_DIRS_PATTERN}")
149+
end
150+
let(:frame_list_limit) { Sentry::Transport::STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD }
151+
let(:frame_list_size) { frame_list_limit * 4 }
152+
126153
before do
127-
100.times do |i|
128-
event.contexts["context_#{i}"] = "s" * Sentry::Event::MAX_MESSAGE_SIZE_IN_BYTES
129-
end
154+
single_exception = event.exception.instance_variable_get(:@values)[0]
155+
new_stacktrace = Sentry::StacktraceInterface.new(
156+
frames: frame_list_size.times.map do |zero_based_index|
157+
Sentry::StacktraceInterface::Frame.new(
158+
"/fake/path",
159+
Sentry::Backtrace::Line.parse("app.rb:#{zero_based_index + 1}:in `/'", in_app_pattern)
160+
)
161+
end,
162+
)
163+
single_exception.instance_variable_set(:@stacktrace, new_stacktrace)
164+
165+
serialized_result = JSON.generate(event.to_hash)
166+
expect(serialized_result.bytesize).to be > Sentry::Event::MAX_SERIALIZED_PAYLOAD_SIZE
130167
end
131168

132-
it "rejects the item and logs attributes size breakdown" do
169+
it "keeps some stacktrace frames and carry on" do
133170
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/)
171+
expect(data.bytesize).to be < Sentry::Event::MAX_SERIALIZED_PAYLOAD_SIZE
172+
173+
expect(envelope.items.count).to eq(1)
174+
175+
event_item = envelope.items.first
176+
frames = event_item.payload[:exception][:values][0][:stacktrace][:frames]
177+
expect(frames.length).to eq(frame_list_limit)
178+
179+
# Last N lines kept
180+
# N = Frame limit / 2
181+
expect(frames[-1][:lineno]).to eq(frame_list_size)
182+
expect(frames[-1][:filename]).to eq('app.rb')
183+
expect(frames[-1][:function]).to eq('/')
184+
#
185+
expect(frames[-(frame_list_limit / 2)][:lineno]).to eq(frame_list_size - ((frame_list_limit / 2) - 1))
186+
expect(frames[-(frame_list_limit / 2)][:filename]).to eq('app.rb')
187+
expect(frames[-(frame_list_limit / 2)][:function]).to eq('/')
188+
189+
# First N lines kept
190+
# N = Frame limit / 2
191+
expect(frames[0][:lineno]).to eq(1)
192+
expect(frames[0][:filename]).to eq('app.rb')
193+
expect(frames[0][:function]).to eq('/')
194+
expect(frames[(frame_list_limit / 2) - 1][:lineno]).to eq(frame_list_limit / 2)
195+
expect(frames[(frame_list_limit / 2) - 1][:filename]).to eq('app.rb')
196+
expect(frames[(frame_list_limit / 2) - 1][:function]).to eq('/')
197+
end
198+
199+
context "if it's still oversized" do
200+
before do
201+
100.times do |i|
202+
event.contexts["context_#{i}"] = "s" * Sentry::Event::MAX_MESSAGE_SIZE_IN_BYTES
203+
end
204+
end
205+
206+
it "rejects the item and logs attributes size breakdown" do
207+
data, _ = subject.serialize_envelope(envelope)
208+
expect(data).to be_nil
209+
expect(io.string).not_to match(/Sending envelope with items \[event\]/)
210+
expect(io.string).to match(/tags: 2, contexts: 820791, extra: 2/)
211+
end
137212
end
138213
end
139214
end

0 commit comments

Comments
 (0)