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..43b5261 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 = public_send(SolidErrors.url_helper_name).error_url(@error, **SolidErrors.url_options.merge(anchor: "occurrence_#{@occurrence.id}")) %> -
-
+
+

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

+ View in Solid Errors → +
- <%= 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") %>
+ + <% 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 %> +
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..c88f09f 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 = public_send(SolidErrors.url_helper_name).error_url(@error, **SolidErrors.url_options.merge(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") %> + +<% 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 %> diff --git a/lib/solid_errors.rb b/lib/solid_errors.rb index c2a3788..31085a3 100644 --- a/lib/solid_errors.rb +++ b/lib/solid_errors.rb @@ -15,6 +15,7 @@ module SolidErrors mattr_accessor :email_to mattr_accessor :email_subject_prefix mattr_accessor :destroy_after + mattr_writer :url_helper_name, :url_options class << self # use method instead of attr_accessor to ensure @@ -32,5 +33,30 @@ def password def send_emails? send_emails && email_to.present? end + + def url_helper_name + @url_helper_name ||= detect_url_helper_name + end + + def url_options + @url_options ||= detect_url_options + end + + private + + def find_solid_errors_route + @solid_errors_route ||= Rails.application.routes.routes.find do |r| + r.name&.to_s&.end_with?("solid_errors") + end + end + + def detect_url_helper_name + find_solid_errors_route&.name&.to_sym || :solid_errors + end + + def detect_url_options + subdomain = find_solid_errors_route&.constraints&.dig(:subdomain) + subdomain.is_a?(String) ? { subdomain: subdomain } : {} + end end end 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