Skip to content

Commit 4fbfb41

Browse files
Merge pull request #1990 from Skumring/fix-sentry-capture-error-after-sentry-ruby-6-upgrade
Restore Sentry event delivery after sentry-ruby 6 upgrade
2 parents f7a243b + 92577db commit 4fbfb41

4 files changed

Lines changed: 143 additions & 10 deletions

File tree

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ group :development, :test do
116116
gem 'bullet'
117117
gem 'byebug'
118118

119-
gem 'brakeman'
119+
gem 'brakeman', require: false
120120
gem 'bundler-audit', require: false
121121
gem 'factory_bot_rails'
122122
gem 'parallel_tests'

config/initializers/sentry.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
# but only run the full configuration when sentry is enabled and not in a Rake task.
55
require 'sentry-ruby'
66

7-
if CaptureError.enabled? && !defined?(::Rake)
7+
if CaptureError.enabled? && !CaptureError.rake_task_invocation?
88
CaptureError.configure!
99
end

lib/capture_error.rb

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,19 @@ def enabled?
1414
YetiConfig.sentry.enabled
1515
end
1616

17+
# Checks whether the current process is executing a Rake task.
18+
#
19+
# `defined?(::Rake)` is unreliable here: `Rake` is an ordinary Ruby constant
20+
# and gets loaded into long-running processes (puma, delayed_job, schedulers)
21+
# transitively via `Bundler.require`. `Rake.application.top_level_tasks` is
22+
# only populated when Rake is actually executing tasks.
23+
# @return [TrueClass,FalseClass]
24+
def rake_task_invocation?
25+
return false unless defined?(::Rake)
26+
27+
::Rake.application.top_level_tasks.any?
28+
end
29+
1730
# Configures Sentry gem.
1831
# Use only once in initializer.
1932
def configure!
@@ -32,14 +45,7 @@ def configure!
3245
sentry_config.inspect_exception_causes_for_exclusion = false
3346

3447
# use Rails' parameter filter to sanitize the event
35-
filters = [
36-
ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters),
37-
ActiveSupport::ParameterFilter.new(['Authorization'])
38-
]
39-
sentry_config.before_send = lambda { |event, _hint = nil|
40-
filters.each { |filter| event = filter.filter(event.to_hash) }
41-
event
42-
}
48+
sentry_config.before_send = build_before_send
4349

4450
# Usage of an async option is discouraged by sentry-ruby developers.
4551
# By default sentry will create its own background workers to send sentry events.
@@ -170,6 +176,40 @@ def with_clean_context(context)
170176

171177
private
172178

179+
# Builds the `before_send` lambda that sanitizes events using Rails'
180+
# configured `filter_parameters` plus an explicit `Authorization` filter.
181+
#
182+
# Sentry-ruby 6 requires `before_send` to return a `Sentry::ErrorEvent`
183+
# (events that come back as `Hash` are dropped by `Sentry::Client#send_event`
184+
# with `Discarded event because before_send didn't return a Sentry::ErrorEvent`).
185+
# We therefore mutate the event in place via its public setters and return
186+
# the event itself.
187+
# @return [Proc]
188+
def build_before_send
189+
filters = [
190+
ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters),
191+
ActiveSupport::ParameterFilter.new(['Authorization'])
192+
]
193+
lambda do |event, _hint = nil|
194+
filters.each { |filter| filter_event!(event, filter) }
195+
event
196+
end
197+
end
198+
199+
def filter_event!(event, filter)
200+
event.user = filter.filter(event.user) if event.user.is_a?(Hash)
201+
event.extra = filter.filter(event.extra) if event.extra.is_a?(Hash)
202+
event.tags = filter.filter(event.tags) if event.tags.is_a?(Hash)
203+
event.contexts = filter.filter(event.contexts) if event.contexts.is_a?(Hash)
204+
205+
return unless event.request
206+
207+
request = event.request
208+
request.data = filter.filter(request.data) if request.data.is_a?(Hash)
209+
request.headers = filter.filter(request.headers) if request.headers.is_a?(Hash)
210+
request.env = filter.filter(request.env) if request.env.is_a?(Hash)
211+
end
212+
173213
def with_capture_context(context, exception: nil)
174214
Sentry.with_scope do |scope|
175215
if scope

