Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
/tmp/
.DS_Store
test/dummy/log/*.log
test/dummy/db/*.sqlite3
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ You can configure Solid Errors via the Rails configuration object, under the `so
* `email_to` - The email address(es) to send a notification to. See [Email notifications](#email-notifications) for more information.
* `email_subject_prefix` - Prefix added to 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.

### Database Configuration

Expand Down Expand Up @@ -256,6 +257,20 @@ config.solid_errors.email_subject_prefix = "[#{Rails.application.name}][#{Rails.

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.

#### Automatically destroying old records

Setting `destroy_after` to a duration will allow Solid Errors to be self-maintaining by peridically destroying **resolved** records that are older than that value. The value provided must respond to `.ago`.

```ruby
# Automatically destroy records older than 30 days
config.solid_errors.destroy_after = 30.days
```

```ruby
# Automatically destroy records older than 6 months
config.solid_errors.destroy_after = 6.months
```

### Examples

There are only two screens in the dashboard.
Expand Down Expand Up @@ -351,7 +366,7 @@ You can always take control of the views by creating your own views and/or parti

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. If you want to set up a local development database, run `rake db:migrate`.

To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).

Expand Down
14 changes: 9 additions & 5 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
# frozen_string_literal: true

require "bundler/setup"

APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
load "rails/tasks/engine.rake"

require "bundler/gem_tasks"
require "rake/testtask"
require "standard/rake"

task default: %i[test standard]

Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList["test/**/test_*.rb"]
t.test_files = FileList["test/**/*_test.rb"]
end

require "standard/rake"

task default: %i[test standard]
22 changes: 22 additions & 0 deletions app/models/solid_errors/occurrence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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_commit :clear_resolved_errors, if: :should_clear_resolved_errors?

# The parsed exception backtrace. Lines in this backtrace that are from installed gems
# have the base path for gem installs replaced by "[GEM_ROOT]", while those in the project
Expand All @@ -23,5 +24,26 @@ def parse_backtrace(backtrace)
def send_email
ErrorMailer.error_occurred(self).deliver_later
end

def clear_resolved_errors
transaction do
SolidErrors::Occurrence
.where(error: SolidErrors::Error.resolved)
.where(created_at: ...SolidErrors.destroy_after.ago)
.delete_all
SolidErrors::Error.resolved
.where
.missing(:occurrences)
.delete_all
end
end

def should_clear_resolved_errors?
return false unless SolidErrors.destroy_after
return false unless SolidErrors.destroy_after.respond_to?(:ago)
return false unless (id % 100).zero?

true
end
end
end
27 changes: 9 additions & 18 deletions lib/solid_errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,27 @@ module SolidErrors
mattr_accessor :base_controller_class, default: "::ActionController::Base"
mattr_writer :username
mattr_writer :password
mattr_writer :send_emails
mattr_writer :email_from
mattr_writer :email_to
mattr_writer :email_subject_prefix
mattr_accessor :send_emails, default: false
mattr_accessor :email_from, default: "solid_errors@noreply.com"
mattr_accessor :email_to
mattr_accessor :email_subject_prefix
mattr_accessor :destroy_after

class << self
# 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
end

# use method instead of attr_accessor to ensure
# this works if ENV variable set after SolidErrors is loaded
def password
@password ||= ENV["SOLIDERRORS_PASSWORD"] || @@password
end

def send_emails?
@send_emails ||= ENV["SOLIDERRORS_SEND_EMAILS"] || @@send_emails || false
end

def email_from
@email_from ||= ENV["SOLIDERRORS_EMAIL_FROM"] || @@email_from || "solid_errors@noreply.com"
end

def email_to
@email_to ||= ENV["SOLIDERRORS_EMAIL_TO"] || @@email_to
end

def email_subject_prefix
@email_subject_prefix ||= ENV["SOLIDERRORS_EMAIL_SUBJECT_PREFIX"] || @@email_subject_prefix
send_emails && email_to.present?
end
end
end
2 changes: 1 addition & 1 deletion lib/solid_errors/subscriber.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def report(error, handled:, severity:, context:, source: nil)

SolidErrors::Occurrence.create(
error_id: record.id,
backtrace: error.backtrace.join("\n"),
backtrace: error.backtrace&.join("\n"),
context: s(context)
)
end
Expand Down
6 changes: 6 additions & 0 deletions test/dummy/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

