diff --git a/README.md b/README.md index 937b804..5a71eda 100644 --- a/README.md +++ b/README.md @@ -157,14 +157,21 @@ end ### What each occurrence records -Every occurrence stores the backtrace and the error's context. Rails adds the current request or job to that context, and Solid Errors records it as: +Every occurrence stores the backtrace and the error's context. Rails adds the current request or job to that context, and Solid Errors records which one it was: the controller action (`UsersController#show`), or the job's class, id and queue. -* **Requests:** the controller action (`UsersController#show`) and its params, with your `config.filter_parameters` applied. -* **Background jobs:** the job class, its id and queue, and its arguments, also filtered. +Request params and job arguments are not recorded unless you opt in, because they often carry user content that `config.filter_parameters` can't catch: it matches keys, so positional job arguments, mail delivery jobs (the mail's recipients and body) and webhook payloads would be stored and emailed as they are. + +```ruby +config.solid_errors.record_params = true # the request's params, with filter_parameters applied +config.solid_errors.record_arguments = true # the job's arguments, with filter_parameters applied +``` + +Context you set yourself takes precedence. For example, `Rails.error.set_context(params: ...)` in a `before_action` replaces the recorded params. Any Active Record object in the context is stored as its [GlobalID](https://github.com/rails/globalid) (`gid://my-app/User/1`), so you can tell which record was involved. Add your own context with `Rails.error.set_context(...)` or the `context:` option shown above. Errors raised in background jobs reach Solid Errors when your job backend runs jobs inside the Rails executor, as Solid Queue, Sidekiq and GoodJob do. You don't need any extra setup for this. + ### Configuration You can configure Solid Errors via the Rails configuration object, under the `solid_errors` key. Currently, the following configuration options are available: @@ -181,6 +188,7 @@ You can configure Solid Errors via the Rails configuration object, under the `so * `email_subject` - A proc that builds the subject line for email notifications. See [Email notifications](#email-notifications) for more information. * `base_controller_class` - Specify a different controller as the base class for the Solid Errors controller. See [Authentication](#authentication) for more information. * `destroy_after` - If set, Solid Errors will periodically destroy resolved records that are older than the value specified. See [Automatically destroying old records](#automatically-destroying-old-records) for more information. +* `record_params` / `record_arguments` - Whether to record request params and job arguments with each occurrence. Both default to `false`. See [What each occurrence records](#what-each-occurrence-records). * `ignored_errors` - Error classes, or their names, to never record, in addition to the built-in list in `SolidErrors::Subscriber::IGNORED_ERRORS`. For example, `config.solid_errors.ignored_errors = ["ActiveStorage::FileNotFoundError"]`. ### Database Configuration diff --git a/lib/solid_errors.rb b/lib/solid_errors.rb index c4faa29..81355fb 100644 --- a/lib/solid_errors.rb +++ b/lib/solid_errors.rb @@ -21,6 +21,9 @@ module SolidErrors } mattr_accessor :destroy_after mattr_accessor :ignored_errors, default: [] + # Request params and job arguments carry user content that filter_parameters' key matching misses + mattr_accessor :record_params, default: false + mattr_accessor :record_arguments, default: false mattr_writer :url_helper_name, :url_options class << self diff --git a/lib/solid_errors/subscriber.rb b/lib/solid_errors/subscriber.rb index 21b2b6b..72e7248 100644 --- a/lib/solid_errors/subscriber.rb +++ b/lib/solid_errors/subscriber.rb @@ -52,13 +52,13 @@ def readable_context(context) context = context.dup if defined?(ActionController::Metal) && (controller = context[:controller]).is_a?(ActionController::Metal) context[:controller] = "#{controller.class.name}##{controller.action_name}" - context[:params] ||= controller.request.filtered_parameters.except("controller", "action") + context[:params] ||= controller.request.filtered_parameters.except("controller", "action") if SolidErrors.record_params end if defined?(ActiveJob::Base) && (job = context[:job]).is_a?(ActiveJob::Base) context[:job] = job.class.name context[:job_id] ||= job.job_id context[:queue] ||= job.queue_name - context[:arguments] ||= parameter_filter.filter(arguments: s(job.arguments))[:arguments] + context[:arguments] ||= parameter_filter.filter(arguments: s(job.arguments))[:arguments] if SolidErrors.record_arguments end context end diff --git a/test/solid_errors/context_test.rb b/test/solid_errors/context_test.rb index be589b6..3af49e3 100644 --- a/test/solid_errors/context_test.rb +++ b/test/solid_errors/context_test.rb @@ -1,7 +1,31 @@ require "test_helper" class SolidErrors::ContextTest < ActionDispatch::IntegrationTest - test "an error in a request records the controller action and its filtered params" do + teardown do + SolidErrors.record_params = false + SolidErrors.record_arguments = false + end + + test "an error in a request records the controller action but not its params by default" do + post "/reports", params: {message: {content: "something private"}} + + context = SolidErrors::Occurrence.last.context + assert_equal "ReportsController#create", context["controller"] + assert_not context.key?("params") + end + + test "an error in a job records the job but not its arguments by default" do + job = ReportJob.new("someone@example.com", {"body" => "something private"}) + job.perform_now + + context = SolidErrors::Occurrence.last.context + assert_equal ["ReportJob", job.job_id, "default"], context.values_at("job", "job_id", "queue") + assert_not context.key?("arguments") + end + + test "an error in a request records its filtered params when the app opts in" do + SolidErrors.record_params = true + post "/reports", params: {name: "Ada", password: "secret"} context = SolidErrors::Occurrence.last.context @@ -9,7 +33,8 @@ class SolidErrors::ContextTest < ActionDispatch::IntegrationTest assert_equal({"name" => "Ada", "password" => "[FILTERED]"}, context["params"]) end - test "an error in a job records the job and its filtered arguments, with records as global ids" do + test "an error in a job records its filtered arguments, with records as global ids, when the app opts in" do + SolidErrors.record_arguments = true record = record_an_error job = ReportJob.new(record, {"password" => "secret", "retries" => 3})