spec/lib/capture_error_spec.rb

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,99 @@
11
# frozen_string_literal: true
22

33
RSpec.describe CaptureError do
4+
describe '.rake_task_invocation?' do
5+
subject { described_class.rake_task_invocation? }
6+
7+
context 'when Rake is not loaded' do
8+
before { hide_const('Rake') }
9+
10+
it { is_expected.to be false }
11+
end
12+
13+
context 'when Rake is loaded but no top-level tasks (puma, scheduler, console)' do
14+
before do
15+
allow(Rake.application).to receive(:top_level_tasks).and_return([])
16+
end
17+
18+
it { is_expected.to be false }
19+
end
20+
21+
context 'when Rake is executing a top-level task' do
22+
before do
23+
allow(Rake.application).to receive(:top_level_tasks).and_return(['db:migrate'])
24+
end
25+
26+
it { is_expected.to be true }
27+
end
28+
end
29+
30+
describe '#build_before_send (private)' do
31+
subject(:before_send) { described_class.send(:build_before_send) }
32+
33+
let(:configuration) do
34+
config = Sentry::Configuration.new
35+
config.dsn = 'http://public@example.invalid/1'
36+
config
37+
end
38+
let(:event) { Sentry::ErrorEvent.new(configuration: configuration) }
39+
40+
before do
41+
allow(Rails.application.config).to receive(:filter_parameters).and_return([:password])
42+
end
43+
44+
it 'should return the same Sentry::ErrorEvent object (not a Hash)' do
45+
result = before_send.call(event)
46+
expect(result).to be(event)
47+
expect(result).to be_a(Sentry::ErrorEvent)
48+
end
49+
50+
it 'should mask filter_parameters keys in event.extra and keep other keys' do
51+
event.extra = { password: 'secret', visible: 'ok' }
52+
result = before_send.call(event)
53+
expect(result.extra).to eq(password: '[FILTERED]', visible: 'ok')
54+
end
55+
56+
it 'should mask Authorization in event.tags' do
57+
event.tags = { 'Authorization' => 'Bearer abc', 'probe' => 'on' }
58+
result = before_send.call(event)
59+
expect(result.tags).to eq('Authorization' => '[FILTERED]', 'probe' => 'on')
60+
end
61+
62+
it 'should mask filter_parameters keys in event.user and event.contexts' do
63+
event.user = { id: 1, password: 'pw' }
64+
event.contexts = { extra_ctx: { password: 'pw2', note: 'visible' } }
65+
result = before_send.call(event)
66+
expect(result.user).to eq(id: 1, password: '[FILTERED]')
67+
expect(result.contexts).to eq(extra_ctx: { password: '[FILTERED]', note: 'visible' })
68+
end
69+
70+
it 'should mask filter_parameters keys and Authorization in event.request fields' do
71+
request = Sentry::RequestInterface.allocate
72+
request.data = { password: 'pw', visible: 'ok' }
73+
request.headers = { 'Authorization' => 'Bearer xyz', 'X-Trace' => 't1' }
74+
request.env = { 'HTTP_AUTHORIZATION' => 'Bearer xyz', 'REMOTE_ADDR' => '127.0.0.1' }
75+
event.instance_variable_set(:@request, request)
76+
77+
result = before_send.call(event)
78+
expect(result.request.data).to eq(password: '[FILTERED]', visible: 'ok')
79+
expect(result.request.headers).to eq('Authorization' => '[FILTERED]', 'X-Trace' => 't1')
80+
expect(result.request.env).to eq('HTTP_AUTHORIZATION' => '[FILTERED]', 'REMOTE_ADDR' => '127.0.0.1')
81+
end
82+
83+
it 'should not raise when event.request is nil' do
84+
event.extra = { password: 'pw' }
85+
expect { before_send.call(event) }.not_to raise_error
86+
end
87+
88+
it 'should not raise when event fields are nil' do
89+
event.extra = nil
90+
event.tags = nil
91+
event.user = nil
92+
event.contexts = nil
93+
expect { before_send.call(event) }.not_to raise_error
94+
end
95+
end
96+
497
describe '.with_clean_context' do
598
subject do
699
described_class.with_clean_context(context) { block_result }

0 commit comments

Comments
 (0)