bard-exception_notification 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a24301e4be9951e47b9c36677feddb1e821b1f2ce800c9981267cea23c3b62a7
4
+ data.tar.gz: 6ba33cc979646e6291832a1d7e00e78db3006773bda7dfcb25bd88d864eba420
5
+ SHA512:
6
+ metadata.gz: 86e2d68724422b51e9851d22c940fb9a57b59539f70815eb200e3f0776bbdf91b1ceda1dfa8899e4433b07942ac4d6ef838c9aaca1b3ebfb185dcbccc000d814
7
+ data.tar.gz: bd446d6a565fba957c8f82b4dadf318c78304c0ee56e1f182f672e07b252ba2172b96e92ba826bfc20910db6d5b26e70d6d175d1278d6d4756848e24fc8bb39b
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Appraisals ADDED
@@ -0,0 +1,11 @@
1
+ appraise "rails-7.2" do
2
+ gem "rails", "~> 7.2.0"
3
+ end
4
+
5
+ appraise "rails-8.0" do
6
+ gem "rails", "~> 8.0.0"
7
+ end
8
+
9
+ appraise "rails-8.1" do
10
+ gem "rails", "~> 8.1.0"
11
+ end
data/CLAUDE.md ADDED
@@ -0,0 +1,30 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this is
6
+
7
+ A Rails engine gem that gives every BARD-managed Rails app the same exception_notification setup: email notifications for web requests, ActiveJob, rake tasks and `ActionController::Live`, plus `rake exception_notification:test` to verify delivery end to end in a deployed app.
8
+
9
+ ## Commands
10
+
11
+ - Full suite (fast, under a second): `bundle exec rspec` (also `bundle exec rake`)
12
+ - Single file / example: `bundle exec rspec spec/bard/exception_notification_spec.rb:42`
13
+ - Rails matrix (as CI runs it, Ruby 3.2–4.0 × Rails 7.2/8.0/8.1): `bundle exec appraisal install`, then `bundle exec appraisal rails-7.2 rspec`
14
+
15
+ ## Architecture
16
+
17
+ - `lib/bard/exception_notification.rb` holds the shared pieces: `email_options` (recipient, sender, `[<app dir name>]` subject prefix, SMTP from `credentials.exception_notification_smtp_settings`, and `sections` that deliberately omit `session`), `notify_job_failure`, and two prepend modules (`LiveErrorNotification`, `UnknownTaskNotification`).
18
+ - `lib/bard/exception_notification/engine.rb` wires it into the host app:
19
+ - The defaults initializer runs `before: :load_config_initializers`, so a host app's own `ExceptionNotification.configure` can add to (`+=`) or override the defaults. Preserve that ordering.
20
+ - It adds `filter_parameters` for cookie/auth/session env keys so they show as `[FILTERED]` in emails, and ignores everything outside `Rails.env.production?`.
21
+ - Job failures come from a `perform.active_job` notification subscriber, which fires only once retries are exhausted and never for discarded jobs.
22
+ - The test route is prepended with a constraint on `Test::ENV_KEY`, an env key set only in-process, so it can't be reached over HTTP.
23
+ - `lib/bard/exception_notification/test.rb` + `lib/tasks/exception_notification.rake` implement the end-to-end check. `Test.web` drives `Rails.application.call` with a mock request and inspects `exception_notifier.delivered`. It sets `ExceptionNotifier.testing_mode = true` so delivery failures raise instead of being only logged. The `test` task runs `web`, then `job`, then `rake`; `rake` goes last because it aborts by raising.
24
+ - `app/` holds the engine's `TestsController` and `TestJob` (which inherits the host's `::ApplicationJob`).
25
+
26
+ ## Specs
27
+
28
+ - `spec/spec_helper.rb` defines an inline `Dummy::Application` (root `spec/dummy`), the `/boom` and `/login` routes, and the `request`/`deliveries` helpers. It stubs `Rails.env` to `production` before every example, so notifications fire by default. Tests that need "not production" re-stub it.
29
+ - `spec/dummy/config/initializers/exception_notification.rb` acts as a host app's initializer and switches the email notifier to `delivery_method: :test`.
30
+ - `module Bard::Rake` is defined in the spec helper on purpose: in real apps, bard-rake makes `Rake` inside `module Bard` resolve to `Bard::Rake`, which is why the engine uses `::Rake`. Keep top-level constants (`::Rails`, `::Rake`, `::ExceptionNotification`) fully qualified inside `module Bard`.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Micah Geisel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # bard-exception_notification
2
+
3
+ Shared [exception_notification](https://rubygems.org/gems/exception_notification) setup for BARD-managed Rails apps. Add the gem and every production app emails its exceptions the same way, from:
4
+
5
+ - web requests
6
+ - ActiveJob jobs, once retries are exhausted (discarded jobs send nothing)
7
+ - rake tasks, including unknown task names
8
+ - `ActionController::Live` actions that raise after the response has started streaming
9
+
10
+ It also adds `rake exception_notification:test` to check that notifications actually arrive from a deployed app.
11
+
12
+ ## Installation
13
+
14
+ ```ruby
15
+ gem "bard-exception_notification"
16
+ ```
17
+
18
+ Requires Ruby 3.2+, Rails 7.2+ and exception_notification 5.0+.
19
+
20
+ Add SMTP settings to the app's credentials:
21
+
22
+ ```yaml
23
+ exception_notification_smtp_settings:
24
+ address: smtp.example.com
25
+ port: 587
26
+ user_name: ...
27
+ password: ...
28
+ ```
29
+
30
+ ## What it configures
31
+
32
+ - Emails go to `micah@botandrose.com` from `errors@bardtracker.com`, with the app's directory name as the subject prefix, e.g. `[myapp] `.
33
+ - Notifications are sent only in production.
34
+ - Common noise is ignored: `ActionController::InvalidAuthenticityToken`, `ActionView::MissingTemplate`, `ActionController::BadRequest`, `ActionDispatch::Http::Parameters::ParseError`, `Mime::Type::InvalidMimeType`, `URI::InvalidComponentError` and `Addressable::URI::InvalidURIError`.
35
+ - Cookies, the `Authorization` header and the session are kept out of emails. They are added to `filter_parameters`, and the session section is left out.
36
+
37
+ ## Customizing
38
+
39
+ The defaults are applied before the app's own initializers, so an initializer can add to them or override them:
40
+
41
+ ```ruby
42
+ # config/initializers/exception_notification.rb
43
+ ExceptionNotification.configure do |config|
44
+ config.ignored_exceptions += %w[MyApp::ExpectedError]
45
+ config.add_notifier :email, Bard::ExceptionNotification.email_options.merge(exception_recipients: "team@example.com")
46
+ end
47
+ ```
48
+
49
+ ## Testing a deployed app
50
+
51
+ ```sh
52
+ bin/rake exception_notification:test
53
+ ```
54
+
55
+ This runs three checks in order:
56
+
57
+ - `exception_notification:test:web` sends a request through the app's middleware stack to a test page that raises, then fails unless a notification was delivered. The page is reachable only from this task, never over HTTP. The request uses the host from `Rails.application.default_url_options`, so that host must be allowed by `config.hosts`.
58
+ - `exception_notification:test:job` enqueues a job that raises. Its notification is sent once a worker runs the job and any retries configured by `ApplicationJob` are exhausted.
59
+ - `exception_notification:test:rake` raises from the task itself, so the task exits with a failure after sending its notification.
60
+
61
+ The web and rake checks raise if the notification can't be delivered, instead of only logging the failure.
62
+
63
+ ## Development
64
+
65
+ ```sh
66
+ bundle install
67
+ bundle exec rspec
68
+ bundle exec appraisal install && bundle exec appraisal rspec # every supported Rails version
69
+ ```
70
+
71
+ ## License
72
+
73
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bard
4
+ module ExceptionNotification
5
+ class TestsController < ActionController::Base
6
+ def show
7
+ raise TestError, Test.message("web")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bard
4
+ module ExceptionNotification
5
+ class TestJob < ::ApplicationJob
6
+ def perform(message)
7
+ raise TestError, message
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake", "~> 13.0"
6
+ gem "rspec", "~> 3.0"
7
+ gem "appraisal"
8
+ gem "rails", "~> 7.2.0"
9
+
10
+ gemspec path: "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake", "~> 13.0"
6
+ gem "rspec", "~> 3.0"
7
+ gem "appraisal"
8
+ gem "rails", "~> 8.0.0"
9
+
10
+ gemspec path: "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake", "~> 13.0"
6
+ gem "rspec", "~> 3.0"
7
+ gem "appraisal"
8
+ gem "rails", "~> 8.1.0"
9
+
10
+ gemspec path: "../"
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/engine"
4
+
5
+ module Bard
6
+ module ExceptionNotification
7
+ class Engine < ::Rails::Engine
8
+ # Before the app's own initializers, so they can adjust any of this with ExceptionNotification.configure.
9
+ initializer "bard-exception_notification.defaults", before: :load_config_initializers do |app|
10
+ app.config.filter_parameters += [
11
+ /\AHTTP_COOKIE\z/,
12
+ /\AHTTP_AUTHORIZATION\z/,
13
+ /\Arack\.request\.cookie_(hash|string)\z/,
14
+ /\Arack\.session\z/,
15
+ /\Aaction_dispatch\.request\.unsigned_session_cookie\z/,
16
+ ]
17
+
18
+ ::ExceptionNotification.configure do |config|
19
+ config.ignored_exceptions += %w[
20
+ ActionController::InvalidAuthenticityToken
21
+ ActionView::MissingTemplate
22
+ ActionController::BadRequest
23
+ ActionDispatch::Http::Parameters::ParseError
24
+ Mime::Type::InvalidMimeType
25
+ URI::InvalidComponentError
26
+ Addressable::URI::InvalidURIError
27
+ ]
28
+
29
+ config.ignore_if do |exception, options|
30
+ !::Rails.env.production?
31
+ end
32
+
33
+ config.add_notifier :email, ExceptionNotification.email_options
34
+ end
35
+ end
36
+
37
+ initializer "bard-exception_notification.active_job" do
38
+ ActiveSupport::Notifications.subscribe("perform.active_job") do |event|
39
+ exception = event.payload[:exception_object]
40
+ ExceptionNotification.notify_job_failure(exception, event.payload[:job]) if exception
41
+ end
42
+ end
43
+
44
+ initializer "bard-exception_notification.live" do
45
+ ActiveSupport.on_load(:action_controller_base) do
46
+ ActionController::Live.prepend LiveErrorNotification
47
+ end
48
+ end
49
+
50
+ initializer "bard-exception_notification.test_route" do |app|
51
+ app.routes.prepend do
52
+ get "/exception_notification/test",
53
+ to: "bard/exception_notification/tests#show",
54
+ as: nil,
55
+ constraints: ->(request) { request.env[Test::ENV_KEY] }
56
+ end
57
+ end
58
+
59
+ rake_tasks do
60
+ ::Rake::Application.prepend UnknownTaskNotification
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bard
4
+ module ExceptionNotification
5
+ class TestError < StandardError; end
6
+
7
+ module Test
8
+ # Set only in-process, so no HTTP request can reach the test page.
9
+ ENV_KEY = "bard.exception_notification.test"
10
+
11
+ extend self
12
+
13
+ def web
14
+ surface_delivery_failures!
15
+ env = ::Rack::MockRequest.env_for("https://#{host}/exception_notification/test", "HTTP_HOST" => host, ENV_KEY => true)
16
+ status, _headers, body = ::Rails.application.call(env)
17
+ body.close if body.respond_to?(:close)
18
+
19
+ raise "Expected the test page to raise #{TestError}, but it responded #{status}" unless env["action_controller.instance"].is_a?(TestsController)
20
+
21
+ exception = env["action_dispatch.exception"]
22
+ raise exception if exception && !exception.is_a?(TestError)
23
+ raise "#{TestError} from the test page was not sent; check ExceptionNotification's ignore rules" unless env["exception_notifier.delivered"]
24
+ puts "web: sent a #{TestError} notification from #{TestsController}"
25
+ end
26
+
27
+ def job
28
+ TestJob.perform_later(message("job"))
29
+ puts "job: enqueued #{TestJob}; its notification is sent once the job fails for good, after any retries ApplicationJob configures"
30
+ end
31
+
32
+ def rake
33
+ surface_delivery_failures!
34
+ raise TestError, message("rake")
35
+ end
36
+
37
+ def message(context)
38
+ "Exception notification test from #{context} at #{Time.now.utc.iso8601}"
39
+ end
40
+
41
+ private
42
+
43
+ # Otherwise ExceptionNotifier only logs a failed delivery, and the test looks like it passed.
44
+ def surface_delivery_failures!
45
+ ExceptionNotifier.testing_mode = true
46
+ end
47
+
48
+ def host
49
+ ::Rails.application.default_url_options.fetch(:host, "localhost")
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bard
4
+ module ExceptionNotification
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "exception_notification"
4
+ require "exception_notification/rails"
5
+ require "bard/exception_notification/version"
6
+ require "bard/exception_notification/test"
7
+ require "bard/exception_notification/engine"
8
+
9
+ module Bard
10
+ module ExceptionNotification
11
+ def self.email_options
12
+ {
13
+ email_prefix: "[#{::Rails.root.basename}] ",
14
+ exception_recipients: "micah@botandrose.com",
15
+ sender_address: "errors@bardtracker.com",
16
+ sections: %w[request environment backtrace], # not session, which can hold a login's persistence token
17
+ smtp_settings: ::Rails.application.credentials.exception_notification_smtp_settings,
18
+ }
19
+ end
20
+
21
+ def self.notify_job_failure(exception, job)
22
+ ExceptionNotifier.notify_exception(exception, data: {
23
+ job_class: job.class.name,
24
+ job_id: job.job_id,
25
+ arguments: job.arguments,
26
+ queue_name: job.queue_name,
27
+ executions: job.executions,
28
+ })
29
+ end
30
+
31
+ module LiveErrorNotification
32
+ def log_error(exception)
33
+ ExceptionNotifier.notify_exception(exception, env: request.env)
34
+ super
35
+ end
36
+ end
37
+
38
+ module UnknownTaskNotification
39
+ def invoke_task(task_string)
40
+ super
41
+ rescue RuntimeError => exception
42
+ ExceptionNotifier.notify_exception(exception) if exception.message.start_with?("Don't know how to build task")
43
+ raise
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bard/exception_notification"
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :exception_notification do
4
+ desc "Send a test exception notification from a web request, a job and a rake task"
5
+ task test: %w[test:web test:job test:rake]
6
+
7
+ namespace :test do
8
+ desc "Send a test exception notification from a web request"
9
+ task web: :environment do
10
+ Bard::ExceptionNotification::Test.web
11
+ end
12
+
13
+ desc "Enqueue a job that fails, to send a test exception notification from a job"
14
+ task job: :environment do
15
+ Bard::ExceptionNotification::Test.job
16
+ end
17
+
18
+ desc "Send a test exception notification from a rake task"
19
+ task rake: :environment do
20
+ Bard::ExceptionNotification::Test.rake
21
+ end
22
+ end
23
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bard-exception_notification
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Micah Geisel
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: exception_notification
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '5.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '5.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rails
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.2'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.2'
40
+ description: Configures exception_notification for web requests, ActiveJob, rake and
41
+ ActionController::Live, and adds rake exception_notification:test to check it end
42
+ to end
43
+ email:
44
+ - micah@botandrose.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".rspec"
50
+ - Appraisals
51
+ - CLAUDE.md
52
+ - LICENSE.txt
53
+ - README.md
54
+ - Rakefile
55
+ - app/controllers/bard/exception_notification/tests_controller.rb
56
+ - app/jobs/bard/exception_notification/test_job.rb
57
+ - gemfiles/rails_7.2.gemfile
58
+ - gemfiles/rails_8.0.gemfile
59
+ - gemfiles/rails_8.1.gemfile
60
+ - lib/bard-exception_notification.rb
61
+ - lib/bard/exception_notification.rb
62
+ - lib/bard/exception_notification/engine.rb
63
+ - lib/bard/exception_notification/test.rb
64
+ - lib/bard/exception_notification/version.rb
65
+ - lib/tasks/exception_notification.rake
66
+ homepage: https://github.com/botandrose/bard-exception_notification
67
+ licenses:
68
+ - MIT
69
+ metadata:
70
+ homepage_uri: https://github.com/botandrose/bard-exception_notification
71
+ source_code_uri: https://github.com/botandrose/bard-exception_notification
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 3.2.0
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubygems_version: 4.0.20
87
+ specification_version: 4
88
+ summary: Shared exception_notification setup for BARD-managed Rails projects
89
+ test_files: []