Skip to content
Open
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
166 changes: 159 additions & 7 deletions app/views/solid_errors/error_mailer/error_occurred.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,168 @@
<head>
<title>Solid Errors | <%= @error.severity_emoji %> <%= @error.exception_class %></title>
<%= render "layouts/solid_errors/style" %>
<style>
body {
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.email-header {
margin-bottom: 16px;
}
.email-header h1 {
font-size: 20px;
font-weight: 700;
margin: 0 0 8px 0;
}
.email-header h1 a {
color: #1f2937;
text-decoration: none;
}
.email-header h1 a:hover {
text-decoration: underline;
}
.email-link {
font-size: 14px;
color: #2563eb;
text-decoration: none;
margin-bottom: 12px;
display: inline-block;
}
.error-message {
background: #f3f4f6;
padding: 12px;
border-radius: 4px;
margin: 12px 0;
font-family: monospace;
font-size: 13px;
white-space: pre-wrap;
}
.info-table {
width: auto;
border-collapse: collapse;
margin: 12px 0;
font-size: 13px;
}
.info-table td {
padding: 4px 8px;
border-bottom: 1px solid #e5e7eb;
}
.info-table td:first-child {
font-weight: 600;
white-space: nowrap;
vertical-align: top;
}
.backtrace {
background: #1e293b;
color: #fff;
padding: 12px;
border-radius: 4px;
margin: 12px 0;
font-family: 'Monaco', 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
overflow-x: auto;
}
.backtrace-line {
margin: 2px 0;
}
.backtrace-highlight {
background: #374151;
margin-left: -12px;
margin-right: -12px;
padding-left: 12px;
padding-right: 12px;
}
.context-table {
width: auto;
border-collapse: collapse;
margin: 12px 0;
font-size: 13px;
}
.context-table th {
background: #f3f4f6;
padding: 6px 8px;
text-align: left;
font-weight: 600;
border: 1px solid #e5e7eb;
white-space: nowrap;
}
.context-table td {
padding: 6px 8px;
border: 1px solid #e5e7eb;
font-family: monospace;
font-size: 12px;
}
.context-table td:first-child {
white-space: nowrap;
}
.section-title {
font-weight: 700;
font-size: 14px;
margin: 16px 0 8px 0;
}
</style>
</head>
<body>
<%= 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}")) %>

<br>
<br>
<div class="email-header">
<h1>
<%= @error.severity_emoji %>
<%= @error.exception_class %>
from <em><%= @error.source %></em>
</h1>
<a href="<%= occurrence_url %>" class="email-link">View in Solid Errors &rarr;</a>
</div>

<%= render "solid_errors/occurrences/occurrence",
occurrence: @occurrence %>
<div class="error-message"><%= @error.message %></div>

<table class="info-table">
<tr>
<td>Severity</td>
<td><%= @error.severity_emoji %> <%= @error.severity %></td>
</tr>
<tr>
<td>Status</td>
<td><%= @error.status_emoji %> <%= @error.status %></td>
</tr>
<tr>
<td>Occurred at</td>
<td><%= @occurrence.created_at.strftime("%Y-%m-%d %H:%M:%S %Z") %></td>
</tr>
</table>

<% if @occurrence.context&.any? %>
<div class="section-title">Context</div>
<table class="context-table">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<% @occurrence.context.each do |key, value| %>
<tr>
<td><%= key %></td>
<td><%= value %></td>
</tr>
<% end %>
</tbody>
</table>
<% end %>

<div class="section-title">Backtrace</div>
<div class="backtrace">
<% @occurrence.parsed_backtrace.lines.each_with_index do |line, i| %>
<div class="backtrace-line <%= i.zero? ? 'backtrace-highlight' : '' %>">
<% if line.filtered_file %>
<span style="color: #9ca3af;"><%= File.dirname(line.filtered_file) %>/</span><span style="color: #60a5fa; font-weight: 500;"><%= File.basename(line.filtered_file) %></span>:<span style="color: #fff; font-weight: 500;"><%= line.filtered_number %></span> <span style="color: #9ca3af;">in</span> <span style="color: #34d399;"><%= line.filtered_method %></span>
<% else %>
<span style="color: #9ca3af;"><%= line.unparsed_line %></span>
<% end %>
</div>
<% end %>
</div>
</body>
</html>
31 changes: 31 additions & 0 deletions app/views/solid_errors/error_mailer/error_occurred.text.erb
Original file line number Diff line number Diff line change
@@ -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 %>
26 changes: 26 additions & 0 deletions lib/solid_errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
68 changes: 68 additions & 0 deletions test/dummy/config/environments/development.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions test/dummy/config/routes.rb
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
47 changes: 47 additions & 0 deletions test/mailers/previews/solid_errors/error_mailer_preview.rb
Original file line number Diff line number Diff line change
@@ -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" => "#<SyncMessageServiceChannelsJob>",
"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