From b67ac5bec59041f434632f5ce698a7910c95b421 Mon Sep 17 00:00:00 2001 From: Dan Loman Date: Wed, 7 Jan 2026 16:42:52 -0800 Subject: [PATCH 1/7] Update email markup for better support and tighter formatting --- .../error_mailer/error_occurred.html.erb | 166 +++++++++++++++++- 1 file changed, 159 insertions(+), 7 deletions(-) diff --git a/app/views/solid_errors/error_mailer/error_occurred.html.erb b/app/views/solid_errors/error_mailer/error_occurred.html.erb index 8e63065..c55fbd7 100644 --- a/app/views/solid_errors/error_mailer/error_occurred.html.erb +++ b/app/views/solid_errors/error_mailer/error_occurred.html.erb @@ -2,16 +2,168 @@ Solid Errors | <%= @error.severity_emoji %> <%= @error.exception_class %> <%= render "layouts/solid_errors/style" %> + - <%= render "solid_errors/errors/error", - error: @error, - show_actions: false %> + <% occurrence_url = solid_errors.error_url(@error, anchor: "occurrence_#{@occurrence.id}") %> -
-
+
+

+ <%= @error.severity_emoji %> + <%= @error.exception_class %> + from <%= @error.source %> +

+ +
- <%= render "solid_errors/occurrences/occurrence", - occurrence: @occurrence %> +
<%= @error.message %>
+ + + + + + + + + + + + + + +
Severity<%= @error.severity_emoji %> <%= @error.severity %>
Status<%= @error.status_emoji %> <%= @error.status %>
Occurred at<%= @occurrence.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %> (<%= time_ago_in_words(@occurrence.created_at) %> ago)
+ + <% if @occurrence.context&.any? %> +
Context
+ + + + + + + + + <% @occurrence.context.each do |key, value| %> + + + + + <% end %> + +
KeyValue
<%= key %><%= value %>
+ <% end %> + +
Backtrace
+
+ <% @occurrence.parsed_backtrace.lines.each_with_index do |line, i| %> +
+ <% if line.filtered_file %> +<%= File.dirname(line.filtered_file) %>/<%= File.basename(line.filtered_file) %>:<%= line.filtered_number %> in <%= line.filtered_method %> + <% else %> +<%= line.unparsed_line %> + <% end %> +
+ <% end %> +
From a3d30636ee38a735e876a12ccb58c0215212f935 Mon Sep 17 00:00:00 2001 From: Dan Loman Date: Wed, 7 Jan 2026 16:43:11 -0800 Subject: [PATCH 2/7] Add ability to preview mailer --- test/dummy/config/environments/development.rb | 68 +++++++++++++++++++ test/dummy/config/routes.rb | 3 + .../solid_errors/error_mailer_preview.rb | 47 +++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 test/dummy/config/environments/development.rb create mode 100644 test/mailers/previews/solid_errors/error_mailer_preview.rb diff --git a/test/dummy/config/environments/development.rb b/test/dummy/config/environments/development.rb new file mode 100644 index 0000000..c00bcf4 --- /dev/null +++ b/test/dummy/config/environments/development.rb @@ -0,0 +1,68 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing + config.server_timing = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + "Cache-Control" => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + # Configure Action Mailer for development + config.action_mailer.default_url_options = { host: "localhost", port: 3000 } + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb index 9f768e5..90d654c 100644 --- a/test/dummy/config/routes.rb +++ b/test/dummy/config/routes.rb @@ -1,4 +1,7 @@ Rails.application.routes.draw do + # Mount the SolidErrors engine + mount SolidErrors::Engine, at: "/solid_errors" + # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. diff --git a/test/mailers/previews/solid_errors/error_mailer_preview.rb b/test/mailers/previews/solid_errors/error_mailer_preview.rb new file mode 100644 index 0000000..f59eefb --- /dev/null +++ b/test/mailers/previews/solid_errors/error_mailer_preview.rb @@ -0,0 +1,47 @@ +module SolidErrors + class ErrorMailerPreview < ActionMailer::Preview + # Preview this email at http://localhost:3000/rails/mailers/solid_errors/error_mailer/error_occurred + def error_occurred + error = SolidErrors::Error.new( + id: 3, + exception_class: "Stack::Web::Api::Errors::MissingScope", + message: "missing_scope", + severity: "warning", + source: "application", + resolved_at: nil + ) + + occurrence = SolidErrors::Occurrence.new( + id: 123, + error: error, + backtrace: sample_backtrace, + context: { + "Job" => "#", + "Request ID" => "abc123-def456", + "User ID" => "42", + "Environment" => "production" + }, + created_at: 1.day.ago + ) + + SolidErrors::ErrorMailer.error_occurred(occurrence) + end + + private + + def sample_backtrace + [ + "[GEM_ROOT]/gems/slack-ruby-client-3.1.0/lib/slack/web/faraday/response/raise_error.rb:19:in `Slack::Web::Faraday::Response::RaiseError#on_complete'", + "/rails/app/services/sync_message_service.rb:17:in `SyncMessageService#call'", + "/rails/app/jobs/sync_message_service_channels_job.rb:5:in `SyncMessageServiceChannelsJob#perform'", + "[GEM_ROOT]/gems/activejob-7.0.4/lib/active_job/execution.rb:48:in `ActiveJob::Execution#perform_now'", + "[GEM_ROOT]/gems/activejob-7.0.4/lib/active_job/callbacks.rb:145:in `ActiveJob::Callbacks#perform_now'", + "/rails/app/controllers/webhooks_controller.rb:23:in `WebhooksController#create'", + "[GEM_ROOT]/gems/actionpack-7.0.4/lib/action_controller/metal/basic_implicit_render.rb:6:in `ActionController::Metal::BasicImplicitRender#send_action'", + "[GEM_ROOT]/gems/actionpack-7.0.4/lib/abstract_controller/base.rb:215:in `AbstractController::Base#process_action'", + "[GEM_ROOT]/gems/actionpack-7.0.4/lib/action_controller/metal/rendering.rb:53:in `ActionController::Metal::Rendering#process_action'", + "[GEM_ROOT]/gems/railties-7.0.4/lib/rails/engine.rb:531:in `Rails::Engine#call'" + ].join("\n") + end + end +end From 1e72f3d816d1ac3c708f92446a0ba8c2fff690e3 Mon Sep 17 00:00:00 2001 From: Dan Loman Date: Wed, 7 Jan 2026 16:57:53 -0800 Subject: [PATCH 3/7] Add text-based email --- .../error_mailer/error_occurred.text.erb | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/app/views/solid_errors/error_mailer/error_occurred.text.erb b/app/views/solid_errors/error_mailer/error_occurred.text.erb index e69de29..771f82b 100644 --- a/app/views/solid_errors/error_mailer/error_occurred.text.erb +++ b/app/views/solid_errors/error_mailer/error_occurred.text.erb @@ -0,0 +1,31 @@ +<% occurrence_url = solid_errors.error_url(@error, anchor: "occurrence_#{@occurrence.id}") %> +<%= @error.severity_emoji %> <%= @error.exception_class %> from <%= @error.source %> +================================================================================ + +<%= @error.message %> + +View in Solid Errors: <%= occurrence_url %> + +DETAILS +-------------------------------------------------------------------------------- +Severity: <%= @error.severity_emoji %> <%= @error.severity %> +Status: <%= @error.status_emoji %> <%= @error.status %> +Occurred at: <%= @occurrence.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %> (<%= time_ago_in_words(@occurrence.created_at) %> ago) + +<% if @occurrence.context&.any? %> +CONTEXT +-------------------------------------------------------------------------------- +<% @occurrence.context.each do |key, value| %> +<%= key.to_s.ljust(20) %> <%= value %> +<% end %> + +<% end %> +BACKTRACE +-------------------------------------------------------------------------------- +<% @occurrence.parsed_backtrace.lines.each_with_index do |line, i| %> +<% if line.filtered_file %> +<%= i.zero? ? '>>>' : ' ' %> <%= File.join(File.dirname(line.filtered_file), File.basename(line.filtered_file)) %>:<%= line.filtered_number %> in <%= line.filtered_method %> +<% else %> +<%= i.zero? ? '>>>' : ' ' %> <%= line.unparsed_line %> +<% end %> +<% end %> From 764e4242aa219116696de3f5d380516bef4d9081 Mon Sep 17 00:00:00 2001 From: Dan Loman Date: Wed, 7 Jan 2026 18:14:47 -0800 Subject: [PATCH 4/7] time_ago_in_words doesn't always work well for emails --- app/views/solid_errors/error_mailer/error_occurred.html.erb | 2 +- app/views/solid_errors/error_mailer/error_occurred.text.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/solid_errors/error_mailer/error_occurred.html.erb b/app/views/solid_errors/error_mailer/error_occurred.html.erb index c55fbd7..2806ff9 100644 --- a/app/views/solid_errors/error_mailer/error_occurred.html.erb +++ b/app/views/solid_errors/error_mailer/error_occurred.html.erb @@ -129,7 +129,7 @@ Occurred at - <%= @occurrence.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %> (<%= time_ago_in_words(@occurrence.created_at) %> ago) + <%= @occurrence.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %> diff --git a/app/views/solid_errors/error_mailer/error_occurred.text.erb b/app/views/solid_errors/error_mailer/error_occurred.text.erb index 771f82b..cd090be 100644 --- a/app/views/solid_errors/error_mailer/error_occurred.text.erb +++ b/app/views/solid_errors/error_mailer/error_occurred.text.erb @@ -10,7 +10,7 @@ DETAILS -------------------------------------------------------------------------------- Severity: <%= @error.severity_emoji %> <%= @error.severity %> Status: <%= @error.status_emoji %> <%= @error.status %> -Occurred at: <%= @occurrence.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %> (<%= time_ago_in_words(@occurrence.created_at) %> ago) +Occurred at: <%= @occurrence.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %> <% if @occurrence.context&.any? %> CONTEXT From 55b90668df5f4c5c5d7169852ec0d9a2a038755a Mon Sep 17 00:00:00 2001 From: Dan Loman Date: Wed, 7 Jan 2026 23:29:02 -0800 Subject: [PATCH 5/7] Add dynamic url_helper lookup in case Engine mounted in namespace --- .../error_mailer/error_occurred.html.erb | 2 +- .../error_mailer/error_occurred.text.erb | 2 +- lib/solid_errors.rb | 13 +++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/views/solid_errors/error_mailer/error_occurred.html.erb b/app/views/solid_errors/error_mailer/error_occurred.html.erb index 2806ff9..63ef303 100644 --- a/app/views/solid_errors/error_mailer/error_occurred.html.erb +++ b/app/views/solid_errors/error_mailer/error_occurred.html.erb @@ -105,7 +105,7 @@ - <% occurrence_url = solid_errors.error_url(@error, anchor: "occurrence_#{@occurrence.id}") %> + <% occurrence_url = public_send(SolidErrors.url_helper_name).error_url(@error, anchor: "occurrence_#{@occurrence.id}") %>