From 651110bd87d2b581ff433b750dd2e3f2cf7d12b7 Mon Sep 17 00:00:00 2001 From: Ron Shinall <81988008+ron-shinall@users.noreply.github.com> Date: Sun, 3 Nov 2024 22:51:49 -0700 Subject: [PATCH 01/17] Introducing destroy_after config property and SolidErrors::Cleaner model --- README.md | 15 ++++++++++++ app/models/solid_errors/cleaner.rb | 35 +++++++++++++++++++++++++++ app/models/solid_errors/occurrence.rb | 1 + lib/solid_errors.rb | 5 ++++ 4 files changed, 56 insertions(+) create mode 100644 app/models/solid_errors/cleaner.rb diff --git a/README.md b/README.md index 8332ad4..a95d8fe 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ You can configure Solid Errors via the Rails configuration object, under the `so * `email_from` - The email address to send a notification from. See [Email notifications](#email-notifications) for more information. * `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. +* `destroy_after` - If set, Solid Errors will periodically destroy resolved records that are older than the duration specified. See [Automatically destroying old records](#automatically-destroying-old-records) for more information. ### Database Configuration @@ -242,6 +243,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. diff --git a/app/models/solid_errors/cleaner.rb b/app/models/solid_errors/cleaner.rb new file mode 100644 index 0000000..5fdbde7 --- /dev/null +++ b/app/models/solid_errors/cleaner.rb @@ -0,0 +1,35 @@ +module SolidErrors + class Cleaner + class << self + def go! + destroy_records if destroy_after_last_create? + end + + private + def destroy_after_last_create? + SolidErrors.destroy_after.respond_to?(:ago) && (SolidErrors::Occurrence.last.id % 100).zero? + end + + def destroy_records + ActiveRecord::Base.transaction do + destroy_occurrences + destroy_errors + end + end + + def destroy_occurrences + SolidErrors::Occurrence.joins(:error) + .merge(SolidErrors::Error.resolved) + .where(created_at: ...SolidErrors.destroy_after.ago) + .delete_all + end + + def destroy_errors + SolidErrors::Error.resolved + .where + .missing(:occurrences) + .delete_all + end + end + end +end diff --git a/app/models/solid_errors/occurrence.rb b/app/models/solid_errors/occurrence.rb index 2c815b5..7046550 100644 --- a/app/models/solid_errors/occurrence.rb +++ b/app/models/solid_errors/occurrence.rb @@ -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 -> { SolidErrors::Cleaner.go! } # 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 diff --git a/lib/solid_errors.rb b/lib/solid_errors.rb index e0716a8..b417358 100644 --- a/lib/solid_errors.rb +++ b/lib/solid_errors.rb @@ -13,6 +13,7 @@ module SolidErrors mattr_writer :email_from mattr_writer :email_to mattr_writer :email_subject_prefix + mattr_writer :destroy_after class << self # use method instead of attr_accessor to ensure @@ -42,5 +43,9 @@ def email_to def email_subject_prefix @email_subject_prefix ||= ENV["SOLIDERRORS_EMAIL_SUBJECT_PREFIX"] || @@email_subject_prefix end + + def destroy_after + @destroy_after ||= ENV["SOLIDERRORS_DESTROY_AFTER"] || @@destroy_after + end end end From fefb8873d56c2990766b3c534f9d9a7a7ef44e50 Mon Sep 17 00:00:00 2001 From: Ron Shinall <81988008+ron-shinall@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:02:24 -0700 Subject: [PATCH 02/17] Update indentation --- app/models/solid_errors/cleaner.rb | 41 +++++++++++++++--------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/app/models/solid_errors/cleaner.rb b/app/models/solid_errors/cleaner.rb index 5fdbde7..b02bdb7 100644 --- a/app/models/solid_errors/cleaner.rb +++ b/app/models/solid_errors/cleaner.rb @@ -6,30 +6,31 @@ def go! end private - def destroy_after_last_create? - SolidErrors.destroy_after.respond_to?(:ago) && (SolidErrors::Occurrence.last.id % 100).zero? - end - def destroy_records - ActiveRecord::Base.transaction do - destroy_occurrences - destroy_errors - end - end + def destroy_after_last_create? + SolidErrors.destroy_after.respond_to?(:ago) && (SolidErrors::Occurrence.last.id % 100).zero? + end - def destroy_occurrences - SolidErrors::Occurrence.joins(:error) - .merge(SolidErrors::Error.resolved) - .where(created_at: ...SolidErrors.destroy_after.ago) - .delete_all + def destroy_records + ActiveRecord::Base.transaction do + destroy_occurrences + destroy_errors end + end - def destroy_errors - SolidErrors::Error.resolved - .where - .missing(:occurrences) - .delete_all - end + def destroy_occurrences + SolidErrors::Occurrence.joins(:error) + .merge(SolidErrors::Error.resolved) + .where(created_at: ...SolidErrors.destroy_after.ago) + .delete_all + end + + def destroy_errors + SolidErrors::Error.resolved + .where + .missing(:occurrences) + .delete_all + end end end end From b5e38fab1efbedbdadf7a15ffb9849caad82729b Mon Sep 17 00:00:00 2001 From: Ron Shinall <81988008+ron-shinall@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:09:13 -0700 Subject: [PATCH 03/17] Update indentation --- app/models/solid_errors/cleaner.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/models/solid_errors/cleaner.rb b/app/models/solid_errors/cleaner.rb index b02bdb7..83abd9d 100644 --- a/app/models/solid_errors/cleaner.rb +++ b/app/models/solid_errors/cleaner.rb @@ -20,16 +20,16 @@ def destroy_records def destroy_occurrences SolidErrors::Occurrence.joins(:error) - .merge(SolidErrors::Error.resolved) - .where(created_at: ...SolidErrors.destroy_after.ago) - .delete_all + .merge(SolidErrors::Error.resolved) + .where(created_at: ...SolidErrors.destroy_after.ago) + .delete_all end def destroy_errors SolidErrors::Error.resolved - .where - .missing(:occurrences) - .delete_all + .where + .missing(:occurrences) + .delete_all end end end From 0b46552d66edfcfcdb939d54df2e3ac9cc87a79a Mon Sep 17 00:00:00 2001 From: Ron Shinall <81988008+ron-shinall@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:38:36 -0700 Subject: [PATCH 04/17] Add Rakefile to test/dummy --- test/dummy/Rakefile | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 test/dummy/Rakefile diff --git a/test/dummy/Rakefile b/test/dummy/Rakefile new file mode 100644 index 0000000..9a5ea73 --- /dev/null +++ b/test/dummy/Rakefile @@ -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 From 5c59282d0108a26979193d0f0dcb031d951664f5 Mon Sep 17 00:00:00 2001 From: Ron Shinall <81988008+ron-shinall@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:40:21 -0700 Subject: [PATCH 05/17] Update main Rakefile --- Rakefile | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Rakefile b/Rakefile index 5bb6087..5a2fe60 100644 --- a/Rakefile +++ b/Rakefile @@ -1,14 +1,20 @@ # frozen_string_literal: true +require "bundler/setup" + +APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__) +# load "rails/tasks/engine.rake" +# load "rails/tasks/statistics.rake" + require "bundler/gem_tasks" require "rake/testtask" +require "standard/rake" + +task default: %i[test standard] Rake::TestTask.new(:test) do |t| + sh("TARGET_DB=sqlite bin/setup") t.libs << "test" t.libs << "lib" t.test_files = FileList["test/**/test_*.rb"] end - -require "standard/rake" - -task default: %i[test standard] From 41d2c205e19bd53fd4d9dd9473c5261d53f781ce Mon Sep 17 00:00:00 2001 From: Ron Shinall <81988008+ron-shinall@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:09:57 -0700 Subject: [PATCH 06/17] Update main Rakefile --- Rakefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Rakefile b/Rakefile index 5a2fe60..92d2798 100644 --- a/Rakefile +++ b/Rakefile @@ -3,8 +3,7 @@ require "bundler/setup" APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__) -# load "rails/tasks/engine.rake" -# load "rails/tasks/statistics.rake" +load "rails/tasks/engine.rake" require "bundler/gem_tasks" require "rake/testtask" From e1396babbc4fcf36d85880e23095615fc2655dfb Mon Sep 17 00:00:00 2001 From: Ron Shinall <81988008+ron-shinall@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:21:21 -0700 Subject: [PATCH 07/17] Simplify database config --- Rakefile | 1 - test/dummy/config/database.yml | 33 ++-------------- test/dummy/db/errors.sqlite3 | Bin 20480 -> 0 bytes .../20241115125349_add_solid_errors_tables.rb | 27 +++++++++++++ test/dummy/db/queue.sqlite3 | Bin 20480 -> 0 bytes test/dummy/db/schema.rb | 37 ++++++++++++++++++ test/dummy/db/test.sqlite3 | Bin 20480 -> 0 bytes 7 files changed, 67 insertions(+), 31 deletions(-) delete mode 100644 test/dummy/db/errors.sqlite3 create mode 100644 test/dummy/db/migrate/20241115125349_add_solid_errors_tables.rb delete mode 100644 test/dummy/db/queue.sqlite3 create mode 100644 test/dummy/db/schema.rb delete mode 100644 test/dummy/db/test.sqlite3 diff --git a/Rakefile b/Rakefile index 92d2798..fc46bed 100644 --- a/Rakefile +++ b/Rakefile @@ -12,7 +12,6 @@ require "standard/rake" task default: %i[test standard] Rake::TestTask.new(:test) do |t| - sh("TARGET_DB=sqlite bin/setup") t.libs << "test" t.libs << "lib" t.test_files = FileList["test/**/test_*.rb"] diff --git a/test/dummy/config/database.yml b/test/dummy/config/database.yml index fbf0653..cfa50d8 100644 --- a/test/dummy/config/database.yml +++ b/test/dummy/config/database.yml @@ -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 diff --git a/test/dummy/db/errors.sqlite3 b/test/dummy/db/errors.sqlite3 deleted file mode 100644 index 31668398a50ae216d7427e7b15c9b99aa9f2489e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmeI%Jx{_w7{Kvs#g~`>ZVV1yI!R(A#Ngs08e)w2hAR)$+DyVrAuySQEZ4_N5d;_-|XXGv!3~!l}sD~1Q0*~0R#|0009ILK;T~lUh50Xxl+k{ zJF)7fqm>DxuGOZiI$^A>YAdUz#Zrg0O1WN<_3}ZrB7F%#>6zk31{CK`g89XKfvs3a% zsioX?$rDkxt)Kf(9ryVgs)6l|`r)+44U5yFi_hM2u2Aq^H%2`D*1e8Z7)N(T*{Ij; zPXzqGFcag6%21oWn>?<0T-k4#eA!>hH43vzn7D9QoCqzr6}6JAs&sN#OAc%ZAbZVV1yI!R(A#Ngs08e)w2hAR)$+DyVrAuySQEZ4_N5d;_-|XXGv!3~!l}sD~1Q0*~0R#|0009ILK;T~lUh50Xxl+k{ zJF)7fqm>DxuGOZiI$^A>YAdUz#Zrg0O1WN<_3}ZrB7F%#>6zk31{CK`g89XKfvs3a% zsioX?$rDkxt)Kf(9ryVgs)6l|`r)+44U5yFi_hM2u2Aq^H%2`D*1e8Z7)N(T*{Ij; zPXzqGFcag6%21oWn>?<0T-k4#eA!>hH43vzn7D9QoCqzr6}6JAs&sN#OAc%ZAbZVV1yI!R(A#Ngs08e)w2hAR)$+DyVrAuySQEZ4_N5d;_-|XXGv!3~!l}sD~1Q0*~0R#|0009ILK;T~lUh50Xxl+k{ zJF)7fqm>DxuGOZiI$^A>YAdUz#Zrg0O1WN<_3}ZrB7F%#>6zk31{CK`g89XKfvs3a% zsioX?$rDkxt)Kf(9ryVgs)6l|`r)+44U5yFi_hM2u2Aq^H%2`D*1e8Z7)N(T*{Ij; zPXzqGFcag6%21oWn>?<0T-k4#eA!>hH43vzn7D9QoCqzr6}6JAs&sN#OAc%ZAb Date: Fri, 15 Nov 2024 09:29:44 -0700 Subject: [PATCH 08/17] Updating tests to better follow Rails convention --- Rakefile | 2 +- test/{test_solid_errors.rb => solid_errors_test.rb} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename test/{test_solid_errors.rb => solid_errors_test.rb} (100%) diff --git a/Rakefile b/Rakefile index fc46bed..66a8ce6 100644 --- a/Rakefile +++ b/Rakefile @@ -14,5 +14,5 @@ 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 diff --git a/test/test_solid_errors.rb b/test/solid_errors_test.rb similarity index 100% rename from test/test_solid_errors.rb rename to test/solid_errors_test.rb From 0dd2ea4797a4516ae432c8efe9c31aa1013a543a Mon Sep 17 00:00:00 2001 From: Ron Shinall <81988008+ron-shinall@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:23:40 -0700 Subject: [PATCH 09/17] Update test environment --- Gemfile.lock | 4 ++++ solid_errors.gemspec | 1 + test/test_helper.rb | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9a997c5..83d777c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -88,6 +88,8 @@ GEM net-smtp mini_mime (1.1.5) minitest (5.25.1) + mocha (2.4.5) + ruby2_keywords (>= 0.0.5) net-imap (0.4.14) date net-protocol @@ -157,6 +159,7 @@ GEM rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) securerandom (0.3.1) sqlite3 (2.0.4-arm64-darwin) sqlite3 (2.0.4-x86_64-linux-gnu) @@ -189,6 +192,7 @@ PLATFORMS DEPENDENCIES minitest (~> 5.0) + mocha rake (~> 13.0) solid_errors! sqlite3 diff --git a/solid_errors.gemspec b/solid_errors.gemspec index 67e2228..842d9e6 100644 --- a/solid_errors.gemspec +++ b/solid_errors.gemspec @@ -30,4 +30,5 @@ Gem::Specification.new do |spec| end spec.add_development_dependency "sqlite3" + spec.add_development_dependency "mocha" end diff --git a/test/test_helper.rb b/test/test_helper.rb index e83f265..86dd1e7 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -9,4 +9,4 @@ require "rails/test_help" require "solid_errors" -require "minitest/autorun" +require "mocha/minitest" From 31fd6a8340dcc24437edaeec5438872c5b663b38 Mon Sep 17 00:00:00 2001 From: Ron Shinall <81988008+ron-shinall@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:24:16 -0700 Subject: [PATCH 10/17] Update cleaner_test --- test/models/solid_errors/cleaner_test.rb | 55 ++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 test/models/solid_errors/cleaner_test.rb diff --git a/test/models/solid_errors/cleaner_test.rb b/test/models/solid_errors/cleaner_test.rb new file mode 100644 index 0000000..e87a2a4 --- /dev/null +++ b/test/models/solid_errors/cleaner_test.rb @@ -0,0 +1,55 @@ +require "test_helper" + +class SolidErrors::CleanerTest < ActiveSupport::TestCase + setup do + assert_nil SolidErrors.destroy_after + end + + test "not destroy if destroy_after is not set" do + simulate_99_old_exceptions(:resolved) + previous_error = SolidErrors::Error.last + previous_occurrence = SolidErrors::Occurrence.last + Rails.error.report(dummy_exception) + + assert SolidErrors::Error.exists?(id: previous_error.id) + assert SolidErrors::Occurrence.exists?(id: previous_occurrence.id) + end + + test "destroy old occurrences every 100 insertions if destroy_after is set" do + set_destroy_after + simulate_99_old_exceptions(:resolved) + Rails.error.report(dummy_exception) + + assert_equal 1, SolidErrors::Error.count + assert_equal 1, SolidErrors::Occurrence.count + end + + test "not destroy if errors are unresolved" do + set_destroy_after + simulate_99_old_exceptions(:unresolved) + + assert_difference -> { SolidErrors::Error.count }, +1 do + assert_difference -> { SolidErrors::Occurrence.count }, +1 do + Rails.error.report(dummy_exception) + end + end + end + + private + + def simulate_99_old_exceptions(status) + Rails.error.report(dummy_exception("argh")) + SolidErrors::Error.update_all(resolved_at: Time.current) if status == :resolved + SolidErrors::Occurrence.last.update!(id: 99, created_at: 1.day.ago) + end + + def set_destroy_after + SolidErrors.stubs(destroy_after: 1.day) + end + + def dummy_exception(message = "oof") + exception = StandardError.new(message) + exception.set_backtrace(caller) + exception + end +end From eb5454fe0ad69292aba528157167ffdfaf219000 Mon Sep 17 00:00:00 2001 From: Ron Shinall <81988008+ron-shinall@users.noreply.github.com> Date: Fri, 15 Nov 2024 12:42:44 -0700 Subject: [PATCH 11/17] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a95d8fe..8b2a3a1 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ You can configure Solid Errors via the Rails configuration object, under the `so * `email_from` - The email address to send a notification from. See [Email notifications](#email-notifications) for more information. * `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. -* `destroy_after` - If set, Solid Errors will periodically destroy resolved records that are older than the duration specified. See [Automatically destroying old records](#automatically-destroying-old-records) 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 From b98e2d628b2bfda5ae512fe46499261265393e31 Mon Sep 17 00:00:00 2001 From: Ron Shinall <81988008+ron-shinall@users.noreply.github.com> Date: Fri, 15 Nov 2024 13:03:38 -0700 Subject: [PATCH 12/17] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b2a3a1..eb16a99 100644 --- a/README.md +++ b/README.md @@ -352,7 +352,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). From a740e51ae8bd4fe61ebc75f802945b15a9e0d9fc Mon Sep 17 00:00:00 2001 From: Ron Shinall <81988008+ron-shinall@users.noreply.github.com> Date: Fri, 15 Nov 2024 20:14:34 -0700 Subject: [PATCH 13/17] Add sqlite3 files to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 927a7dc..46510a7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /tmp/ .DS_Store test/dummy/log/*.log +test/dummy/db/*.sqlite3 From 3b08e935016faddab01efd8cecc4c62c3ef24b9c Mon Sep 17 00:00:00 2001 From: Ron Shinall <81988008+ron-shinall@users.noreply.github.com> Date: Sat, 16 Nov 2024 09:19:13 -0700 Subject: [PATCH 14/17] Improve tests --- test/models/solid_errors/cleaner_test.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/models/solid_errors/cleaner_test.rb b/test/models/solid_errors/cleaner_test.rb index e87a2a4..57bc9d0 100644 --- a/test/models/solid_errors/cleaner_test.rb +++ b/test/models/solid_errors/cleaner_test.rb @@ -7,12 +7,12 @@ class SolidErrors::CleanerTest < ActiveSupport::TestCase test "not destroy if destroy_after is not set" do simulate_99_old_exceptions(:resolved) - previous_error = SolidErrors::Error.last - previous_occurrence = SolidErrors::Occurrence.last - Rails.error.report(dummy_exception) - assert SolidErrors::Error.exists?(id: previous_error.id) - assert SolidErrors::Occurrence.exists?(id: previous_occurrence.id) + assert_difference -> { SolidErrors::Error.count }, +1 do + assert_difference -> { SolidErrors::Occurrence.count }, +1 do + Rails.error.report(dummy_exception) + end + end end test "destroy old occurrences every 100 insertions if destroy_after is set" do @@ -30,6 +30,7 @@ class SolidErrors::CleanerTest < ActiveSupport::TestCase assert_difference -> { SolidErrors::Error.count }, +1 do assert_difference -> { SolidErrors::Occurrence.count }, +1 do + assert_empty SolidErrors::Error.resolved Rails.error.report(dummy_exception) end end From da248f9bba5a5025e9883477becfabb6f4f670ca Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Wed, 11 Jun 2025 16:38:16 +0200 Subject: [PATCH 15/17] Tweak some details --- Gemfile.lock | 4 -- app/models/solid_errors/cleaner.rb | 36 ------------------ app/models/solid_errors/occurrence.rb | 25 ++++++++++++- lib/solid_errors/subscriber.rb | 2 +- solid_errors.gemspec | 1 - .../{cleaner_test.rb => occurrence_test.rb} | 37 ++++++++----------- test/test_helper.rb | 2 - 7 files changed, 39 insertions(+), 68 deletions(-) delete mode 100644 app/models/solid_errors/cleaner.rb rename test/models/solid_errors/{cleaner_test.rb => occurrence_test.rb} (55%) diff --git a/Gemfile.lock b/Gemfile.lock index 83d777c..9a997c5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -88,8 +88,6 @@ GEM net-smtp mini_mime (1.1.5) minitest (5.25.1) - mocha (2.4.5) - ruby2_keywords (>= 0.0.5) net-imap (0.4.14) date net-protocol @@ -159,7 +157,6 @@ GEM rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) securerandom (0.3.1) sqlite3 (2.0.4-arm64-darwin) sqlite3 (2.0.4-x86_64-linux-gnu) @@ -192,7 +189,6 @@ PLATFORMS DEPENDENCIES minitest (~> 5.0) - mocha rake (~> 13.0) solid_errors! sqlite3 diff --git a/app/models/solid_errors/cleaner.rb b/app/models/solid_errors/cleaner.rb deleted file mode 100644 index 83abd9d..0000000 --- a/app/models/solid_errors/cleaner.rb +++ /dev/null @@ -1,36 +0,0 @@ -module SolidErrors - class Cleaner - class << self - def go! - destroy_records if destroy_after_last_create? - end - - private - - def destroy_after_last_create? - SolidErrors.destroy_after.respond_to?(:ago) && (SolidErrors::Occurrence.last.id % 100).zero? - end - - def destroy_records - ActiveRecord::Base.transaction do - destroy_occurrences - destroy_errors - end - end - - def destroy_occurrences - SolidErrors::Occurrence.joins(:error) - .merge(SolidErrors::Error.resolved) - .where(created_at: ...SolidErrors.destroy_after.ago) - .delete_all - end - - def destroy_errors - SolidErrors::Error.resolved - .where - .missing(:occurrences) - .delete_all - end - end - end -end diff --git a/app/models/solid_errors/occurrence.rb b/app/models/solid_errors/occurrence.rb index 7046550..201fe4b 100644 --- a/app/models/solid_errors/occurrence.rb +++ b/app/models/solid_errors/occurrence.rb @@ -2,8 +2,8 @@ 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_commit -> { SolidErrors::Cleaner.go! } + 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 @@ -24,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 diff --git a/lib/solid_errors/subscriber.rb b/lib/solid_errors/subscriber.rb index 05f6e46..2063250 100644 --- a/lib/solid_errors/subscriber.rb +++ b/lib/solid_errors/subscriber.rb @@ -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 diff --git a/solid_errors.gemspec b/solid_errors.gemspec index 842d9e6..67e2228 100644 --- a/solid_errors.gemspec +++ b/solid_errors.gemspec @@ -30,5 +30,4 @@ Gem::Specification.new do |spec| end spec.add_development_dependency "sqlite3" - spec.add_development_dependency "mocha" end diff --git a/test/models/solid_errors/cleaner_test.rb b/test/models/solid_errors/occurrence_test.rb similarity index 55% rename from test/models/solid_errors/cleaner_test.rb rename to test/models/solid_errors/occurrence_test.rb index 57bc9d0..c994e7d 100644 --- a/test/models/solid_errors/cleaner_test.rb +++ b/test/models/solid_errors/occurrence_test.rb @@ -1,37 +1,40 @@ require "test_helper" -class SolidErrors::CleanerTest < ActiveSupport::TestCase - setup do - assert_nil SolidErrors.destroy_after +class SolidErrors::OccurrenceTest < ActiveSupport::TestCase + def teardown + SolidErrors.destroy_after = nil end - test "not destroy if destroy_after is not set" do + 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(dummy_exception) + Rails.error.report(StandardError.new("oof")) end end end test "destroy old occurrences every 100 insertions if destroy_after is set" do - set_destroy_after + SolidErrors.destroy_after = 1.day simulate_99_old_exceptions(:resolved) - Rails.error.report(dummy_exception) - assert_equal 1, SolidErrors::Error.count - assert_equal 1, SolidErrors::Occurrence.count + 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 - set_destroy_after + 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(dummy_exception) + Rails.error.report(StandardError.new("oof")) end end end @@ -39,18 +42,8 @@ class SolidErrors::CleanerTest < ActiveSupport::TestCase private def simulate_99_old_exceptions(status) - Rails.error.report(dummy_exception("argh")) + 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 - - def set_destroy_after - SolidErrors.stubs(destroy_after: 1.day) - end - - def dummy_exception(message = "oof") - exception = StandardError.new(message) - exception.set_backtrace(caller) - exception - end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 86dd1e7..9fcf2e2 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -8,5 +8,3 @@ ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] require "rails/test_help" require "solid_errors" - -require "mocha/minitest" From b407f2a4d596afe22c04b74d6998e979dba91537 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Wed, 11 Jun 2025 16:54:17 +0200 Subject: [PATCH 16/17] fix test --- app/models/solid_errors/occurrence.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/solid_errors/occurrence.rb b/app/models/solid_errors/occurrence.rb index 201fe4b..4f034e0 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_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 From 7fddfb56b61a86d3982f70e4df53e9dd114e2fc4 Mon Sep 17 00:00:00 2001 From: Stephen Margheim Date: Wed, 11 Jun 2025 17:06:39 +0200 Subject: [PATCH 17/17] Use mattr_accessor for most things, since they can't be string from ENV variables anyway --- lib/solid_errors.rb | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/lib/solid_errors.rb b/lib/solid_errors.rb index 4686d3b..c2a3788 100644 --- a/lib/solid_errors.rb +++ b/lib/solid_errors.rb @@ -10,41 +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_writer :destroy_after + 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 - end - - def destroy_after - @destroy_after ||= ENV["SOLIDERRORS_DESTROY_AFTER"] || @@destroy_after + send_emails && email_to.present? end end end