Skip to content

Commit 5a2f1ed

Browse files
authored
feat(data-collection): Port Rails controller and ActiveStorage (#3033)
* Rails controller span data query inclusion is modified to comply with URL collection spec. * Rails `filter_parameters` are backfilled into `url_query_params` * Allow regexes in `terms` as a result to enable this
1 parent 78372de commit 5a2f1ed

11 files changed

Lines changed: 189 additions & 24 deletions

File tree

sentry-rails/lib/sentry/rails/configuration.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ class Configuration
3636

3737
after(:configured) do
3838
rails.structured_logging.enabled = enable_logs if rails.structured_logging.enabled.nil?
39+
40+
collection = data_collection.url_query_params
41+
if collection.mode == :deny_list
42+
filter_parameters = ::Rails.application.config.filter_parameters.select do |filter|
43+
filter.is_a?(String) || filter.is_a?(Symbol) || filter.is_a?(Regexp)
44+
end
45+
collection.terms = (Array(collection.terms) + filter_parameters).uniq
46+
end
3947
end
4048
end
4149

sentry-rails/lib/sentry/rails/controller_transaction.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require "sentry/utils/http_tracing"
4+
35
module Sentry
46
module Rails
57
module ControllerTransaction
@@ -24,9 +26,16 @@ def sentry_around_action
2426
child_span.set_data(:format, request.format)
2527
child_span.set_data(:method, request.method)
2628

27-
pii = Sentry.configuration.send_default_pii
28-
child_span.set_data(:path, pii ? request.fullpath : request.filtered_path)
29-
child_span.set_data(:params, pii ? request.params : request.filtered_parameters)
29+
data_collection = Sentry.configuration.data_collection
30+
query = data_collection.url_query_params.filter(request.query_parameters)
31+
path = request.path
32+
path = "#{path}?#{Sentry::Utils::HttpTracing.format_query(query)}" unless query.empty?
33+
34+
child_span.set_data(:path, path)
35+
child_span.set_data(
36+
:params,
37+
data_collection.url_query_params.filter(request.params)
38+
)
3039
end
3140

3241
result

sentry-rails/lib/sentry/rails/tracing/active_storage_subscriber.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ def self.subscribe!
3434
) do |span|
3535
payload.each do |key, value|
3636
next if key == START_TIMESTAMP_NAME
37-
next if key == :key && !Sentry.configuration.send_default_pii
37+
# Active Storage keys are automatically generated storage data,
38+
# rather than URL/query data. Use the database query-data switch
39+
# for the legacy PII-compatible on/off behavior.
40+
next if key == :key && !Sentry.configuration.data_collection.database_query_data
3841

3942
span.set_data(key, value)
4043
end

sentry-rails/spec/dummy/test_rails_app/config/application.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,10 @@ def configure
109109
:custom_secret,
110110
:api_key,
111111
:credit_card,
112+
/billing_reference/,
112113
:authorization,
113-
:token
114+
:token,
115+
proc { |_key, _value| }
114116
]
115117

116118
# Eager load namespaces can be accumulated after repeated initializations and make initialization

sentry-rails/spec/sentry/rails/tracing/active_storage_subscriber_spec.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,54 @@
7979
end
8080
end
8181

82+
context "data collection" do
83+
context "with database query data enabled" do
84+
before do
85+
make_basic_app do |config|
86+
config.traces_sample_rate = 1.0
87+
config.rails.tracing_subscribers = [described_class]
88+
config.data_collection.database_query_data = true
89+
end
90+
end
91+
92+
it "records the :key in span.data" do
93+
ActiveStorage::AnalyzeJob.queue_adapter.perform_enqueued_jobs = true
94+
95+
p = Post.create!
96+
get "/posts/#{p.id}/attach"
97+
98+
request_transaction = transport.events.last.to_h
99+
upload_span = request_transaction[:spans].find { |s| s[:op] == "file.service_upload.active_storage" }
100+
101+
expect(upload_span).not_to be_nil
102+
expect(upload_span.dig(:data, :key)).to eq(p.cover.key)
103+
end
104+
end
105+
106+
context "with database query data disabled" do
107+
before do
108+
make_basic_app do |config|
109+
config.traces_sample_rate = 1.0
110+
config.rails.tracing_subscribers = [described_class]
111+
config.data_collection.database_query_data = false
112+
end
113+
end
114+
115+
it "does not record the :key in span.data" do
116+
ActiveStorage::AnalyzeJob.queue_adapter.perform_enqueued_jobs = true
117+
118+
p = Post.create!
119+
get "/posts/#{p.id}/attach"
120+
121+
request_transaction = transport.events.last.to_h
122+
upload_span = request_transaction[:spans].find { |s| s[:op] == "file.service_upload.active_storage" }
123+
124+
expect(upload_span).not_to be_nil
125+
expect(upload_span.dig(:data, :key)).to be_nil
126+
end
127+
end
128+
end
129+
82130
context "when transaction is not sampled" do
83131
before do
84132
make_basic_app

