Skip to content

Commit c40f83d

Browse files
committed
stuff
1 parent 756ca36 commit c40f83d

9 files changed

Lines changed: 94 additions & 110 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,6 @@ We recommend migrating to `data_collection` to match the behavior you want event
6464
- Add sensitive key-value collection filter by @sl0thentr0py in [#3025](https://github.com/getsentry/sentry-ruby/pull/3025)
6565
- Add base DataCollection configuration with defaults and backfill by @sl0thentr0py in [#3022](https://github.com/getsentry/sentry-ruby/pull/3022)
6666

67-
### Bug Fixes 🐛
68-
69-
- Sanitize breadcrumb data and structured-log attribute values to valid UTF-8 where they're filled in, and add a last-resort rescue for `EncodingError`/`JSON::GeneratorError` in `Transport#send_envelope`, to support the `json` gem 3.0, which now raises instead of warning when generating JSON from a String tagged with a non-UTF-8 encoding. Fixes #2462
70-
7167
## 6.7.0
7268

7369
### New Features ✨

sentry-ruby/lib/sentry/breadcrumb.rb

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22

33
module Sentry
44
class Breadcrumb
5-
MAX_NESTING = 10
6-
DATA_SERIALIZATION_ERROR_MESSAGE = "[data were removed due to serialization issues]"
7-
85
# @return [String, nil]
96
attr_accessor :category
107
# @return [Hash, nil]
@@ -37,7 +34,7 @@ def initialize(category: nil, data: nil, message: nil, timestamp: nil, level: ni
3734
def to_h
3835
{
3936
category: @category,
40-
data: serialized_data,
37+
data: @data,
4138
level: @level,
4239
message: @message,
4340
timestamp: @timestamp,
@@ -51,8 +48,7 @@ def message=(message)
5148
@message = message && Utils::EncodingHelper.valid_utf_8?(message) ? message.byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES) : ""
5249
end
5350

54-
# Sanitizes the breadcrumb's arbitrary, user-supplied data so it doesn't
55-
# carry a String with an invalid/non-UTF-8 encoding into JSON generation.
51+
# Sanitizes the breadcrumb's arbitrary, user-supplied data encoding.
5652
# @param data [Hash, nil]
5753
# @return [void]
5854
def data=(data)
@@ -64,22 +60,5 @@ def data=(data)
6460
def level=(level) # needed to meet the Sentry spec
6561
@level = level == "warn" ? "warning" : level
6662
end
67-
68-
private
69-
70-
def serialized_data
71-
begin
72-
::JSON.parse(::JSON.generate(@data, max_nesting: MAX_NESTING))
73-
rescue Exception => e
74-
Sentry.sdk_logger.debug(LOGGER_PROGNAME) do
75-
<<~MSG
76-
can't serialize breadcrumb data because of error: #{e}
77-
data: #{@data}
78-
MSG
79-
end
80-
81-
{ error: DATA_SERIALIZATION_ERROR_MESSAGE }
82-
end
83-
end
8463
end
8564
end

sentry-ruby/lib/sentry/envelope/item.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ def serialize
7272
end
7373

7474
[result, result.bytesize > size_limit]
75+
rescue EncodingError, JSON::GeneratorError => e
76+
[nil, false, e]
7577
end
7678

7779
def size_breakdown

sentry-ruby/lib/sentry/transport.rb

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,28 +67,21 @@ def send_envelope(envelope)
6767
serialized_items&.each do |item|
6868
record_lost_event(:send_error, item.data_category, num: item.item_count, num_bytes: item.lost_event_byte_size)
6969
end
70-
rescue EncodingError, JSON::GeneratorError => e
71-
# As of json 3.0, `JSON.generate` raises instead of warning when it
72-
# encounters a String with an invalid/non-UTF-8 encoding (e.g. a
73-
# BINARY-tagged String). We sanitize known data-entry points (e.g.
74-
# breadcrumb data, log attributes), but this is a last resort so an
75-
# unexpected case can't crash the background worker.
76-
log_error("[Transport] Failed to serialize envelope", e, debug: @debug)
77-
78-
# `serialized_items` may still be nil here if the error was raised
79-
# while serializing the envelope itself (rather than while sending
80-
# already-serialized data), so fall back to the envelope's own items.
81-
(serialized_items || envelope.items).each do |item|
82-
record_lost_event(:send_error, item.data_category, num: item.item_count)
83-
end
8470
end
8571

8672
def serialize_envelope(envelope)
8773
serialized_items = []
8874
serialized_results = []
8975

9076
envelope.items.each do |item|
91-
result, oversized = item.serialize
77+
result, oversized, serialization_error = item.serialize
78+
79+
if serialization_error
80+
log_error("[Transport] Failed to serialize envelope item [#{item.type}]", serialization_error, debug: @debug)
81+
record_lost_event(:send_error, item.data_category, num: item.item_count)
82+
83+
next
84+
end
9285

9386
if oversized
9487
log_debug("Envelope item [#{item.type}] is still oversized after size reduction: {#{item.size_breakdown}}")

sentry-ruby/lib/sentry/utils/encoding_helper.rb

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,44 @@ def self.safe_utf_8_string(value)
2727
# Recursively walks a Hash/Array/String structure and returns a copy
2828
# with every String forced into valid UTF-8 encoding.
2929
#
30-
# This is needed because `JSON.generate`/`JSON.dump` on json 3.0+ raise
31-
# an `Encoding::UndefinedConversionError` (previously just a
32-
# deprecation warning on json 2.8+) when they encounter a String
33-
# tagged with a non-UTF-8 encoding (e.g. `ASCII-8BIT`/`BINARY`) that
34-
# contains bytes invalid for the target encoding. Use this to sanitize
35-
# payloads before handing them to the JSON generator.
30+
# Circular Hash and Array references are replaced with nil in the
31+
# returned copy.
3632
#
3733
# @param value [Object]
3834
# @return [Object]
39-
def self.deep_encode_utf_8(value)
35+
def self.deep_encode_utf_8(value, seen = {})
4036
case value
4137
when String
4238
encode_to_utf_8(value)
4339
when Hash
44-
value.each_with_object({}) do |(key, val), memo|
45-
memo[deep_encode_utf_8(key)] = deep_encode_utf_8(val)
40+
return nil if seen.key?(value.object_id)
41+
42+
seen[value.object_id] = true
43+
encoded_value = {}
44+
45+
begin
46+
value.each do |key, val|
47+
encoded_key = deep_encode_utf_8(key, seen)
48+
encoded_value[encoded_key] = deep_encode_utf_8(val, seen)
49+
end
50+
encoded_value
51+
ensure
52+
seen.delete(value.object_id)
4653
end
4754
when Array
48-
value.map { |val| deep_encode_utf_8(val) }
55+
return nil if seen.key?(value.object_id)
56+
57+
seen[value.object_id] = true
58+
encoded_value = []
59+
60+
begin
61+
value.each do |val|
62+
encoded_value << deep_encode_utf_8(val, seen)
63+
end
64+
encoded_value
65+
ensure
66+
seen.delete(value.object_id)
67+
end
4968
else
5069
value
5170
end

sentry-ruby/spec/sentry/breadcrumb_buffer_spec.rb

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,6 @@
2323
)
2424
end
2525

26-
let(:problematic_crumb) do
27-
# circular reference
28-
a = []
29-
b = []
30-
a.push(b)
31-
b.push(a)
32-
33-
Sentry::Breadcrumb.new(
34-
category: "baz",
35-
message: "crumb_3",
36-
data: a
37-
)
38-
end
39-
4026
describe "#record" do
4127
subject do
4228
described_class.new(1)
@@ -56,18 +42,15 @@
5642
end
5743

5844
describe "#to_h" do
59-
it "doesn't break because of 1 problematic crumb" do
45+
it "serializes breadcrumbs" do
6046
subject.record(crumb_1)
6147
subject.record(crumb_2)
62-
subject.record(problematic_crumb)
6348

6449
result = subject.to_h[:values]
6550

6651
expect(result[0][:category]).to eq("foo")
67-
expect(result[0][:data]).to eq({ "name" => "John", "age" => 25 })
52+
expect(result[0][:data]).to eq({ name: "John", age: 25 })
6853
expect(result[1][:category]).to eq("bar")
69-
expect(result[2][:category]).to eq("baz")
70-
expect(result[2][:data][:error]).to eq("[data were removed due to serialization issues]")
7154
end
7255
end
7356
end

sentry-ruby/spec/sentry/breadcrumb_spec.rb

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -79,40 +79,21 @@
7979
)
8080
end
8181

82-
let(:very_deep_crumb) do
83-
data = [[[[[ { a: [{ b: [[{ c: 4 }]] }] }]]]]]
84-
85-
Sentry::Breadcrumb.new(
86-
category: "cow",
87-
message: "I cause too much recursion",
88-
data: data
89-
)
90-
end
91-
92-
it "serializes data correctly" do
82+
it "returns the sanitized data" do
9383
result = crumb.to_h
9484

9585
expect(result[:category]).to eq("foo")
9686
expect(result[:message]).to eq("crumb")
97-
expect(result[:data]).to eq({ "name" => "John", "age" => 25 })
87+
expect(result[:data]).to eq({ name: "John", age: 25 })
9888
end
9989

100-
it "rescues data serialization issue and ditch the data" do
90+
it "handles a circular breadcrumb without recursing forever" do
10191
result = problematic_crumb.to_h
10292

10393
expect(result[:category]).to eq("baz")
10494
expect(result[:message]).to eq("I cause issues")
105-
expect(result[:data][:error]).to eq("[data were removed due to serialization issues]")
106-
expect(stringio.string).to match(/can't serialize breadcrumb data because of error: nesting of 10 is too deep/)
107-
end
108-
109-
it "rescues data serialization issue for extremely nested data and ditch the data" do
110-
result = very_deep_crumb.to_h
111-
112-
expect(result[:category]).to eq("cow")
113-
expect(result[:message]).to eq("I cause too much recursion")
114-
expect(result[:data][:error]).to eq("[data were removed due to serialization issues]")
115-
expect(stringio.string).to match(/can't serialize breadcrumb data because of error: nesting of 10 is too deep/)
95+
expect(result[:data]).to eq([[nil]])
96+
expect { JSON.generate(result[:data]) }.not_to raise_error
11697
end
11798

11899
it "sanitizes non-UTF-8 encoded strings in data at assignment time (json 3.0+ behavior)" do
@@ -128,7 +109,8 @@
128109
expect(crumb.data[:note].valid_encoding?).to eq(true)
129110

130111
result = crumb.to_h
131-
expect(result[:data]).not_to eq({ error: Sentry::Breadcrumb::DATA_SERIALIZATION_ERROR_MESSAGE })
112+
expect(result[:data][:note].encoding).to eq(Encoding::UTF_8)
113+
expect(result[:data][:note].valid_encoding?).to eq(true)
132114
end
133115
end
134116
end

sentry-ruby/spec/sentry/transport_spec.rb

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,20 @@
499499
end
500500
end
501501

502+
context "when sending raises an encoding error" do
503+
let(:event) { client.event_from_exception(ZeroDivisionError.new("divided by 0")) }
504+
let(:envelope) { subject.envelope_from_event(event) }
505+
506+
before do
507+
allow(subject).to receive(:send_data).and_raise(EncodingError, "simulated send error")
508+
end
509+
510+
it "does not handle the error as a serialization failure" do
511+
expect { subject.send_envelope(envelope) }.to raise_error(EncodingError, "simulated send error")
512+
expect(io.string).not_to match(/Failed to serialize envelope/)
513+
end
514+
end
515+
502516
context "transaction event" do
503517
let(:transaction) do
504518
Sentry::Transaction.new(name: "test transaction", op: "rack.request")
@@ -652,27 +666,33 @@
652666
end
653667
end
654668

655-
context "when JSON.generate raises an encoding error (json 3.0+ behavior)" do
656-
# json 3.0+ raises Encoding::UndefinedConversionError (a subclass of
657-
# EncodingError) instead of just warning when JSON.generate encounters
658-
# a String tagged with a non-UTF-8 encoding that contains bytes invalid
659-
# for the target encoding. Simulate that here regardless of the json
660-
# gem version actually loaded, as a last-resort safety net for cases
661-
# not already covered by sanitizing data at the point it's filled in
662-
# (e.g. breadcrumb data, log attributes).
663-
let(:event) { client.event_from_exception(ZeroDivisionError.new("divided by 0")) }
664-
let(:envelope) { subject.envelope_from_event(event) }
669+
context "when JSON.generate raises an encoding error for an item (json 3.0+ behavior)" do
670+
let(:bad_payload) { { message: "bad payload" } }
671+
let(:good_payload) { { message: "good payload" } }
672+
let(:envelope) do
673+
Sentry::Envelope.new.tap do |new_envelope|
674+
new_envelope.add_item({ type: "event" }, bad_payload)
675+
new_envelope.add_item({ type: "event" }, good_payload)
676+
end
677+
end
665678

666679
before do
667-
allow(JSON).to receive(:generate).and_raise(EncodingError, "simulated json 3.0 encoding error")
680+
allow(JSON).to receive(:generate).and_wrap_original do |original, value|
681+
raise EncodingError, "simulated json 3.0 encoding error" if value.equal?(bad_payload)
682+
683+
original.call(value)
684+
end
668685
end
669686

670-
it "does not raise, logs the failure, and records a lost event instead of sending" do
671-
expect(subject).not_to receive(:send_data)
687+
it "skips the failed item, sends the remaining items, and records the loss" do
688+
expect(subject).to receive(:send_data) do |data|
689+
expect(data).to include("good payload")
690+
expect(data).not_to include("bad payload")
691+
end
672692

673693
expect { subject.send_envelope(envelope) }.not_to raise_error
674694

675-
expect(io.string).to match(/Failed to serialize envelope/)
695+
expect(io.string).to match(/Failed to serialize envelope item/)
676696
expect(subject).to have_recorded_lost_event(:send_error, 'error')
677697
end
678698
end

sentry-ruby/spec/sentry/utils/encoding_helper_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,15 @@
5555

5656
expect(value[:message].encoding).to eq(Encoding::BINARY)
5757
end
58+
59+
it "replaces circular references without recursing forever" do
60+
value = []
61+
value << value
62+
63+
result = described_class.deep_encode_utf_8(value)
64+
65+
expect(result).to eq([nil])
66+
expect(value.first).to equal(value)
67+
end
5868
end
5969
end

0 commit comments

Comments
 (0)