Skip to content

Commit a5b79c8

Browse files
committed
feat(data-collection): Port redis, net:http, faraday and excon
integrations
1 parent de296be commit a5b79c8

11 files changed

Lines changed: 397 additions & 51 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: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,39 @@
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+
format_query(filtered_query_hash)
24+
rescue
25+
nil
26+
end
27+
28+
def format_query(query)
29+
query.flat_map do |key, value|
30+
Array(value).map { |item| "#{key}=#{item}" }
31+
end.join("&")
32+
end
33+
634
def set_span_info(sentry_span, request_info, response_status)
735
sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
8-
sentry_span.set_data(Span::DataConventions::URL, request_info[:url])
36+
sentry_span.set_data(Span::DataConventions::URL, url_with_query(request_info))
937
sentry_span.set_data(Span::DataConventions::HTTP_METHOD, request_info[:method])
1038
sentry_span.set_data(Span::DataConventions::HTTP_QUERY, request_info[:query]) if request_info[:query]
1139
sentry_span.set_data(Span::DataConventions::HTTP_STATUS_CODE, response_status)
@@ -43,37 +71,25 @@ def propagate_trace?(url)
4371
Sentry.configuration.trace_propagation_targets.any? { |target| url.match?(target) }
4472
end
4573

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
6474

6575
private
6676

77+
def url_with_query(request_info)
78+
return request_info[:url] unless request_info[:query]
79+
80+
"#{request_info[:url]}?#{request_info[:query]}"
81+
end
82+
6783
def get_level(status)
6884
return :info unless status && status.is_a?(Integer)
6985

70-
if status >= 500
71-
:error
72-
elsif status >= 400
73-
:warning
74-
else
75-
:info
76-
end
86+
if status >= 500
87+
:error
88+
elsif status >= 400
89+
:warning
90+
else
91+
:info
92+
end
7793
end
7894
end
7995
end

sentry-ruby/spec/sentry/excon_spec.rb

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,85 @@
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+
expect(transaction.span_recorder.spans.last.data["url"]).to eq("http://example.com/path")
75+
end
76+
77+
it "filters sensitive values in deny-list mode" do
78+
Sentry.configuration.data_collection.url_query_params.mode = :deny_list
79+
transaction = perform_get_request
80+
81+
expect(transaction.span_recorder.spans.last.data["http.query"]).to eq(
82+
"token=[Filtered]&page=5"
83+
)
84+
expect(transaction.span_recorder.spans.last.data["url"]).to eq(
85+
"http://example.com/path?token=[Filtered]&page=5"
86+
)
87+
end
88+
89+
it "collects only allowed values in allow-list mode" do
90+
Sentry.configuration.data_collection.url_query_params.mode = :allow_list
91+
Sentry.configuration.data_collection.url_query_params.terms = ["page"]
92+
transaction = perform_get_request(query: "another=value&page=5")
93+
94+
expect(transaction.span_recorder.spans.last.data["http.query"]).to eq(
95+
"another=[Filtered]&page=5"
96+
)
97+
expect(transaction.span_recorder.spans.last.data["url"]).to eq(
98+
"http://example.com/path?another=[Filtered]&page=5"
99+
)
100+
end
101+
end
102+
103+
describe "request bodies" do
104+
before do
105+
Sentry.configuration.breadcrumbs_logger = [:http_logger]
106+
end
107+
108+
def perform_post_request
109+
transaction = Sentry.start_transaction
110+
Sentry.get_current_scope.set_span(transaction)
111+
Excon.stub({}, { body: "", status: 200 })
112+
Excon.post("http://example.com/path", body: "secret body", mock: true)
113+
end
114+
115+
it "collects them when all body types are enabled" do
116+
Sentry.configuration.data_collection.http_bodies = nil
117+
perform_post_request
118+
119+
expect(Sentry.get_current_scope.breadcrumbs.peek.data[:body]).to eq("secret body")
120+
end
121+
122+
it "collects them when outgoing requests are enabled" do
123+
Sentry.configuration.data_collection.http_bodies = [:outgoing_request]
124+
perform_post_request
125+
126+
expect(Sentry.get_current_scope.breadcrumbs.peek.data[:body]).to eq("secret body")
127+
end
128+
129+
it "does not collect them when body types are disabled" do
130+
Sentry.configuration.data_collection.http_bodies = []
131+
perform_post_request
132+
133+
expect(Sentry.get_current_scope.breadcrumbs.peek.data).not_to have_key(:body)
134+
end
135+
end
136+
end
137+
59138
context "with config.send_default_pii = true" do
60139
before do
61140
Sentry.configuration.send_default_pii = true
@@ -82,7 +161,7 @@
82161
expect(request_span.description).to eq("GET http://example.com/path")
83162
expect(request_span.data).to eq({
84163
"http.response.status_code" => 200,
85-
"url" => "http://example.com/path",
164+
"url" => "http://example.com/path?foo=bar",
86165
"http.request.method" => "GET",
87166
"http.query" => "foo=bar"
88167
})
@@ -95,7 +174,7 @@
95174
Sentry.get_current_scope.set_span(transaction)
96175

