Skip to content

Commit 6affacd

Browse files
committed
feat(data-collection): Port redis, net:http, faraday and excon
integrations
1 parent 612525c commit 6affacd

11 files changed

Lines changed: 343 additions & 44 deletions

File tree

sentry-ruby/lib/sentry/data_collection.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,15 @@ def initialize
153153
end
154154

155155
# Returns whether incoming HTTP request bodies should be collected.
156-
# nil imples all BODY_TYPES according to spec
156+
# nil implies all BODY_TYPES according to spec
157157
def collect_incoming_http_body?
158158
http_bodies.nil? || http_bodies.include?(:incoming_request)
159159
end
160+
161+
# Returns whether outgoing HTTP request bodies should be collected.
162+
# nil implies all BODY_TYPES according to spec
163+
def collect_outgoing_http_body?
164+
http_bodies.nil? || http_bodies.include?(:outgoing_request)
165+
end
160166
end
161167
end

sentry-ruby/lib/sentry/excon/middleware.rb

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,9 @@ def extract_request_info(env)
6161
url = env[:scheme] + "://" + env[:hostname] + env[:path]
6262
result = { method: env[:method].to_s.upcase, url: url }
6363

64-
if Sentry.configuration.send_default_pii
65-
result[:query] = env[:query]
66-
67-
# Handle excon 1.0.0+
68-
result[:query] = build_nested_query(result[:query]) unless result[:query].is_a?(String)
69-
70-
result[:body] = env[:body]
71-
end
64+
query = filter_query_params(env[:query])
65+
result[:query] = query if query
66+
result[:body] = env[:body] if Sentry.configuration.data_collection.collect_outgoing_http_body?
7267

7368
result
7469
end

sentry-ruby/lib/sentry/faraday.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,9 @@ def extract_request_info(env)
5959
url = env[:url].scheme + "://" + env[:url].host + env[:url].path
6060
result = { method: env[:method].to_s.upcase, url: url }
6161

62-
if Sentry.configuration.send_default_pii
63-
result[:query] = env[:url].query
64-
result[:body] = env[:body]
65-
end
62+
query = filter_query_params(env[:url].query)
63+
result[:query] = query if query
64+
result[:body] = env[:body] if Sentry.configuration.data_collection.collect_outgoing_http_body?
6665

6766
result
6867
end

sentry-ruby/lib/sentry/net/http.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,9 @@ def extract_request_info(req)
7272

7373
result = { method: req.method, url: url }
7474

75-
if Sentry.configuration.send_default_pii
76-
result[:query] = uri.query
77-
result[:body] = req.body
78-
end
75+
query = filter_query_params(uri.query)
76+
result[:query] = query if query
77+
result[:body] = req.body if Sentry.configuration.data_collection.collect_outgoing_http_body?
7978

8079
result
8180
end

sentry-ruby/lib/sentry/redis.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def parsed_commands
6262
command_set = { command: command.to_s.upcase }
6363
command_set[:key] = key if Utils::EncodingHelper.valid_utf_8?(key)
6464

65-
if Sentry.configuration.send_default_pii
65+
if Sentry.configuration.data_collection.database_query_data
6666
command_set[:arguments] = arguments
6767
.select { |a| Utils::EncodingHelper.valid_utf_8?(a) }
6868
.join(" ")

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

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,30 @@
11
# frozen_string_literal: true
22

3+
require "uri"
4+
35
module Sentry
46
module Utils
57
module HttpTracing
8+
def filter_query_params(query)
9+
return nil unless query
10+
return nil unless query.is_a?(String) || query.is_a?(Hash)
11+
12+
query_hash = if query.is_a?(String)
13+
URI.decode_www_form(query).each_with_object({}) do |(key, value), params|
14+
params[key] = params.key?(key) ? Array(params[key]) + [value] : value
15+
end
16+
else
17+
query
18+
end
19+
20+
filtered_query_hash = Sentry.configuration.data_collection.url_query_params.filter(query_hash)
21+
return nil if filtered_query_hash.empty?
22+
23+
URI.encode_www_form(filtered_query_hash)
24+
rescue
25+
nil
26+
end
27+
628
def set_span_info(sentry_span, request_info, response_status)
729
sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
830
sentry_span.set_data(Span::DataConventions::URL, request_info[:url])
@@ -43,37 +65,19 @@ def propagate_trace?(url)
4365
Sentry.configuration.trace_propagation_targets.any? { |target| url.match?(target) }
4466
end
4567

