Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion sentry-rails/lib/sentry/rails/error_reporter_context.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
# frozen_string_literal: true

require "sentry/rails/serializer"
require "sentry/rails/parameter_filter"

module Sentry
module Rails
module ErrorReporterContext
SUPPORTS_EXECUTION_CONTEXT = Gem::Version.new(::Rails.version) >= Gem::Version.new("7.0.0")

# Prepare a context hash for the "rails.error" context entry.
#
# Every code path that emits that entry must go through here, so that user data attached
# via `Rails.error.set_context` gets the same treatment regardless of whether the
# exception was handled.
#
# Serialization runs first: the serializer expands Enumerables and Ranges into arrays the
# filter can descend into, and it preserves hash keys, so filtering its output is strictly
# more thorough than filtering the raw context.
#
# @param context [Hash] the raw context
# @return [Hash] the serialized context with sensitive values redacted
def self.rails_error_context(context)
Sentry::Rails::ParameterFilter.filter_sensitive_params(Sentry::Rails::Serializer.serialize(context))
end

if SUPPORTS_EXECUTION_CONTEXT
def execution_context
context = ::ActiveSupport::ExecutionContext.to_h
return {} if context.empty?

{ "rails.error" => Sentry::Rails::Serializer.serialize(context) }
{ "rails.error" => ErrorReporterContext.rails_error_context(context) }
end
else
def execution_context
Expand Down
4 changes: 3 additions & 1 deletion sentry-rails/lib/sentry/rails/error_subscriber.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "sentry/rails/error_reporter_context"

module Sentry
module Rails
# This is not a user-facing class. You should use it with Rails 7.0's error reporter feature and its interfaces.
Expand Down Expand Up @@ -29,7 +31,7 @@ def report(error, handled:, severity:, context:, source: nil)

hint[:mechanism] ||= Sentry::Mechanism.new(type: Sentry::Rails.integration_name, handled: handled)

Sentry::Rails.capture_exception(error, level: severity, contexts: { "rails.error" => context }, tags: tags, hint: hint)
Sentry::Rails.capture_exception(error, level: severity, contexts: { "rails.error" => ErrorReporterContext.rails_error_context(context) }, tags: tags, hint: hint)
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

require "sentry/rails/log_subscriber"
require "sentry/rails/log_subscribers/parameter_filter"
require "sentry/rails/parameter_filter"

module Sentry
module Rails
Expand All @@ -21,7 +21,7 @@ module LogSubscribers
# config.rails.structured_logging.subscribers = { action_controller: Sentry::Rails::LogSubscribers::ActionControllerSubscriber }
# end
class ActionControllerSubscriber < Sentry::Rails::LogSubscriber
include ParameterFilter
include Sentry::Rails::ParameterFilter

# Handle process_action.action_controller events
#
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

require "sentry/rails/log_subscriber"
require "sentry/rails/log_subscribers/parameter_filter"
require "sentry/rails/parameter_filter"

module Sentry
module Rails
Expand All @@ -20,7 +20,7 @@ module LogSubscribers
# config.rails.structured_logging.subscribers = { action_mailer: Sentry::Rails::LogSubscribers::ActionMailerSubscriber }
# end
class ActionMailerSubscriber < Sentry::Rails::LogSubscriber
include ParameterFilter
include Sentry::Rails::ParameterFilter

# Handle deliver.action_mailer events
#
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

require "sentry/rails/log_subscriber"
require "sentry/rails/log_subscribers/parameter_filter"
require "sentry/rails/parameter_filter"

module Sentry
module Rails
Expand All @@ -20,7 +20,7 @@ module LogSubscribers
# config.rails.structured_logging.subscribers = { active_job: Sentry::Rails::LogSubscribers::ActiveJobSubscriber }
# end
class ActiveJobSubscriber < Sentry::Rails::LogSubscriber
include ParameterFilter
include Sentry::Rails::ParameterFilter

# Handle perform.active_job events
#
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

require "sentry/rails/log_subscriber"
require "sentry/rails/log_subscribers/parameter_filter"
require "sentry/rails/parameter_filter"

module Sentry
module Rails
Expand All @@ -21,7 +21,7 @@ module LogSubscribers
# config.rails.structured_logging.subscribers = { active_record: Sentry::Rails::LogSubscribers::ActiveRecordSubscriber }
# end
class ActiveRecordSubscriber < Sentry::Rails::LogSubscriber
include ParameterFilter
include Sentry::Rails::ParameterFilter

EXCLUDED_NAMES = ["SCHEMA", "TRANSACTION"].freeze
EMPTY_ARRAY = [].freeze
Expand Down
51 changes: 7 additions & 44 deletions sentry-rails/lib/sentry/rails/log_subscribers/parameter_filter.rb
Original file line number Diff line number Diff line change
@@ -1,52 +1,15 @@
# frozen_string_literal: true