sentry-rails/spec/sentry/rails/tracing_spec.rb

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -201,17 +201,15 @@
201201
end
202202
end
203203

204-
it "does not record sensitive params" do
204+
it "does not record any params" do
205205
get "/posts?foo=bar&password=42&secret=baz"
206206
transaction = transport.events.last.to_h
207207

208208
params = transaction[:spans][0][:data][:params]
209-
expect(params["foo"]).to eq("bar")
210-
expect(params["password"]).to eq("[FILTERED]")
211-
expect(params["secret"]).to eq("[FILTERED]")
209+
expect(params).to be_empty
212210

213211
path = transaction[:spans][0][:data][:path]
214-
expect(path).to eq("/posts?foo=bar&password=[FILTERED]&secret=[FILTERED]")
212+
expect(path).to eq("/posts")
215213
end
216214
end
217215

@@ -223,17 +221,61 @@
223221
end
224222
end
225223

226-
it "records all params" do
224+
it "records all params except sensitive params" do
227225
get "/posts?foo=bar&password=42&secret=baz"
228226
transaction = transport.events.last.to_h
229227

230228
params = transaction[:spans][0][:data][:params]
231229
expect(params["foo"]).to eq("bar")
232-
expect(params["password"]).to eq("42")
233-
expect(params["secret"]).to eq("baz")
230+
expect(params["password"]).to eq("[Filtered]")
231+
expect(params["secret"]).to eq("[Filtered]")
232+
233+
path = transaction[:spans][0][:data][:path]
234+
expect(path).to eq("/posts?foo=bar&password=[Filtered]&secret=[Filtered]")
235+
end
236+
end
237+
end
238+
239+
context "data collection" do
240+
context "with url query params enabled" do
241+
before do
242+
make_basic_app do |config|
243+
config.traces_sample_rate = 1.0
244+
config.data_collection.url_query_params.mode = :deny_list
245+
end
246+
end
247+
248+
it "records query params except sensitive params" do
249+
get "/posts?foo=bar&password=42&secret=baz"
250+
transaction = transport.events.last.to_h
251+
252+
params = transaction[:spans][0][:data][:params]
253+
expect(params["foo"]).to eq("bar")
254+
expect(params["password"]).to eq("[Filtered]")
255+
expect(params["secret"]).to eq("[Filtered]")
256+
257+
path = transaction[:spans][0][:data][:path]
258+
expect(path).to eq("/posts?foo=bar&password=[Filtered]&secret=[Filtered]")
259+
end
260+
end
261+
262+
context "with url query params disabled" do
263+
before do
264+
make_basic_app do |config|
265+
config.traces_sample_rate = 1.0
266+
config.data_collection.url_query_params.mode = :off
267+
end
268+
end
269+
270+
it "does not record query params" do
271+
get "/posts?foo=bar&password=42&secret=baz"
272+
transaction = transport.events.last.to_h
273+
274+
params = transaction[:spans][0][:data][:params]
275+
expect(params).to be_empty
234276

235277
path = transaction[:spans][0][:data][:path]
236-
expect(path).to eq("/posts?foo=bar&password=42&secret=baz")
278+
expect(path).to eq("/posts")
237279
end
238280
end
239281
end

sentry-rails/spec/sentry/rails_spec.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,38 @@
105105
end
106106
end
107107