require_relative "config/application"

Rails.application.load_tasks
33 changes: 3 additions & 30 deletions test/dummy/config/database.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,15 @@ default: &default
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000

primary: &primary
<<: *default
database: storage/<%= ENV.fetch("RAILS_ENV", "development") %>.sqlite3

queue: &queue
<<: *default
migrations_paths: db/queue_migrate
database: storage/queue.sqlite3

errors: &errors
<<: *default
migrations_paths: db/errors_migrate
database: storage/errors.sqlite3

development:
primary:
<<: *primary
database: storage/<%= `git branch --show-current`.chomp || 'development' %>.sqlite3
queue: *queue
errors: *errors
<<: *default
database: db/development.sqlite3

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
primary:
<<: *primary
<<: *default
database: db/test.sqlite3
queue:
<<: *queue
database: db/queue.sqlite3
errors:
<<: *errors
database: db/errors.sqlite3

production:
primary: *primary
queue: *queue
errors: *errors
Binary file removed test/dummy/db/errors.sqlite3
Binary file not shown.
27 changes: 27 additions & 0 deletions test/dummy/db/migrate/20241115125349_add_solid_errors_tables.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class AddSolidErrorsTables < ActiveRecord::Migration[7.1]
def change
create_table "solid_errors", force: :cascade do |t|
t.text "exception_class", null: false
t.text "message", null: false
t.text "severity", null: false
t.text "source"
t.datetime "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"
end

create_table "solid_errors_occurrences", force: :cascade do |t|
t.bigint "error_id", null: false
t.text "backtrace"
t.json "context"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["error_id"], name: "index_solid_errors_occurrences_on_error_id"
end

add_foreign_key "solid_errors_occurrences", "solid_errors", column: "error_id"
end
end
Binary file removed test/dummy/db/queue.sqlite3
Binary file not shown.
37 changes: 37 additions & 0 deletions test/dummy/db/schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.1].define(version: 2024_11_15_125349) do
create_table "solid_errors", force: :cascade do |t|
t.text "exception_class", null: false
t.text "message", null: false
t.text "severity", null: false
t.text "source"
t.datetime "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"
end

create_table "solid_errors_occurrences", force: :cascade do |t|
t.bigint "error_id", null: false
t.text "backtrace"
t.json "context"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["error_id"], name: "index_solid_errors_occurrences_on_error_id"
end

add_foreign_key "solid_errors_occurrences", "solid_errors", column: "error_id"
end
Binary file removed test/dummy/db/test.sqlite3
Binary file not shown.
49 changes: 49 additions & 0 deletions test/models/solid_errors/occurrence_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require "test_helper"

class SolidErrors::OccurrenceTest < ActiveSupport::TestCase
def teardown
SolidErrors.destroy_after = nil
end

test "do not destroy if destroy_after is not set" do
SolidErrors.destroy_after = nil
simulate_99_old_exceptions(:resolved)

assert_difference -> { SolidErrors::Error.count }, +1 do
assert_difference -> { SolidErrors::Occurrence.count }, +1 do
Rails.error.report(StandardError.new("oof"))
end
end
end

test "destroy old occurrences every 100 insertions if destroy_after is set" do
SolidErrors.destroy_after = 1.day
simulate_99_old_exceptions(:resolved)

assert_difference -> { SolidErrors::Error.count }, 0 do
assert_difference -> { SolidErrors::Occurrence.count }, 0 do
Rails.error.report(StandardError.new("oof"))
end
end
end

test "not destroy if errors are unresolved" do
SolidErrors.destroy_after = 1.day
simulate_99_old_exceptions(:unresolved)

assert_difference -> { SolidErrors::Error.count }, +1 do
assert_difference -> { SolidErrors::Occurrence.count }, +1 do
assert_empty SolidErrors::Error.resolved
Rails.error.report(StandardError.new("oof"))
end
end
end

private

def simulate_99_old_exceptions(status)
Rails.error.report(StandardError.new("argh"))
SolidErrors::Error.update_all(resolved_at: Time.current) if status == :resolved
SolidErrors::Occurrence.last.update!(id: 99, created_at: 1.day.ago)
end
end
File renamed without changes.
2 changes: 0 additions & 2 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,3 @@
ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
require "rails/test_help"
require "solid_errors"

require "minitest/autorun"