46-
# Kindly borrowed from Rack::Utils
47-
def build_nested_query(value, prefix = nil)
48-
case value
49-
when Array
50-
value.map { |v|
51-
build_nested_query(v, "#{prefix}[]")
52-
}.join("&")
53-
when Hash
54-
value.map { |k, v|
55-
build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
56-
}.delete_if(&:empty?).join("&")
57-
when nil
58-
URI.encode_www_form_component(prefix)
59-
else
60-
raise ArgumentError, "value must be a Hash" if prefix.nil?
61-
"#{URI.encode_www_form_component(prefix)}=#{URI.encode_www_form_component(value)}"
62-
end
63-
end
6468

6569
private
6670

6771
def get_level(status)
6872
return :info unless status && status.is_a?(Integer)
6973

70-
if status >= 500
71-
:error
72-
elsif status >= 400
73-
:warning
74-
else
75-
:info
76-
end
74+
if status >= 500
75+
:error
76+
elsif status >= 400
77+
:warning
78+
else
79+
:info
80+
end
7781
end
7882
end
7983
end

sentry-ruby/spec/sentry/excon_spec.rb

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,78 @@
5656
end
5757
end
5858

59+
describe "data collection" do
60+
describe "query parameters" do
61+
def perform_get_request(query: "token=secret&page=5")
62+
transaction = Sentry.start_transaction
63+
Sentry.get_current_scope.set_span(transaction)
64+
Excon.stub({}, { body: "", status: 200 })
65+
Excon.get("http://example.com/path?#{query}", mock: true)
66+
transaction
67+
end
68+
69+
it "does not collect them when the mode is off" do
70+
Sentry.configuration.data_collection.url_query_params.mode = :off
71+
transaction = perform_get_request
72+
73+
expect(transaction.span_recorder.spans.last.data).not_to have_key("http.query")
74+
end
75+
76+
it "filters sensitive values in deny-list mode" do
77+
Sentry.configuration.data_collection.url_query_params.mode = :deny_list
78+
transaction = perform_get_request
79+
80+
expect(transaction.span_recorder.spans.last.data["http.query"]).to eq(
81+
"token=%5BFiltered%5D&page=5"
82+
)
83+
end
84+
85+
it "collects only allowed values in allow-list mode" do
86+
Sentry.configuration.data_collection.url_query_params.mode = :allow_list
87+
Sentry.configuration.data_collection.url_query_params.terms = ["page"]
88+
transaction = perform_get_request(query: "another=value&page=5")
89+
90+
expect(transaction.span_recorder.spans.last.data["http.query"]).to eq(
91+
"another=%5BFiltered%5D&page=5"
92+
)
93+
end
94+
end
95+
96+
describe "request bodies" do
97+
before do
98+
Sentry.configuration.breadcrumbs_logger = [:http_logger]
99+
end
100+
101+
def perform_post_request
102+
transaction = Sentry.start_transaction
103+
Sentry.get_current_scope.set_span(transaction)
104+
Excon.stub({}, { body: "", status: 200 })
105+
Excon.post("http://example.com/path", body: "secret body", mock: true)
106+
end
107+
108+
it "collects them when all body types are enabled" do
109+
Sentry.configuration.data_collection.http_bodies = nil
110+
perform_post_request
111+
112+
expect(Sentry.get_current_scope.breadcrumbs.peek.data[:body]).to eq("secret body")
113+
end
114+
115+
it "collects them when outgoing requests are enabled" do
116+
Sentry.configuration.data_collection.http_bodies = [:outgoing_request]
117+
perform_post_request
118+
119+
expect(Sentry.get_current_scope.breadcrumbs.peek.data[:body]).to eq("secret body")
120+
end
121+
122+
it "does not collect them when body types are disabled" do
123+
Sentry.configuration.data_collection.http_bodies = []
124+
perform_post_request
125+
126+
expect(Sentry.get_current_scope.breadcrumbs.peek.data).not_to have_key(:body)
127+
end
128+
end
129+
end
130+
59131
context "with config.send_default_pii = true" do
60132
before do
61133
Sentry.configuration.send_default_pii = true
@@ -95,7 +167,7 @@
95167
Sentry.get_current_scope.set_span(transaction)
96168