require "sentry/rails/parameter_filter"

module Sentry
module Rails
module LogSubscribers
# Shared utility module for filtering sensitive parameters in log subscribers.
#
# This module provides consistent parameter filtering across all Sentry Rails
# log subscribers, leveraging Rails' built-in parameter filtering when available.
# It automatically detects the correct Rails parameter filtering API based on
# the Rails version and includes the appropriate implementation module.
#
# @example Usage in a log subscriber
# class MySubscriber < Sentry::Rails::LogSubscriber
# include Sentry::Rails::LogSubscribers::ParameterFilter
#
# def my_event(event)
# if Sentry.configuration.send_default_pii && event.payload[:params]
# filtered_params = filter_sensitive_params(event.payload[:params])
# attributes[:params] = filtered_params unless filtered_params.empty?
# end
# end
# end
module ParameterFilter
EMPTY_HASH = {}.freeze

if ::Rails.version.to_f >= 6.0
def self.backend
ActiveSupport::ParameterFilter
end
else
def self.backend
ActionDispatch::Http::ParameterFilter
end
end

# Filter sensitive parameters from a hash, respecting Rails configuration.
#
# @param params [Hash] The parameters to filter
# @return [Hash] Filtered parameters with sensitive data removed
def filter_sensitive_params(params)
return EMPTY_HASH unless params.is_a?(Hash)

filter_parameters = ::Rails.application.config.filter_parameters
parameter_filter = ParameterFilter.backend.new(filter_parameters)

parameter_filter.filter(params)
end
end
# @deprecated Use {Sentry::Rails::ParameterFilter} instead. This alias keeps custom log
# subscribers that include the old constant working, and will be removed in the next
# major version.
ParameterFilter = Sentry::Rails::ParameterFilter
deprecate_constant :ParameterFilter
end
end
end
69 changes: 69 additions & 0 deletions sentry-rails/lib/sentry/rails/parameter_filter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# frozen_string_literal: true

module Sentry
module Rails
# Shared utility for filtering sensitive parameters, leveraging Rails' built-in parameter
# filtering when available. It automatically detects the correct Rails parameter filtering
# API based on the Rails version.
#
# This is public API: it is a supported extension point for custom log subscribers, and it
# is also what the SDK uses to redact the context users attach via `Rails.error.set_context`
# before it is sent as the "rails.error" context.
#
# Mix it in to get `filter_sensitive_params` as an instance method, or call it directly on
# the module.
#
# @example Usage in a log subscriber
# class MySubscriber < Sentry::Rails::LogSubscriber
# include Sentry::Rails::ParameterFilter
#
# def my_event(event)
# if Sentry.configuration.send_default_pii && event.payload[:params]
# filtered_params = filter_sensitive_params(event.payload[:params])
# attributes[:params] = filtered_params unless filtered_params.empty?
# end
# end
# end
#
# @example Usage as a module function
# Sentry::Rails::ParameterFilter.filter_sensitive_params(password: "hunter2")
# # => { password: "[FILTERED]" }
module ParameterFilter
extend self

EMPTY_HASH = {}.freeze

if ::Rails.version.to_f >= 6.0
def self.backend
ActiveSupport::ParameterFilter
end
else
def self.backend
ActionDispatch::Http::ParameterFilter
end
end

# Filter sensitive parameters from a hash, respecting Rails configuration.
#
# @param params [Hash] The parameters to filter
# @return [Hash] Filtered parameters with sensitive data removed, or an empty hash if
# +params+ is not a Hash
def filter_sensitive_params(params)
return EMPTY_HASH unless params.is_a?(Hash)

filter_parameters = ::Rails.application&.config&.filter_parameters

# Without a booted application there are no filters configured, so there is
# nothing to redact. This runs on the exception-capture path, where raising
# would replace the user's exception with a NoMethodError from the SDK.
return params if filter_parameters.nil?

parameter_filter = ParameterFilter.backend.new(filter_parameters)

parameter_filter.filter(params)
end
end
end
end