97176
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 } }))
177+
response = connection.get(mock: true, query: "foo=bar&baz[]=1&baz[]=2&qux[a]=1&qux[b]=2")
99178

100179
expect(response.status).to eq(200)
101180
expect(transaction.span_recorder.spans.count).to eq(2)
@@ -109,9 +188,9 @@
109188
expect(request_span.description).to eq("GET http://example.com/path")
110189
expect(request_span.data).to eq({
111190
"http.response.status_code" => 200,
112-
"url" => "http://example.com/path",
191+
"url" => "http://example.com/path?foo=bar&baz[]=1&baz[]=2&qux[a]=1&qux[b]=2",
113192
"http.request.method" => "GET",
114-
"http.query" => "foo=bar&baz%5B%5D=1&baz%5B%5D=2&qux%5Ba%5D=1&qux%5Bb%5D=2"
193+
"http.query" => "foo=bar&baz[]=1&baz[]=2&qux[a]=1&qux[b]=2"
115194
})
116195
end
117196

sentry-ruby/spec/sentry/faraday_spec.rb

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,95 @@
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+
expect(transaction.span_recorder.spans.last.data["url"]).to eq("http://example.com/test")
79+
end
80+
81+
it "filters sensitive values in deny-list mode" do
82+
Sentry.configuration.data_collection.url_query_params.mode = :deny_list
83+
transaction = Sentry.start_transaction
84+
Sentry.get_current_scope.set_span(transaction)
85+
86+
http.get("/test?token=secret&page=5")
87+
88+
expect(transaction.span_recorder.spans.last.data["http.query"]).to eq(
89+
"page=5&token=[Filtered]"
90+
)
91+
expect(transaction.span_recorder.spans.last.data["url"]).to eq(
92+
"http://example.com/test?page=5&token=[Filtered]"
93+
)
94+
end
95+
96+
it "collects only allowed values in allow-list mode" do
97+
Sentry.configuration.data_collection.url_query_params.mode = :allow_list
98+
Sentry.configuration.data_collection.url_query_params.terms = ["page"]
99+
transaction = Sentry.start_transaction
100+
Sentry.get_current_scope.set_span(transaction)
101+
102+
http.get("/test?another=value&page=5")
103+
104+
expect(transaction.span_recorder.spans.last.data["http.query"]).to eq(
105+
"another=[Filtered]&page=5"
106+
)
107+
expect(transaction.span_recorder.spans.last.data["url"]).to eq(
108+
"http://example.com/test?another=[Filtered]&page=5"
109+
)
110+
end
111+
end
112+
113+
describe "request bodies" do
114+
let(:http) do
115+
Faraday.new("http://example.com") do |f|
116+
f.adapter Faraday::Adapter::Test do |stub|
117+
stub.post("/test") { [200, {}, ""] }
118+
end
119+
end
120+
end
121+
122+
before do
123+
Sentry.configuration.breadcrumbs_logger = [:http_logger]
124+
end
125+
126+
it "collects them when all body types are enabled" do
127+
Sentry.configuration.data_collection.http_bodies = nil
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 "collects them when outgoing requests are enabled" do
134+
Sentry.configuration.data_collection.http_bodies = [:outgoing_request]
135+
http.post("/test", "secret body")
136+
137+
expect(Sentry.get_current_scope.breadcrumbs.peek.data[:body]).to eq("secret body")
138+
end
139+
140+
it "does not collect them when body types are disabled" do
141+
Sentry.configuration.data_collection.http_bodies = []
142+
http.post("/test", "secret body")
143+
144+
expect(Sentry.get_current_scope.breadcrumbs.peek.data).not_to have_key(:body)
145+
end
146+
end
147+
end
148+
60149
context "with config.send_default_pii = true" do
61150
let(:http) do
62151
Faraday.new(url) do |f|
@@ -103,7 +192,7 @@
103192

104193
expect(request_span.data).to eq({
105194
"http.response.status_code" => 200,
106-
"url" => "http://example.com/test",
195+
"url" => "http://example.com/test?foo=bar",
107196
"http.request.method" => "GET",
108197
"http.query" => "foo=bar"
109198
})
@@ -157,7 +246,7 @@
157246

158247
expect(request_span.data).to eq({
159248
"http.response.status_code" => 200,
160-
"url" => "http://example.com/test",
249+
"url" => "http://example.com/test?foo=bar",
161250
"http.request.method" => "POST",
162251
"http.query" => "foo=bar"
163252
})

0 commit comments

Comments
 (0)