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
18 changes: 17 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,34 @@
# 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")

# Serialize a context hash and redact anything matching the application's
# `config.filter_parameters`, so that user data attached via `Rails.error.set_context`
# gets the same treatment whether it reaches us through
# `ActiveSupport::ExecutionContext` or through the error reporter.
#
# 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 sanitize_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" => sanitize_context(context) }
end
else
def execution_context
Expand Down
6 changes: 5 additions & 1 deletion sentry-rails/lib/sentry/rails/error_subscriber.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# 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.
# See https://github.com/rails/rails/blob/main/activesupport/lib/active_support/error_reporter.rb to learn more about reporting APIs.
# If you want Sentry to subscribe to the error reporter, please set `config.rails.register_error_subscriber` to `true`.
class ErrorSubscriber
include ErrorReporterContext

SKIP_SOURCES = Regexp.union([/.*_cache_store.active_support/])

def report(error, handled:, severity:, context:, source: nil)
Expand All @@ -29,7 +33,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" => sanitize_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
47 changes: 3 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,11 @@
# 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
ParameterFilter = Sentry::Rails::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"
73 changes: 55 additions & 18 deletions sentry-rails/spec/active_job/shared_examples/error_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,33 +37,70 @@ def perform
expect(last_frame.vars).to include(a: "1", b: "0")
end

it "includes Rails.error.set_context data attached before the job raises", skip: RAILS_VERSION < 7.0 do
job_with_context = job_fixture do
def perform
Rails.error.set_context(
context "with Rails.error.set_context data attached before the job raises", skip: RAILS_VERSION < 7.0 do
def capture_job_error_with_context(context)
job_with_context = job_fixture do
define_method(:perform) do
Rails.error.set_context(**context)
raise "boom with rails error context"
end
end

expect do
job_with_context.perform_later
drain
end.to raise_error(RuntimeError, /boom with rails error context/)

last_sentry_event
end

it "attaches the context to the captured event" do
event = capture_job_error_with_context(
debug_key: "important_value",
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)
)

expect(event.contexts).to include(
"rails.error" => hash_including(
debug_key: "important_value",
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)
)
raise "boom with rails error context"
end
)
end

expect do
job_with_context.perform_later
drain
end.to raise_error(RuntimeError, /boom with rails error context/)
it "redacts values matching config.filter_parameters" do
event = capture_job_error_with_context(
api_key: "secret-api-key",
nested: { password: "hunter2", safe: "kept" }
)

event = last_sentry_event
expect(event.contexts).to include(
"rails.error" => hash_including(
api_key: "[FILTERED]",
nested: { password: "[FILTERED]", safe: "kept" }
)
)
end

expect(event.contexts).to include(
"rails.error" => hash_including(
debug_key: "important_value",
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)
it "redacts sensitive values inside non-Hash Enumerables" do
event = capture_job_error_with_context(records: Set[{ password: "hunter2" }])

expect(event.contexts).to include(
"rails.error" => hash_including(records: [{ password: "[FILTERED]" }])
)
)
end

it "does not expand the job instance into the context" do
event = capture_job_error_with_context(debug_key: "important_value")

job = event.to_json_compatible.dig("contexts", "rails.error", "job")

expect(job).to be_a(String)
expect(job).to match(/#<JobFixture/)
end
end
end
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
39 changes: 39 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,39 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe Sentry::Rails::ErrorReporterContext do
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
Loading
Loading