Skip to content

Commit

Permalink
Generalize size limits based on envelope item type (#2421)
Browse files Browse the repository at this point in the history
* Fix spec file location

* Extract Envelope::Item to its own file

* Add Envelope::Item#size_limit

* Make Envelope::Item#{headers,payload} just readers

We don't set headers and payloads after initializing an item, so these
accessors were not needed.

* Memoize Envelope::Item#type in the constructor

It's faster like that given that we rely on type value in multiple
places

* Memoize Envelope::Item#data_category

Same as with type, we rely on this value in multiple places so it makes
no sense to keep calculating it multiple times

* Add custom size limit for profile items

* Update CHANGELOG
  • Loading branch information
solnic authored Sep 30, 2024
1 parent e6028ab commit a070e08
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 85 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased

### Internal

- Profile items have bigger size limit now ([#2421](https://github.com/getsentry/sentry-ruby/pull/2421))

## 5.20.1

### Bug Fixes
Expand Down
87 changes: 2 additions & 85 deletions sentry-ruby/lib/sentry/envelope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,91 +3,6 @@
module Sentry
# @api private
class Envelope
class Item
STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD = 500
MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 1000

attr_accessor :headers, :payload

def initialize(headers, payload)
@headers = headers
@payload = payload
end

def type
@headers[:type] || "event"
end

# rate limits and client reports use the data_category rather than envelope item type
def self.data_category(type)
case type
when "session", "attachment", "transaction", "profile", "span" then type
when "sessions" then "session"
when "check_in" then "monitor"
when "statsd", "metric_meta" then "metric_bucket"
when "event" then "error"
when "client_report" then "internal"
else "default"
end
end

def data_category
self.class.data_category(type)
end

def to_s
[JSON.generate(@headers), @payload.is_a?(String) ? @payload : JSON.generate(@payload)].join("\n")
end

def serialize
result = to_s

if result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE
remove_breadcrumbs!
result = to_s
end

if result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE
reduce_stacktrace!
result = to_s
end

[result, result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE]
end

def size_breakdown
payload.map do |key, value|
"#{key}: #{JSON.generate(value).bytesize}"
end.join(", ")
end

private

def remove_breadcrumbs!
if payload.key?(:breadcrumbs)
payload.delete(:breadcrumbs)
elsif payload.key?("breadcrumbs")
payload.delete("breadcrumbs")
end
end

def reduce_stacktrace!
if exceptions = payload.dig(:exception, :values) || payload.dig("exception", "values")
exceptions.each do |exception|
# in most cases there is only one exception (2 or 3 when have multiple causes), so we won't loop through this double condition much
traces = exception.dig(:stacktrace, :frames) || exception.dig("stacktrace", "frames")

if traces && traces.size > STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD
size_on_both_ends = STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD / 2
traces.replace(
traces[0..(size_on_both_ends - 1)] + traces[-size_on_both_ends..-1],
)
end
end
end
end
end

attr_accessor :headers, :items

def initialize(headers = {})
Expand All @@ -108,3 +23,5 @@ def event_id
end
end
end

require_relative "envelope/item"
88 changes: 88 additions & 0 deletions sentry-ruby/lib/sentry/envelope/item.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# frozen_string_literal: true

module Sentry
# @api private
class Envelope::Item
STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD = 500
MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 1000

SIZE_LIMITS = Hash.new(MAX_SERIALIZED_PAYLOAD_SIZE).update(
"profile" => 1024 * 1000 * 50
)

attr_reader :size_limit, :headers, :payload, :type, :data_category

# rate limits and client reports use the data_category rather than envelope item type
def self.data_category(type)
case type
when "session", "attachment", "transaction", "profile", "span" then type
when "sessions" then "session"
when "check_in" then "monitor"
when "statsd", "metric_meta" then "metric_bucket"
when "event" then "error"
when "client_report" then "internal"
else "default"
end
end

def initialize(headers, payload)
@headers = headers
@payload = payload
@type = headers[:type] || "event"
@data_category = self.class.data_category(type)
@size_limit = SIZE_LIMITS[type]
end

def to_s
[JSON.generate(@headers), @payload.is_a?(String) ? @payload : JSON.generate(@payload)].join("\n")
end

def serialize
result = to_s

if result.bytesize > size_limit
remove_breadcrumbs!
result = to_s
end

if result.bytesize > size_limit
reduce_stacktrace!
result = to_s
end

[result, result.bytesize > size_limit]
end

def size_breakdown
payload.map do |key, value|
"#{key}: #{JSON.generate(value).bytesize}"
end.join(", ")
end

private

def remove_breadcrumbs!
if payload.key?(:breadcrumbs)
payload.delete(:breadcrumbs)
elsif payload.key?("breadcrumbs")
payload.delete("breadcrumbs")
end
end

def reduce_stacktrace!
if exceptions = payload.dig(:exception, :values) || payload.dig("exception", "values")
exceptions.each do |exception|
# in most cases there is only one exception (2 or 3 when have multiple causes), so we won't loop through this double condition much
traces = exception.dig(:stacktrace, :frames) || exception.dig("stacktrace", "frames")

if traces && traces.size > STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD
size_on_both_ends = STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD / 2
traces.replace(
traces[0..(size_on_both_ends - 1)] + traces[-size_on_both_ends..-1],
)
end
end
end
end
end
end
File renamed without changes.
31 changes: 31 additions & 0 deletions sentry-ruby/spec/sentry/transport_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,37 @@
expect(profile_payload).to eq(profile.to_json)
end
end

context "allows bigger item size" do
let(:profile) do
{
environment: "test",
release: "release",
profile: {
frames: Array.new(10000) { |i| { function: "function_#{i}", filename: "file_#{i}", lineno: i } },
stacks: Array.new(10000) { |i| [i] },
samples: Array.new(10000) { |i| { stack_id: i, elapsed_since_start_ns: i * 1000, thread_id: i % 10 } }
}
}
end

let(:event_with_profile) do
event.profile = profile
event
end

let(:envelope) { subject.envelope_from_event(event_with_profile) }

it "adds profile item to envelope" do
result, _ = subject.serialize_envelope(envelope)

_profile_header, profile_payload_json = result.split("\n").last(2)

profile_payload = JSON.parse(profile_payload_json)

expect(profile_payload["profile"]).to_not be(nil)
end
end
end

context "client report" do
Expand Down

0 comments on commit a070e08

Please sign in to comment.