97169
connection = Excon.new("http://example.com/path")
98-
response = connection.get(mock: true, query: build_nested_query({ foo: "bar", baz: [1, 2], qux: { a: 1, b: 2 } }))
170+
response = connection.get(mock: true, query: "foo=bar&baz[]=1&baz[]=2&qux[a]=1&qux[b]=2")
99171

100172
expect(response.status).to eq(200)
101173
expect(transaction.span_recorder.spans.count).to eq(2)

sentry-ruby/spec/sentry/faraday_spec.rb

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,88 @@
5757
end
5858
end
5959

60+
describe "data collection" do
61+
describe "query parameters" do
62+
let(:http) do
63+
Faraday.new("http://example.com") do |f|
64+
f.adapter Faraday::Adapter::Test do |stub|
65+
stub.get("/test") { [200, {}, ""] }
66+
end
67+
end
68+
end
69+
70+
it "does not collect them when the mode is off" do
71+
Sentry.configuration.data_collection.url_query_params.mode = :off
72+
transaction = Sentry.start_transaction
73+
Sentry.get_current_scope.set_span(transaction)
74+
75+
http.get("/test?token=secret&page=5")
76+
77+
expect(transaction.span_recorder.spans.last.data).not_to have_key("http.query")
78+
end
79+
80+
it "filters sensitive values in deny-list mode" do
81+
Sentry.configuration.data_collection.url_query_params.mode = :deny_list
82+
transaction = Sentry.start_transaction
83+
Sentry.get_current_scope.set_span(transaction)
84+
85+
http.get("/test?token=secret&page=5")
86+
87+
expect(transaction.span_recorder.spans.last.data["http.query"]).to eq(
88+
"page=5&token=%5BFiltered%5D"
89+
)
90+
end
91+
92+
it "collects only allowed values in allow-list mode" do
93+
Sentry.configuration.data_collection.url_query_params.mode = :allow_list
94+
Sentry.configuration.data_collection.url_query_params.terms = ["page"]
95+
transaction = Sentry.start_transaction
96+
Sentry.get_current_scope.set_span(transaction)
97+
98+
http.get("/test?another=value&page=5")
99+
100+
expect(transaction.span_recorder.spans.last.data["http.query"]).to eq(
101+
"another=%5BFiltered%5D&page=5"
102+
)
103+
end
104+
end
105+
106+
describe "request bodies" do
107+
let(:http) do
108+
Faraday.new("http://example.com") do |f|
109+
f.adapter Faraday::Adapter::Test do |stub|
110+
stub.post("/test") { [200, {}, ""] }
111+
end
112+
end
113+
end
114+
115+
before do
116+
Sentry.configuration.breadcrumbs_logger = [:http_logger]
117+
end
118+
119+
it "collects them when all body types are enabled" do
120+
Sentry.configuration.data_collection.http_bodies = nil
121+
http.post("/test", "secret body")
122+
123+
expect(Sentry.get_current_scope.breadcrumbs.peek.data[:body]).to eq("secret body")
124+
end
125+
126+
it "collects them when outgoing requests are enabled" do
127+
Sentry.configuration.data_collection.http_bodies = [:outgoing_request]
128+
http.post("/test", "secret body")
129+
130+
expect(Sentry.get_current_scope.breadcrumbs.peek.data[:body]).to eq("secret body")
131+
end
132+
133+
it "does not collect them when body types are disabled" do
134+
Sentry.configuration.data_collection.http_bodies = []
135+
http.post("/test", "secret body")
136+
137+
expect(Sentry.get_current_scope.breadcrumbs.peek.data).not_to have_key(:body)
138+
end
139+
end
140+
end
141+
60142
context "with config.send_default_pii = true" do
61143
let(:http) do
62144
Faraday.new(url) do |f|

0 commit comments

Comments
 (0)