error_radar 0.2.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +52 -0
- data/app/controllers/error_radar/dashboard_controller.rb +0 -22
- data/app/controllers/error_radar/errors_controller.rb +137 -0
- data/app/mailers/error_radar/error_mailer.rb +32 -0
- data/app/models/error_radar/error_log.rb +7 -1
- data/app/views/error_radar/dashboard/index.html.erb +9 -3
- data/app/views/error_radar/error_mailer/new_error.html.erb +82 -0
- data/app/views/error_radar/error_mailer/new_error.text.erb +21 -0
- data/app/views/error_radar/errors/index.html.erb +297 -0
- data/app/views/error_radar/errors/show.html.erb +165 -0
- data/app/views/layouts/error_radar/application.html.erb +19 -3
- data/config/routes.rb +6 -2
- data/lib/error_radar/configuration.rb +51 -3
- data/lib/error_radar/engine.rb +23 -0
- data/lib/error_radar/integrations/rake.rb +32 -0
- data/lib/error_radar/notifications/discord.rb +61 -0
- data/lib/error_radar/notifications/email.rb +18 -0
- data/lib/error_radar/notifications/slack.rb +76 -0
- data/lib/error_radar/notifications/webhook.rb +48 -0
- data/lib/error_radar/notifier.rb +91 -0
- data/lib/error_radar/tracking.rb +6 -2
- data/lib/error_radar/version.rb +1 -1
- data/lib/error_radar.rb +1 -0
- data/lib/generators/error_radar/install/templates/initializer.rb +35 -10
- metadata +14 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 486749dc9fc6c6e2cc617340f184cf8dbeb4196199c3017eb7bf6f667718fe74
|
|
4
|
+
data.tar.gz: 13da87d1772c71901b66fb4cd6b4d29a809b539b6fccaee5613defae97c0a04d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6a9fe7aa11d7668ca353e0c6331eac9a339c43263f93bc3650085bd72f2072968376006f7481661037c7d9f80fdca0057b8a28d9779a92050b931fb4eccd441f
|
|
7
|
+
data.tar.gz: f98150abaa5c7bd7695ddd38ba91d65db73bfe81ce7e739c9f91fac20c596b3303643fc282320b2366f9b1b6bd4da48c2bfca7bd8d182e1162c3a7786770384a
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,58 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.4.0] - 2026-07-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Slack notifications**: sends a Block Kit message with error details and a
|
|
9
|
+
deep-link button. Configure with `config.slack_webhook_url` and optional
|
|
10
|
+
`config.slack_channel`.
|
|
11
|
+
- **Discord notifications**: sends a rich embed to any Discord webhook.
|
|
12
|
+
Configure with `config.discord_webhook_url`.
|
|
13
|
+
- **Email notifications**: delivers an HTML + plain-text email via ActionMailer.
|
|
14
|
+
Configure `config.email_recipients` and `config.email_from`.
|
|
15
|
+
- **Generic webhook**: POSTs a JSON payload to any URL (PagerDuty, OpsGenie,
|
|
16
|
+
custom scripts). Configure with `config.webhook_urls = [url1, url2]`.
|
|
17
|
+
- **Custom callbacks**: `config.on_error { |log| ... }` for arbitrary alerting
|
|
18
|
+
logic (e.g. PagerDuty SDK, Telegram, SMS).
|
|
19
|
+
- **`notify_on` rule set**: controls when alerts fire — `:new_error` (default,
|
|
20
|
+
first occurrence per fingerprint), `:critical` (any critical severity, 1/hour
|
|
21
|
+
throttle), `:all` (every occurrence, 1/hour throttle per fingerprint).
|
|
22
|
+
- **In-memory throttle**: prevents notification storms for `:critical` and `:all`
|
|
23
|
+
rules — at most one alert per fingerprint per hour.
|
|
24
|
+
- **Deep-links in notifications**: set `config.app_host` to include a link to
|
|
25
|
+
the error detail page in every notification.
|
|
26
|
+
- **`ErrorLog#new_fingerprint?`**: transient predicate (not persisted) that is
|
|
27
|
+
`true` when the record was just created for the first time.
|
|
28
|
+
|
|
29
|
+
## [0.3.0] - 2026-07-03
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- **Zero-config auto-capture for ActiveJob**: any ActiveJob subclass (regardless
|
|
33
|
+
of queue adapter) now has exceptions captured automatically via
|
|
34
|
+
`ActiveSupport.on_load(:active_job)`. No need to include the module manually.
|
|
35
|
+
Disable with `config.install_active_job = false` (e.g. when using Sidekiq only).
|
|
36
|
+
- **Zero-config auto-capture for Rake tasks**: patches `Rake::Task#execute` so
|
|
37
|
+
every task failure is recorded automatically. Disable with
|
|
38
|
+
`config.install_rake = false`.
|
|
39
|
+
- **Full error list page** at `/errors` with pagination (50 per page),
|
|
40
|
+
full-text search (message / error class / source), filter by status /
|
|
41
|
+
severity / category / date range, sortable columns, and bulk actions
|
|
42
|
+
(resolve / ignore / reopen / delete).
|
|
43
|
+
- **Bulk actions**: select individual rows or all rows, then resolve, ignore,
|
|
44
|
+
reopen, or delete in one click.
|
|
45
|
+
- **Quick-action buttons** on the list (✓ resolve, ✕ ignore) without leaving
|
|
46
|
+
the page.
|
|
47
|
+
- **Improved error detail page**: context and backtrace displayed side-by-side,
|
|
48
|
+
inline resolution note field, delete button with confirmation.
|
|
49
|
+
- **Navigation bar** across all dashboard pages linking Dashboard ↔ All Errors.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
- `show` and `update_status` actions moved from `DashboardController` to the
|
|
53
|
+
new `ErrorsController`; route paths (`/errors/:id` etc.) are unchanged.
|
|
54
|
+
- Dashboard index h1 changed from "🛰 Error Radar" to "Dashboard" (branding
|
|
55
|
+
now lives in the shared nav bar).
|
|
56
|
+
|
|
5
57
|
## [0.2.0] - 2026-07-02
|
|
6
58
|
|
|
7
59
|
### Added
|
|
@@ -5,7 +5,6 @@ module ErrorRadar
|
|
|
5
5
|
# kanban board over ErrorLog statuses.
|
|
6
6
|
class DashboardController < ApplicationController
|
|
7
7
|
before_action :authenticate_request!
|
|
8
|
-
before_action :set_error, only: %i[show update_status]
|
|
9
8
|
|
|
10
9
|
KANBAN_LIMIT = 100
|
|
11
10
|
SEVERITY_ORDER = { 'critical' => 0, 'error' => 1, 'warning' => 2, 'info' => 3 }.freeze
|
|
@@ -47,29 +46,8 @@ module ErrorRadar
|
|
|
47
46
|
@external_links = build_external_links
|
|
48
47
|
end
|
|
49
48
|
|
|
50
|
-
def show; end
|
|
51
|
-
|
|
52
|
-
def update_status
|
|
53
|
-
new_status = params[:status].to_s
|
|
54
|
-
unless ErrorLog.statuses.key?(new_status)
|
|
55
|
-
return render json: { ok: false, error: 'invalid status' }, status: :unprocessable_entity
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
if new_status == 'resolved'
|
|
59
|
-
@error.resolve!(by: error_radar_current_user)
|
|
60
|
-
else
|
|
61
|
-
@error.update!(status: new_status, resolved_at: nil)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
render json: { ok: true, id: @error.id, status: @error.status }
|
|
65
|
-
end
|
|
66
|
-
|
|
67
49
|
private
|
|
68
50
|
|
|
69
|
-
def set_error
|
|
70
|
-
@error = ErrorLog.find(params[:id])
|
|
71
|
-
end
|
|
72
|
-
|
|
73
51
|
def build_external_links
|
|
74
52
|
links = {}
|
|
75
53
|
if defined?(::RailsAdmin)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ErrorRadar
|
|
4
|
+
class ErrorsController < ApplicationController
|
|
5
|
+
before_action :authenticate_request!
|
|
6
|
+
before_action :set_error, only: %i[show update_status destroy]
|
|
7
|
+
|
|
8
|
+
rescue_from ActiveRecord::RecordNotFound do
|
|
9
|
+
redirect_to errors_path, alert: 'Error not found.'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
PER_PAGE = 50
|
|
13
|
+
|
|
14
|
+
def index
|
|
15
|
+
scope = build_scope
|
|
16
|
+
@total_count = scope.count
|
|
17
|
+
@page = [params[:page].to_i, 1].max
|
|
18
|
+
@total_pages = [(@total_count.to_f / PER_PAGE).ceil, 1].max
|
|
19
|
+
@page = [@page, @total_pages].min if @total_pages > 0
|
|
20
|
+
@errors = scope.limit(PER_PAGE).offset((@page - 1) * PER_PAGE)
|
|
21
|
+
@filter_params = active_filter_params
|
|
22
|
+
@pages = pagination_pages(@page, @total_pages)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def show; end
|
|
26
|
+
|
|
27
|
+
def update_status
|
|
28
|
+
new_status = params[:status].to_s
|
|
29
|
+
unless ErrorLog.statuses.key?(new_status)
|
|
30
|
+
return render json: { ok: false, error: 'invalid status' }, status: :unprocessable_entity
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if new_status == 'resolved'
|
|
34
|
+
@error.resolve!(by: error_radar_current_user, note: params[:note].presence)
|
|
35
|
+
else
|
|
36
|
+
@error.update!(status: new_status, resolved_at: nil)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
render json: { ok: true, id: @error.id, status: @error.status }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def destroy
|
|
43
|
+
@error.destroy!
|
|
44
|
+
render json: { ok: true }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def bulk
|
|
48
|
+
ids = Array(params[:ids]).map(&:to_i).select(&:positive?)
|
|
49
|
+
action = params[:bulk_action].to_s
|
|
50
|
+
|
|
51
|
+
if ids.empty?
|
|
52
|
+
return render json: { ok: false, error: 'no ids selected' }, status: :unprocessable_entity
|
|
53
|
+
end
|
|
54
|
+
unless %w[resolve ignore reopen delete].include?(action)
|
|
55
|
+
return render json: { ok: false, error: 'unknown action' }, status: :unprocessable_entity
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
count = apply_bulk(ids, action)
|
|
59
|
+
render json: { ok: true, count: count, action: action }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def set_error
|
|
65
|
+
@error = ErrorLog.find(params[:id])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def build_scope
|
|
69
|
+
scope = ErrorLog.all
|
|
70
|
+
|
|
71
|
+
if params[:status].present? && ErrorLog.statuses.key?(params[:status])
|
|
72
|
+
scope = scope.where(status: params[:status])
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if params[:severity].present? && ErrorLog.severities.key?(params[:severity])
|
|
76
|
+
scope = scope.where(severity: params[:severity])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if params[:category].present? && ErrorLog.categories.key?(params[:category])
|
|
80
|
+
scope = scope.where(category: params[:category])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if params[:q].present?
|
|
84
|
+
q = "%#{params[:q].downcase}%"
|
|
85
|
+
scope = scope.where(
|
|
86
|
+
'lower(message) LIKE :q OR lower(error_class) LIKE :q OR lower(source) LIKE :q', q: q
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
from_date = params[:from].present? ? (Date.parse(params[:from]) rescue nil) : nil
|
|
91
|
+
to_date = params[:to].present? ? (Date.parse(params[:to]) rescue nil) : nil
|
|
92
|
+
scope = scope.where('last_seen_at >= ?', from_date) if from_date
|
|
93
|
+
scope = scope.where('last_seen_at <= ?', to_date.end_of_day) if to_date
|
|
94
|
+
|
|
95
|
+
sort_col = %w[last_seen_at first_seen_at occurrences].include?(params[:sort]) ? params[:sort] : 'last_seen_at'
|
|
96
|
+
sort_dir = params[:order] == 'asc' ? :asc : :desc
|
|
97
|
+
scope.order(sort_col => sort_dir)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def apply_bulk(ids, action)
|
|
101
|
+
case action
|
|
102
|
+
when 'resolve'
|
|
103
|
+
ErrorLog.where(id: ids).update_all(
|
|
104
|
+
status: ErrorLog.statuses[:resolved],
|
|
105
|
+
resolved_at: Time.current,
|
|
106
|
+
resolved_by: error_radar_current_user
|
|
107
|
+
)
|
|
108
|
+
when 'ignore'
|
|
109
|
+
ErrorLog.where(id: ids).update_all(status: ErrorLog.statuses[:ignored])
|
|
110
|
+
when 'reopen'
|
|
111
|
+
ErrorLog.where(id: ids).update_all(status: ErrorLog.statuses[:open], resolved_at: nil)
|
|
112
|
+
when 'delete'
|
|
113
|
+
ErrorLog.where(id: ids).delete_all
|
|
114
|
+
end
|
|
115
|
+
ids.size
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def active_filter_params
|
|
119
|
+
params.permit(:q, :status, :severity, :category, :from, :to, :sort, :order).to_h
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def pagination_pages(current, total)
|
|
123
|
+
return [] if total <= 1
|
|
124
|
+
return (1..total).to_a if total <= 9
|
|
125
|
+
|
|
126
|
+
visible = [1, total, *(current - 2..current + 2)]
|
|
127
|
+
.select { |p| p.between?(1, total) }
|
|
128
|
+
.sort.uniq
|
|
129
|
+
result = []
|
|
130
|
+
visible.each_with_index do |p, i|
|
|
131
|
+
result << '...' if i > 0 && p > visible[i - 1] + 1
|
|
132
|
+
result << p
|
|
133
|
+
end
|
|
134
|
+
result
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ErrorRadar
|
|
4
|
+
class ErrorMailer < ActionMailer::Base
|
|
5
|
+
layout false
|
|
6
|
+
|
|
7
|
+
def new_error(error_log)
|
|
8
|
+
@error = error_log
|
|
9
|
+
@is_new = error_log.new_fingerprint?
|
|
10
|
+
@url = build_url
|
|
11
|
+
@app_name = ErrorRadar::Notifier.app_name
|
|
12
|
+
|
|
13
|
+
prefix = @is_new ? 'New' : 'Critical'
|
|
14
|
+
subject = "[#{@app_name}] #{prefix} #{@error.severity}: #{@error.error_class}"
|
|
15
|
+
|
|
16
|
+
mail(
|
|
17
|
+
to: ErrorRadar.config.email_recipients,
|
|
18
|
+
from: ErrorRadar.config.email_from || 'noreply@localhost',
|
|
19
|
+
subject: subject
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def build_url
|
|
26
|
+
host = ErrorRadar.config.app_host.to_s.chomp('/')
|
|
27
|
+
return nil if host.empty?
|
|
28
|
+
|
|
29
|
+
"#{host}/error_radar/errors/#{@error.id}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -29,13 +29,18 @@ module ErrorRadar
|
|
|
29
29
|
# Record (or roll-up) an error. Idempotent per fingerprint: identical errors
|
|
30
30
|
# increment `occurrences` and bump `last_seen_at` instead of creating a new
|
|
31
31
|
# row. NEVER raises — logging must not break the calling code path.
|
|
32
|
+
def new_fingerprint?
|
|
33
|
+
@new_fingerprint || false
|
|
34
|
+
end
|
|
35
|
+
|
|
32
36
|
def self.record(category:, message:, severity: :error, error_class: nil, source: nil,
|
|
33
37
|
backtrace: nil, context: {}, http_status: nil, request_url: nil,
|
|
34
38
|
api_code: nil, api_subcode: nil, fingerprint: nil)
|
|
35
39
|
now = Time.current
|
|
36
40
|
fp = presence(fingerprint) || build_fingerprint(category: category, error_class: error_class, source: source, message: message)
|
|
37
41
|
|
|
38
|
-
log
|
|
42
|
+
log = find_or_initialize_by(fingerprint: fp)
|
|
43
|
+
new_fingerprint = !log.persisted?
|
|
39
44
|
|
|
40
45
|
if log.persisted?
|
|
41
46
|
log.occurrences += 1
|
|
@@ -54,6 +59,7 @@ module ErrorRadar
|
|
|
54
59
|
log.severity = severity if log.new_record? || severity_rank(severity) > severity_rank(log.severity)
|
|
55
60
|
log.last_seen_at = now
|
|
56
61
|
log.save!
|
|
62
|
+
log.instance_variable_set(:@new_fingerprint, new_fingerprint)
|
|
57
63
|
log
|
|
58
64
|
rescue StandardError => e
|
|
59
65
|
ErrorRadar::Tracking.warn_internal("ErrorLog.record failed: #{e.class}: #{e.message}")
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
status_labels = { 'open' => 'Open', 'in_progress' => 'In progress', 'resolved' => 'Resolved', 'ignored' => 'Ignored' }
|
|
4
4
|
%>
|
|
5
5
|
|
|
6
|
-
<
|
|
6
|
+
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:4px">
|
|
7
|
+
<h1>Dashboard</h1>
|
|
8
|
+
<%= link_to 'View All Errors →', errors_path, style: 'font-size:13px' %>
|
|
9
|
+
</div>
|
|
7
10
|
<div class="sub">
|
|
8
|
-
|
|
11
|
+
Error overview & kanban board
|
|
9
12
|
<% @external_links.each do |label, href| %>
|
|
10
13
|
· <%= link_to(label.to_s.humanize, href) %>
|
|
11
14
|
<% end %>
|
|
@@ -70,7 +73,10 @@
|
|
|
70
73
|
|
|
71
74
|
<div class="grid2">
|
|
72
75
|
<div class="panel">
|
|
73
|
-
<
|
|
76
|
+
<div style="display:flex;justify-content:space-between;align-items:baseline">
|
|
77
|
+
<h2 style="margin:0 0 12px">Top 10 errors (unresolved, by occurrences)</h2>
|
|
78
|
+
<%= link_to 'See all →', errors_path(status: 'open'), style: 'font-size:12px' %>
|
|
79
|
+
</div>
|
|
74
80
|
<table>
|
|
75
81
|
<thead><tr><th>Severity</th><th>Source</th><th>Message</th><th>Count</th><th>Last seen</th></tr></thead>
|
|
76
82
|
<tbody>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<style>
|
|
7
|
+
body{margin:0;padding:20px;background:#f4f6f8;font-family:-apple-system,"Segoe UI",Roboto,Arial,sans-serif;color:#1f2933}
|
|
8
|
+
.container{max-width:600px;margin:0 auto}
|
|
9
|
+
.header{background:#1f2933;color:#fff;padding:24px;border-radius:10px 10px 0 0}
|
|
10
|
+
.header h1{margin:0 0 6px;font-size:18px}
|
|
11
|
+
.header .meta{color:#9ca3af;font-size:12px}
|
|
12
|
+
.body{background:#fff;padding:24px;border-radius:0 0 10px 10px}
|
|
13
|
+
.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px}
|
|
14
|
+
.field .label{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:#9ca3af;margin-bottom:3px}
|
|
15
|
+
.field .value{font-size:14px;color:#1f2933}
|
|
16
|
+
.badge{display:inline-block;padding:3px 10px;border-radius:10px;color:#fff;font-size:12px;font-weight:600}
|
|
17
|
+
.msg-box{background:#f8fafc;border-left:4px solid #dc3545;padding:12px 14px;border-radius:0 8px 8px 0;font-family:ui-monospace,monospace;font-size:12px;white-space:pre-wrap;word-break:break-word;color:#374151;margin:16px 0}
|
|
18
|
+
.btn{display:inline-block;background:#2563eb;color:#fff!important;padding:11px 22px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:600;margin-top:8px}
|
|
19
|
+
.footer{margin-top:20px;font-size:11px;color:#9ca3af;text-align:center}
|
|
20
|
+
<%
|
|
21
|
+
sev_colors = { 'critical' => '#7b001c', 'error' => '#dc3545', 'warning' => '#fd7e14', 'info' => '#17a2b8' }
|
|
22
|
+
%>
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
<div class="container">
|
|
27
|
+
<div class="header">
|
|
28
|
+
<h1><%= @is_new ? '🆕 New error detected' : '🔁 Critical error recurring' %></h1>
|
|
29
|
+
<div class="meta"><%= @app_name %> · <%= @error.last_seen_at&.strftime('%Y-%m-%d %H:%M UTC') %></div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="body">
|
|
33
|
+
<div class="field" style="margin-bottom:20px">
|
|
34
|
+
<div class="label">Error class</div>
|
|
35
|
+
<div class="value" style="font-family:ui-monospace,monospace;font-size:17px;font-weight:700"><%= @error.error_class %></div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="grid">
|
|
39
|
+
<div class="field">
|
|
40
|
+
<div class="label">Source</div>
|
|
41
|
+
<div class="value"><%= @error.source || 'unknown' %></div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="field">
|
|
44
|
+
<div class="label">Category</div>
|
|
45
|
+
<div class="value"><%= @error.category.to_s.humanize %></div>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="field">
|
|
48
|
+
<div class="label">Severity</div>
|
|
49
|
+
<div class="value">
|
|
50
|
+
<span class="badge" style="background:<%= sev_colors[@error.severity] || '#6c757d' %>"><%= @error.severity %></span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="field">
|
|
54
|
+
<div class="label">Occurrences</div>
|
|
55
|
+
<div class="value" style="font-weight:700;font-size:16px"><%= @error.occurrences %></div>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="field">
|
|
58
|
+
<div class="label">First seen</div>
|
|
59
|
+
<div class="value"><%= @error.first_seen_at&.strftime('%Y-%m-%d %H:%M') %></div>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="field">
|
|
62
|
+
<div class="label">Last seen</div>
|
|
63
|
+
<div class="value"><%= @error.last_seen_at&.strftime('%Y-%m-%d %H:%M') %></div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div class="field">
|
|
68
|
+
<div class="label">Message</div>
|
|
69
|
+
<div class="msg-box"><%= @error.message.to_s.truncate(600) %></div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<% if @url %>
|
|
73
|
+
<a href="<%= @url %>" class="btn">View in Error Radar →</a>
|
|
74
|
+
<% end %>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="footer">
|
|
78
|
+
Sent by <strong>Error Radar</strong> · <%= @app_name %>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</body>
|
|
82
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[<%= @app_name %>] <%= @is_new ? 'New error' : 'Critical error recurring' %>: <%= @error.error_class %>
|
|
2
|
+
<%= '=' * 60 %>
|
|
3
|
+
|
|
4
|
+
Error class: <%= @error.error_class %>
|
|
5
|
+
Source: <%= @error.source || 'unknown' %>
|
|
6
|
+
Category: <%= @error.category %>
|
|
7
|
+
Severity: <%= @error.severity %>
|
|
8
|
+
Occurrences: <%= @error.occurrences %>
|
|
9
|
+
First seen: <%= @error.first_seen_at&.strftime('%Y-%m-%d %H:%M UTC') %>
|
|
10
|
+
Last seen: <%= @error.last_seen_at&.strftime('%Y-%m-%d %H:%M UTC') %>
|
|
11
|
+
|
|
12
|
+
Message:
|
|
13
|
+
<%= @error.message.to_s.truncate(1000) %>
|
|
14
|
+
<% if @url %>
|
|
15
|
+
|
|
16
|
+
View in Error Radar:
|
|
17
|
+
<%= @url %>
|
|
18
|
+
<% end %>
|
|
19
|
+
|
|
20
|
+
--
|
|
21
|
+
Sent by Error Radar · <%= @app_name %>
|