108+
context "data collection" do
109+
before do
110+
make_basic_app do |config|
111+
config.data_collection.url_query_params.mode = :deny_list
112+
Rails.application.config.filter_parameters << proc { |_key, _value| }
113+
end
114+
end
115+
116+
it "adds Rails filter parameters to URL query parameter data collection" do
117+
expect(Sentry.configuration.data_collection.url_query_params.terms).to include(
118+
"password",
119+
"custom_secret"
120+
)
121+
end
122+
123+
it "preserves and applies Regexp Rails filter parameters" do
124+
expect(Sentry.configuration.data_collection.url_query_params.terms).to include(/billing_reference/)
125+
expect(Sentry.configuration.data_collection.url_query_params.filter(
126+
{ "billing_reference_id" => "secret", "public" => "visible" }
127+
)).to eq(
128+
"billing_reference_id" => "[Filtered]",
129+
"public" => "visible"
130+
)
131+
end
132+
133+
it "only adds String, Symbol, and Regexp Rails filter parameters" do
134+
terms = Sentry.configuration.data_collection.url_query_params.terms
135+
136+
expect(terms).to all(satisfy { |term| term.is_a?(String) || term.is_a?(Symbol) || term.is_a?(Regexp) })
137+
end
138+
end
139+
108140
context "at exit" do
109141
before do
110142
make_basic_app

sentry-ruby/lib/sentry/data_collection/key_value_collection.rb

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class KeyValueCollection
6161
attr_accessor :mode
6262

6363
# `terms` contains the keys or patterns used by the selected mode.
64-
# @return [Array<String>, nil]
64+
# @return [Array<String, Regexp>, nil]
6565
attr_reader :terms
6666

6767
def initialize(mode:, terms:)
@@ -70,7 +70,8 @@ def initialize(mode:, terms:)
7070
end
7171

7272
def terms=(terms)
73-
@terms = terms&.map { |term| term.to_s.downcase }&.reject { |term| term.strip.empty? }
73+
@terms = terms&.map { |term| term.is_a?(Regexp) ? term : term.to_s.downcase }
74+
&.reject { |term| !term.is_a?(Regexp) && term.strip.empty? }
7475
end
7576

7677
# Applies this collection configuration without changing the input hash.
@@ -90,14 +91,15 @@ def filter(values, cookie: false)
9091
private
9192

9293
def safe_value?(key, cookie: false)
93-
key_downcase = key.to_s.downcase
94+
key_string = key.to_s
95+
key_downcase = key_string.downcase
9496
return false if sensitive?(key_downcase, cookie: cookie)
9597

9698
case mode
9799
when :deny_list
98-
!matches_any_term?(key_downcase)
100+
!matches_any_term?(key_string, key_downcase)
99101
when :allow_list
100-
matches_any_term?(key_downcase)
102+
matches_any_term?(key_string, key_downcase)
101103
else
102104
false
103105
end
@@ -108,8 +110,10 @@ def sensitive?(key, cookie: false)
108110
(cookie && SENSITIVE_COOKIE_NAME_DENY_LIST.any? { |term| key.include?(term) })
109111
end
110112

111-
def matches_any_term?(key)
112-
@terms&.any? { |term| key.include?(term) }
113+
def matches_any_term?(key, key_downcase)
114+
@terms&.any? do |term|
115+
term.is_a?(Regexp) ? term.match?(key) : key_downcase.include?(term)
116+
end
113117
end
114118
end
115119
end

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ def filter_query_params(query)
2020
filtered_query_hash = Sentry.configuration.data_collection.url_query_params.filter(query_hash)
2121
return nil if filtered_query_hash.empty?
2222

23-
format_query(filtered_query_hash)
23+
HttpTracing.format_query(filtered_query_hash)
2424
rescue
2525
nil
2626
end
2727

28-
def format_query(query)
28+
def self.format_query(query)
2929
query.flat_map do |key, value|
3030
Array(value).map { |item| "#{key}=#{item}" }
3131
end.join("&")

sentry-ruby/spec/sentry/data_collection/key_value_collection_spec.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,23 @@
5454
)
5555
end
5656

57+
it "matches regular expression terms" do
58+
expect(described_class.new(mode: :deny_list, terms: [/private[-_]data/]).filter(
59+
{ "private_data" => "secret", "PRIVATE_DATA" => "visible", "public_data" => "visible" }
60+
)).to eq(
61+
"private_data" => "[Filtered]",
62+
"PRIVATE_DATA" => "visible",
63+
"public_data" => "visible"
64+
)
65+
end
66+
67+
it "preserves regular expression terms when assigning terms" do
68+
regexp = /private/i
69+
collection.terms = [regexp]
70+
71+
expect(collection.terms).to eq([regexp])
72+
end
73+
5774
it "normalizes terms assigned after initialization" do
5875
collection.terms = ["USER"]
5976

0 commit comments

Comments
 (0)