Skip to content

Commit 2377baa

Browse files
committed
perf: optimize RequestInterface — avoid env.dup, cache header names
Reduce allocations in the request interface which runs on every captured exception: - Remove env.dup + IP header deletion loop. Instead, filter IP headers inline in both filter_and_format_headers and filter_and_format_env when send_default_pii is false. This avoids duplicating the entire Rack env hash (typically 40+ entries) on every request. - Cache header name transformations (e.g. HTTP_ACCEPT_LANGUAGE → Accept-Language) at the class level. Header names are deterministic and repeat on every request. - Add ASCII fast-path for header value encoding — ascii_only? strings are already valid UTF-8, skipping the dup+force_encoding dance. - Change Hub#add_breadcrumb hint default from {} to nil. The empty hash was allocated on every breadcrumb call even though most callers don't use hints. Lazily materialized with || {} only when before_breadcrumb callback is configured.
1 parent ebb05d6 commit 2377baa

File tree

2 files changed

+41
-21
lines changed

2 files changed

+41
-21
lines changed

sentry-ruby/lib/sentry/hub.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,12 +283,12 @@ def capture_event(event, **options, &block)
283283
event
284284
end
285285

286-
def add_breadcrumb(breadcrumb, hint: {})
286+
def add_breadcrumb(breadcrumb, hint: nil)
287287
return unless current_client
288288
return unless configuration.enabled_in_current_env?
289289

290290
if before_breadcrumb = current_client.configuration.before_breadcrumb
291-
breadcrumb = before_breadcrumb.call(breadcrumb, hint)
291+
breadcrumb = before_breadcrumb.call(breadcrumb, hint || {})
292292
end
293293

294294
return unless breadcrumb

sentry-ruby/lib/sentry/interfaces/request.rb

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ class RequestInterface < Interface
1111
"HTTP_X_FORWARDED_FOR"
1212
].freeze
1313

14+
# Cache for Rack env key → HTTP header name transformations
15+
# e.g. "HTTP_ACCEPT_LANGUAGE" → "Accept-Language", "CONTENT_TYPE" → "Content-Type"
16+
@header_name_cache = {}
17+
18+
class << self
19+
attr_reader :header_name_cache
20+
end
21+
1422
# See Sentry server default limits at
1523
# https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py
1624
MAX_BODY_LIMIT = 4096 * 4
@@ -42,15 +50,6 @@ class RequestInterface < Interface
4250
# @see Configuration#send_default_pii
4351
# @see Configuration#rack_env_whitelist
4452
def initialize(env:, send_default_pii:, rack_env_whitelist:)
45-
env = env.dup
46-
47-
unless send_default_pii
48-
# need to completely wipe out ip addresses
49-
RequestInterface::IP_HEADERS.each do |header|
50-
env.delete(header)
51-
end
52-
end
53-
5453
request = ::Rack::Request.new(env)
5554

5655
if send_default_pii
@@ -63,7 +62,7 @@ def initialize(env:, send_default_pii:, rack_env_whitelist:)
6362
self.method = request.request_method
6463

6564
self.headers = filter_and_format_headers(env, send_default_pii)
66-
self.env = filter_and_format_env(env, rack_env_whitelist)
65+
self.env = filter_and_format_env(env, rack_env_whitelist, send_default_pii)
6766
end
6867

6968
private
@@ -91,12 +90,22 @@ def filter_and_format_headers(env, send_default_pii)
9190
next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"])
9291
next if is_skippable_header?(key)
9392
next if key == "HTTP_AUTHORIZATION" && !send_default_pii
93+
# Filter IP headers inline instead of env.dup + delete
94+
next if !send_default_pii && IP_HEADERS.include?(key)
9495

9596
# Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
96-
key = key.sub(/^HTTP_/, "")
97-
key = key.split("_").map(&:capitalize).join("-")
98-
99-
memo[key] = Utils::EncodingHelper.encode_to_utf_8(value.to_s)
97+
key = self.class.header_name_cache[key] ||= begin
98+
k = key.delete_prefix("HTTP_")
99+
k.split("_").map(&:capitalize).join("-").freeze
100+
end
101+
102+
# Fast path: ASCII strings are valid UTF-8, skip dup+force_encoding
103+
str = value.to_s
104+
memo[key] = if str.ascii_only?
105+
str
106+
else
107+
Utils::EncodingHelper.encode_to_utf_8(str)
108+
end
100109
rescue StandardError => e
101110
# Rails adds objects to the Rack env that can sometimes raise exceptions
102111
# when `to_s` is called.
@@ -107,8 +116,11 @@ def filter_and_format_headers(env, send_default_pii)
107116
end
108117
end
109118

119+
# Regex to detect lowercase chars — match? is allocation-free (no MatchData/String)
120+
LOWERCASE_PATTERN = /[a-z]/.freeze
121+
110122
def is_skippable_header?(key)
111-
key.upcase != key || # lower-case envs aren't real http headers
123+
key.match?(LOWERCASE_PATTERN) || # lower-case envs aren't real http headers
112124
key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else
113125
!(key.start_with?("HTTP_") || CONTENT_HEADERS.include?(key))
114126
end
@@ -119,17 +131,25 @@ def is_skippable_header?(key)
119131
# if the request has legitimately sent a Version header themselves.
120132
# See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29
121133
def is_server_protocol?(key, value, protocol_version)
122-
rack_version = Gem::Version.new(::Rack.release)
123-
return false if rack_version >= Gem::Version.new("3.0")
134+
return false if self.class.rack_3_or_above?
124135

125136
key == "HTTP_VERSION" && value == protocol_version
126137
end
127138

128-
def filter_and_format_env(env, rack_env_whitelist)
139+
def self.rack_3_or_above?
140+
return @rack_3_or_above if defined?(@rack_3_or_above)
141+
142+
@rack_3_or_above = defined?(::Rack) &&
143+
Gem::Version.new(::Rack.release) >= Gem::Version.new("3.0")
144+
end
145+
146+
def filter_and_format_env(env, rack_env_whitelist, send_default_pii)
129147
return env if rack_env_whitelist.empty?
130148

131149
env.select do |k, _v|
132-
rack_env_whitelist.include? k.to_s
150+
key = k.to_s
151+
next false if !send_default_pii && IP_HEADERS.include?(key)
152+
rack_env_whitelist.include?(key)
133153
end
134154
end
135155
end

0 commit comments

Comments
 (0)