Skip to content

Commit b48a5b4

Browse files
committed
feat(data-collection): Port Rails controller and ActiveStorage
* 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 0b2ef30 commit b48a5b4

9 files changed

Lines changed: 179 additions & 20 deletions

File tree

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

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

3+
require "sentry/utils/http_tracing"
4+
35
module Sentry
46
module Rails
57
module ControllerTransaction
68
SPAN_ORIGIN = "auto.view.rails"
79

10+
include Sentry::Utils::HttpTracing
11+
812
def self.included(base)
913
base.prepend_around_action(:sentry_around_action)
1014
end
@@ -24,9 +28,16 @@ def sentry_around_action
2428
child_span.set_data(:format, request.format)
2529
child_span.set_data(:method, request.method)
2630

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)
31+
data_collection = Sentry.configuration.data_collection
32+
query = data_collection.url_query_params.filter(request.query_parameters)
33+
path = request.path
34+
path = "#{path}?#{format_query(query)}" unless query.empty?
35+
36+
child_span.set_data(:path, path)
37+
child_span.set_data(
38+
:params,
39+
data_collection.url_query_params.filter(request.params)
40+
)
3041
end
3142

3243
result

sentry-rails/lib/sentry/rails/railtie.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class Railtie < ::Rails::Railtie
4848
next unless Sentry.initialized?
4949

5050
configure_project_root
51+
configure_data_collection
5152
configure_trusted_proxies
5253
configure_cron_timezone
5354
extend_controller_methods if defined?(ActionController)
@@ -82,6 +83,14 @@ def configure_project_root
8283
Sentry.configuration.project_root = ::Rails.root.to_s
8384
end
8485

86+
def configure_data_collection
87+
collection = Sentry.configuration.data_collection.url_query_params
88+
return unless collection.mode == :deny_list
89+
90+
filter_parameters = ::Rails.application.config.filter_parameters
91+
collection.terms = Array(collection.terms) + Array(filter_parameters)
92+
end
93+
8594
def configure_trusted_proxies
8695
Sentry.configuration.trusted_proxies += Array(::Rails.application.config.action_dispatch.trusted_proxies)
8796
end

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def configure
109109
:custom_secret,
110110
:api_key,
111111
:credit_card,
112+
/billing_reference/,
112113
:authorization,
113114
:token
114115
]

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: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,31 @@
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+
end
113+
end
114+
115+
it "adds Rails filter parameters to URL query parameter data collection" do
116+
expect(Sentry.configuration.data_collection.url_query_params.terms).to include(
117+
"password",
118+
"custom_secret"
119+
)
120+
end
121+
122+
it "preserves and applies Regexp Rails filter parameters" do
123+
expect(Sentry.configuration.data_collection.url_query_params.terms).to include(/billing_reference/)
124+
expect(Sentry.configuration.data_collection.url_query_params.filter(
125+
{ "billing_reference_id" => "secret", "public" => "visible" }
126+
)).to eq(
127+
"billing_reference_id" => "[Filtered]",
128+
"public" => "visible"
129+
)
130+
end
131+
end
132+
108133
context "at exit" do
109134
before do
110135
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/spec/sentry/data_collection/key_value_collection_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@
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", "public_data" => "visible" }
60+
)).to eq(
61+
"private_data" => "[Filtered]",
62+
"public_data" => "visible"
63+
)
64+
end
65+
66+
it "preserves regular expression terms when assigning terms" do
67+
regexp = /private/i
68+
collection.terms = [regexp]
69+
70+
expect(collection.terms).to eq([regexp])
71+
end
72+
5773
it "normalizes terms assigned after initialization" do
5874
collection.terms = ["USER"]
5975

0 commit comments

Comments
 (0)