diff --git a/.gitignore b/.gitignore index 46510a7..efbc0cb 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ .DS_Store test/dummy/log/*.log test/dummy/db/*.sqlite3 +.idea diff --git a/Gemfile.lock b/Gemfile.lock index e4aa820..8079d3e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - solid_errors (0.7.0) + solid_errors (0.7.5) actionpack (>= 7.0) actionview (>= 7.0) activerecord (>= 7.0) diff --git a/README.md b/README.md index 23a7582..6930437 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,11 @@ authenticate :user, -> (user) { user.admin? } do end ``` +After updating the gem, run the migration installer so that any new migrations are copied over: +```bash +$ rails solid_errors:install_migrations +``` + > [!NOTE] > Be sure to [secure the dashboard](#authentication) in production. @@ -247,10 +252,13 @@ Second, you can set the values via the configuration object: config.solid_errors.send_emails = true config.solid_errors.email_from = "errors@myapp.com" config.solid_errors.email_to = "devs@myapp.com" +# Tell Solid Errors whether or not to limit the total emails per occurrence. Defaults to false. config.solid_errors.email_subject_prefix = "[#{Rails.application.name}][#{Rails.env}]" ``` -If you have set `send_emails` to `true` and have set an `email_to` address, Solid Errors will send an email notification whenever an error occurs. If you have not set `send_emails` to `true` or have not set an `email_to` address, Solid Errors will not send any email notifications. +If you have set `send_emails` to `true` and have set an `email_to` address, Solid Errors will send an email notification when an error first occurs. Subsequent occurrences of the error will not trigger additional emails to be sent; however, if an error is resolved and then reoccurs, an email will be sent, again, on the first reoccurrence of the error. + +If you have not set `send_emails` to `true` or have not set an `email_to` address, Solid Errors will not send any email notifications. #### Automatically destroying old records diff --git a/app/models/solid_errors/occurrence.rb b/app/models/solid_errors/occurrence.rb index 4f034e0..1c9bc0a 100644 --- a/app/models/solid_errors/occurrence.rb +++ b/app/models/solid_errors/occurrence.rb @@ -2,7 +2,7 @@ module SolidErrors class Occurrence < Record belongs_to :error, class_name: "SolidErrors::Error" - after_create_commit :send_email, if: -> { SolidErrors.send_emails? && SolidErrors.email_to.present? } + after_create :send_email, if: :should_send_email? after_create_commit :clear_resolved_errors, if: :should_clear_resolved_errors? # The parsed exception backtrace. Lines in this backtrace that are from installed gems @@ -45,5 +45,18 @@ def should_clear_resolved_errors? true end + + def should_send_email? + return false unless SolidErrors.send_emails? && SolidErrors.email_to.present? + + # Check if resolved_at changed from a datetime to nil (resolved error reoccurred) + resolved_at_changes = error.previous_changes['resolved_at'] + resolved_error_reoccurred = resolved_at_changes&.first.present? && resolved_at_changes&.last.nil? + + # Check if this is the first occurrence of a brand new error + first_occurrence = error.occurrences.count == 1 + + resolved_error_reoccurred || first_occurrence + end end end diff --git a/lib/generators/solid_errors/install/install_generator.rb b/lib/generators/solid_errors/install/install_generator.rb index 862cd2c..8cb25b2 100644 --- a/lib/generators/solid_errors/install/install_generator.rb +++ b/lib/generators/solid_errors/install/install_generator.rb @@ -21,6 +21,7 @@ def configure_solid_errors '\1config.solid_errors.send_emails = true', '\1config.solid_errors.email_from = ""', '\1config.solid_errors.email_to = ""', + '\1config.solid_errors.full_backtrace = false', '\1config.solid_errors.username = Rails.application.credentials.dig(:solid_errors, :username)', '\1config.solid_errors.password = Rails.application.credentials.dig(:solid_errors, :password)' ].join("\n") diff --git a/lib/generators/solid_errors/install/templates/db/errors_schema.rb b/lib/generators/solid_errors/install/templates/db/errors_schema.rb index 1e60891..055c5d6 100644 --- a/lib/generators/solid_errors/install/templates/db/errors_schema.rb +++ b/lib/generators/solid_errors/install/templates/db/errors_schema.rb @@ -7,11 +7,13 @@ t.text "severity", null: false t.text "source" t.datetime "resolved_at" + t.datetime "prev_resolved_at" t.string "fingerprint", limit: 64, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["fingerprint"], name: "index_solid_errors_on_fingerprint", unique: true t.index ["resolved_at"], name: "index_solid_errors_on_resolved_at" + t.index ["prev_resolved_at"], name: "index_solid_errors_on_prev_resolved_at" end create_table "solid_errors_occurrences", force: :cascade do |t| diff --git a/lib/solid_errors.rb b/lib/solid_errors.rb index c2a3788..a6753e0 100644 --- a/lib/solid_errors.rb +++ b/lib/solid_errors.rb @@ -7,6 +7,7 @@ module SolidErrors mattr_accessor :connects_to + mattr_accessor :full_backtrace mattr_accessor :base_controller_class, default: "::ActionController::Base" mattr_writer :username mattr_writer :password @@ -18,6 +19,13 @@ module SolidErrors class << self # use method instead of attr_accessor to ensure + # this works if variable set after SolidErrors is loaded + def full_backtrace? + @full_backtrace ||= ENV["SOLIDERRORS_FULL_BACKTRACE"] || @@full_backtrace || false + end + + # use method instead of attr_accessor to ensure + # this works if variable set after SolidErrors is loaded # this works if ENV variable set after SolidErrors is loaded def username @username ||= ENV["SOLIDERRORS_USERNAME"] || @@username diff --git a/lib/solid_errors/subscriber.rb b/lib/solid_errors/subscriber.rb index 2063250..2be7e5b 100644 --- a/lib/solid_errors/subscriber.rb +++ b/lib/solid_errors/subscriber.rb @@ -1,25 +1,25 @@ module SolidErrors class Subscriber IGNORED_ERRORS = ["ActionController::RoutingError", - "AbstractController::ActionNotFound", - "ActionController::MethodNotAllowed", - "ActionController::UnknownHttpMethod", - "ActionController::NotImplemented", - "ActionController::UnknownFormat", - "ActionController::InvalidAuthenticityToken", - "ActionController::InvalidCrossOriginRequest", - "ActionDispatch::Http::Parameters::ParseError", - "ActionController::BadRequest", - "ActionController::ParameterMissing", - "ActiveRecord::RecordNotFound", - "ActionController::UnknownAction", - "ActionDispatch::Http::MimeNegotiation::InvalidType", - "Rack::QueryParser::ParameterTypeError", - "Rack::QueryParser::InvalidParameterError", - "CGI::Session::CookieStore::TamperedWithCookie", - "Mongoid::Errors::DocumentNotFound", - "Sinatra::NotFound", - "Sidekiq::JobRetry::Skip"].map(&:freeze).freeze + "AbstractController::ActionNotFound", + "ActionController::MethodNotAllowed", + "ActionController::UnknownHttpMethod", + "ActionController::NotImplemented", + "ActionController::UnknownFormat", + "ActionController::InvalidAuthenticityToken", + "ActionController::InvalidCrossOriginRequest", + "ActionDispatch::Http::Parameters::ParseError", + "ActionController::BadRequest", + "ActionController::ParameterMissing", + "ActiveRecord::RecordNotFound", + "ActionController::UnknownAction", + "ActionDispatch::Http::MimeNegotiation::InvalidType", + "Rack::QueryParser::ParameterTypeError", + "Rack::QueryParser::InvalidParameterError", + "CGI::Session::CookieStore::TamperedWithCookie", + "Mongoid::Errors::DocumentNotFound", + "Sinatra::NotFound", + "Sidekiq::JobRetry::Skip"].map(&:freeze).freeze def report(error, handled:, severity:, context:, source: nil) return if ignore_by_class?(error.class.name) @@ -37,9 +37,13 @@ def report(error, handled:, severity:, context:, source: nil) record = SolidErrors::Error.create!(error_attributes.merge(fingerprint: fingerprint)) end + backtrace_cleaner = ActiveSupport::BacktraceCleaner.new + backtrace_cleaner.add_silencer { |line| /puma|rubygems|gems/.match?(line) } + backtrace = SolidErrors.full_backtrace? ? error.backtrace : backtrace_cleaner.clean(error.backtrace) + SolidErrors::Occurrence.create( error_id: record.id, - backtrace: error.backtrace&.join("\n"), + backtrace: backtrace&.join("\n"), context: s(context) ) end diff --git a/lib/solid_errors/version.rb b/lib/solid_errors/version.rb index 0fb8883..2e9d29e 100644 --- a/lib/solid_errors/version.rb +++ b/lib/solid_errors/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SolidErrors - VERSION = "0.7.0" + VERSION = "0.8.0" end