require "sentry/rails/log_subscribers/parameter_filter"
4 changes: 4 additions & 0 deletions sentry-rails/spec/active_job/shared_examples/error_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ def perform
def perform
Rails.error.set_context(
debug_key: "important_value",
api_key: "secret-api-key",
nested: { password: "hunter2" },
timestamp: Time.utc(2026, 7, 21, 12, 34, 56),
zoned_timestamp: ActiveSupport::TimeZone["Eastern Time (US & Canada)"].parse("2026-07-21 12:34:56"),
date: Date.new(2026, 7, 21)
Expand All @@ -60,6 +62,8 @@ def perform
expect(event.contexts).to include(
"rails.error" => hash_including(
debug_key: "important_value",
api_key: "[FILTERED]",
nested: { password: "[FILTERED]" },
timestamp: Time.utc(2026, 7, 21, 12, 34, 56),
zoned_timestamp: ActiveSupport::TimeZone["Eastern Time (US & Canada)"].parse("2026-07-21 12:34:56"),
date: Date.new(2026, 7, 21)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ def exception
def exception_with_error_context
Rails.error.set_context(
debug_key: "important_value",
api_key: "secret-api-key",
nested: { password: "hunter2", safe: "kept" },
timestamp: Time.utc(2026, 7, 21, 12, 34, 56),
zoned_timestamp: ActiveSupport::TimeZone["Eastern Time (US & Canada)"].parse("2026-07-21 12:34:56"),
date: Date.new(2026, 7, 21)
Expand Down
52 changes: 52 additions & 0 deletions sentry-rails/spec/sentry/rails/error_reporter_context_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

require "spec_helper"
require "sentry/rails/active_job"

RSpec.describe Sentry::Rails::ErrorReporterContext do
describe "the objects that mix it in" do
it "does not expose parameter filtering on the CaptureExceptions middleware" do
middleware = Sentry::Rails::CaptureExceptions.new(->(_env) { [200, {}, []] })

expect(middleware).not_to respond_to(:filter_sensitive_params)
end

it "does not expose parameter filtering on the ActiveJob reporter" do
expect(Sentry::Rails::ActiveJobExtensions::SentryReporter).not_to respond_to(:filter_sensitive_params)
end
end

describe "#execution_context", skip: Rails.version.to_f < 7.0 do
subject(:context) do
Class.new { include Sentry::Rails::ErrorReporterContext }.new.execution_context["rails.error"]
end

before { make_basic_app }

it "filters sensitive values the serializer expands out of an Enumerable" do
Rails.error.set_context(records: Set[{ password: "hunter2" }])

expect(context).to eq(records: [{ password: "[FILTERED]" }])
end

it "filters sensitive values nested under an Enumerable" do
Rails.error.set_context(audit: { entries: Set[{ api_key: "secret-api-key" }] })

expect(context).to eq(audit: { entries: [{ api_key: "[FILTERED]" }] })
end

it "still passes non-enumerable values through the serializer untouched" do
Rails.error.set_context(
debug_key: "important_value",
timestamp: Time.utc(2026, 7, 21, 12, 34, 56),
date: Date.new(2026, 7, 21)
)

expect(context).to include(
debug_key: "important_value",
timestamp: Time.utc(2026, 7, 21, 12, 34, 56),
date: Date.new(2026, 7, 21)
)
end
end
end
4 changes: 2 additions & 2 deletions sentry-rails/spec/sentry/rails/log_subscriber_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
require "spec_helper"

require "sentry/rails/log_subscriber"
require "sentry/rails/log_subscribers/parameter_filter"
require "sentry/rails/parameter_filter"

RSpec.describe Sentry::Rails::LogSubscriber, type: :request do
let!(:test_subscriber) { test_subscriber_class.new }
Expand Down Expand Up @@ -218,7 +218,7 @@ def temp_event(event)
context "parameter filtering integration" do
let(:test_subscriber_class) do
Class.new(described_class) do
include Sentry::Rails::LogSubscribers::ParameterFilter
include Sentry::Rails::ParameterFilter

attach_to :filtering_test

Expand Down
39 changes: 39 additions & 0 deletions sentry-rails/spec/sentry/rails/parameter_filter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe Sentry::Rails::ParameterFilter do
context "when mixed into a class" do
before { make_basic_app }

include_examples "parameter filtering", Class.new { include Sentry::Rails::ParameterFilter }
end

it "is also callable directly on the module" do
make_basic_app

expect(described_class.filter_sensitive_params(password: "hunter2", safe: "kept"))
.to eq(password: "[FILTERED]", safe: "kept")
end

describe "#filter_sensitive_params" do
let(:includer) { Class.new { include Sentry::Rails::ParameterFilter }.new }

context "when the Rails application is not available" do
before { allow(::Rails).to receive(:application).and_return(nil) }

it "returns the params instead of raising" do
params = { password: "hunter2" }

expect { includer.filter_sensitive_params(params) }.not_to raise_error
expect(includer.filter_sensitive_params(params)).to eq(params)
end
end
end

describe "Sentry::Rails::LogSubscribers::ParameterFilter" do
it "still resolves to the module for backwards compatibility" do
expect(Sentry::Rails::LogSubscribers::ParameterFilter).to be(described_class)
end
end
end
Loading
Loading