rails_error_dashboard 0.5.10 → 0.5.11
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/README.md +34 -1
- data/app/controllers/rails_error_dashboard/errors_controller.rb +31 -0
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +12 -0
- data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +40 -0
- data/app/mailers/rails_error_dashboard/digest_mailer.rb +23 -0
- data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +2 -1
- data/app/views/layouts/rails_error_dashboard.html.erb +5 -0
- data/app/views/rails_error_dashboard/digest_mailer/digest_summary.html.erb +172 -0
- data/app/views/rails_error_dashboard/digest_mailer/digest_summary.text.erb +49 -0
- data/app/views/rails_error_dashboard/errors/_source_code.html.erb +20 -7
- data/app/views/rails_error_dashboard/errors/settings.html.erb +6 -2
- data/app/views/rails_error_dashboard/errors/show.html.erb +21 -0
- data/app/views/rails_error_dashboard/errors/user_impact.html.erb +172 -0
- data/config/routes.rb +3 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +6 -0
- data/lib/generators/rails_error_dashboard/install/templates/README +9 -18
- data/lib/rails_error_dashboard/configuration.rb +45 -0
- data/lib/rails_error_dashboard/middleware/rate_limiter.rb +16 -12
- data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +2 -1
- data/lib/rails_error_dashboard/queries/user_impact_summary.rb +93 -0
- data/lib/rails_error_dashboard/services/coverage_tracker.rb +139 -0
- data/lib/rails_error_dashboard/services/digest_builder.rb +158 -0
- data/lib/rails_error_dashboard/services/notification_helpers.rb +2 -1
- data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +2 -1
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +3 -0
- data/lib/tasks/error_dashboard.rake +23 -0
- metadata +33 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e91844bfccd6f88a89ad933cb4f100bb79e941f5b43a78155b6b21a46a58fffb
|
|
4
|
+
data.tar.gz: fffcde71a4ed1859cf70ef17ed7eca18531ac9d6be9f17fb3d1a9deceba4e0ca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab28a1843285439c9874a8d3552e5dceda81f4ced4cbc1d8d0ee6950a772758a1769f0a323bc207f2884c991c22d3938551b5b0a25f693ab19c6c5d0d0da23a7
|
|
7
|
+
data.tar.gz: a921a83a5fafceb78323dc3f2c48d559c722efbbf79b2708113f5c9d482ace9c2347700415aeee63e1af864d11a036bdc08ba43cfd860f2f28ea0621c8c7eb30
|
data/README.md
CHANGED
|
@@ -168,6 +168,28 @@ config.issue_tracker_token = ENV["RED_BOT_TOKEN"]
|
|
|
168
168
|
[Complete documentation →](docs/guides/CONFIGURATION.md)
|
|
169
169
|
</details>
|
|
170
170
|
|
|
171
|
+
<details>
|
|
172
|
+
<summary><strong>User Impact Scoring</strong></summary>
|
|
173
|
+
|
|
174
|
+
Dedicated `/errors/user_impact` page ranking errors by unique users affected — not occurrence count. An error hitting 1000 users once ranks higher than hitting 1 user 1000 times. Shows impact percentage (when `total_users_for_impact` is configured or auto-detected), severity badges, and per-error drill-down links.
|
|
175
|
+
|
|
176
|
+
No configuration needed — works automatically when errors have `user_id` (auto-detected via `CurrentAttributes` or `current_user`).
|
|
177
|
+
</details>
|
|
178
|
+
|
|
179
|
+
<details>
|
|
180
|
+
<summary><strong>Scheduled Digests</strong></summary>
|
|
181
|
+
|
|
182
|
+
Daily or weekly error summary emails — new errors, resolution rate, top errors by count, critical unresolved, and period-over-period comparison. HTML + text templates. Users schedule the job via SolidQueue, Sidekiq, or cron.
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
config.enable_scheduled_digests = true
|
|
186
|
+
config.digest_frequency = :daily # or :weekly
|
|
187
|
+
# config.digest_recipients = ["team@example.com"] # defaults to notification_email_recipients
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Schedule: `rails error_dashboard:send_digest PERIOD=daily`
|
|
191
|
+
</details>
|
|
192
|
+
|
|
171
193
|
<details>
|
|
172
194
|
<summary><strong>Release Tracking</strong></summary>
|
|
173
195
|
|
|
@@ -200,6 +222,17 @@ config.enable_git_blame = true
|
|
|
200
222
|
[Complete documentation →](docs/SOURCE_CODE_INTEGRATION.md)
|
|
201
223
|
</details>
|
|
202
224
|
|
|
225
|
+
<details>
|
|
226
|
+
<summary><strong>Code Path Coverage (Diagnostic Mode)</strong></summary>
|
|
227
|
+
|
|
228
|
+
Enable coverage via a dashboard button to see which production code paths were executed. Source code viewer overlays green checkmarks on executed lines and gray dots on unexecuted lines. Uses Ruby's `Coverage.setup(oneshot_lines: true)` — near-zero overhead, each line fires once. Zero overhead when off.
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
config.enable_coverage_tracking = true # shows Enable/Disable buttons on error detail page
|
|
232
|
+
config.enable_source_code_integration = true # required for source code viewer
|
|
233
|
+
```
|
|
234
|
+
</details>
|
|
235
|
+
|
|
203
236
|
<details>
|
|
204
237
|
<summary><strong>Error Replay — Copy as cURL / RSpec / LLM Markdown</strong></summary>
|
|
205
238
|
|
|
@@ -370,7 +403,7 @@ The installer guides you through optional feature selection — notifications, p
|
|
|
370
403
|
### 3. Visit your dashboard
|
|
371
404
|
|
|
372
405
|
```
|
|
373
|
-
http://localhost:3000/
|
|
406
|
+
http://localhost:3000/red
|
|
374
407
|
```
|
|
375
408
|
|
|
376
409
|
Default credentials: `gandalf` / `youshallnotpass`
|
|
@@ -286,6 +286,16 @@ module RailsErrorDashboard
|
|
|
286
286
|
@pagy, @releases = pagy(:offset, all_releases, limit: params[:per_page] || 25)
|
|
287
287
|
end
|
|
288
288
|
|
|
289
|
+
def user_impact
|
|
290
|
+
days = (params[:days] || 30).to_i
|
|
291
|
+
@days = days
|
|
292
|
+
result = Queries::UserImpactSummary.call(days, application_id: @current_application_id)
|
|
293
|
+
all_entries = result[:entries]
|
|
294
|
+
@summary = result[:summary]
|
|
295
|
+
|
|
296
|
+
@pagy, @entries = pagy(:offset, all_entries, limit: params[:per_page] || 25)
|
|
297
|
+
end
|
|
298
|
+
|
|
289
299
|
def deprecations
|
|
290
300
|
unless RailsErrorDashboard.configuration.enable_breadcrumbs
|
|
291
301
|
flash[:alert] = "Breadcrumbs are not enabled. Enable them in config/initializers/rails_error_dashboard.rb"
|
|
@@ -529,6 +539,27 @@ module RailsErrorDashboard
|
|
|
529
539
|
redirect_to diagnostic_dumps_errors_path
|
|
530
540
|
end
|
|
531
541
|
|
|
542
|
+
def enable_coverage
|
|
543
|
+
unless RailsErrorDashboard.configuration.enable_coverage_tracking
|
|
544
|
+
flash[:alert] = "Coverage tracking is not enabled in configuration."
|
|
545
|
+
redirect_to errors_path
|
|
546
|
+
return
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
if Services::CoverageTracker.enable!
|
|
550
|
+
flash[:notice] = "Code path coverage enabled. Reproduce the error, then view source code to see executed lines."
|
|
551
|
+
else
|
|
552
|
+
flash[:alert] = "Could not enable coverage. Requires Ruby 3.2+."
|
|
553
|
+
end
|
|
554
|
+
redirect_back fallback_location: errors_path
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def disable_coverage
|
|
558
|
+
Services::CoverageTracker.disable!
|
|
559
|
+
flash[:notice] = "Code path coverage disabled."
|
|
560
|
+
redirect_back fallback_location: errors_path
|
|
561
|
+
end
|
|
562
|
+
|
|
532
563
|
def settings
|
|
533
564
|
@config = RailsErrorDashboard.configuration
|
|
534
565
|
end
|
|
@@ -168,6 +168,18 @@ module RailsErrorDashboard
|
|
|
168
168
|
end
|
|
169
169
|
end
|
|
170
170
|
|
|
171
|
+
# Read coverage data for a file when coverage tracking is active
|
|
172
|
+
# @return [Hash{Integer => Boolean}] line_number => executed?, or nil
|
|
173
|
+
def read_coverage_for_file(file_path)
|
|
174
|
+
return nil unless RailsErrorDashboard.configuration.enable_coverage_tracking
|
|
175
|
+
return nil unless Services::CoverageTracker.active?
|
|
176
|
+
|
|
177
|
+
Services::CoverageTracker.peek(file_path)
|
|
178
|
+
rescue => e
|
|
179
|
+
Rails.logger.error("[RailsErrorDashboard] read_coverage_for_file failed: #{e.class}: #{e.message}")
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
|
|
171
183
|
# Read git blame for a backtrace frame
|
|
172
184
|
# Returns blame data hash or nil
|
|
173
185
|
def read_git_blame(frame)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
# Background job to send scheduled error digest emails.
|
|
5
|
+
# Schedule this job via your preferred scheduler (SolidQueue, Sidekiq, cron).
|
|
6
|
+
#
|
|
7
|
+
# @example Schedule daily digest
|
|
8
|
+
# RailsErrorDashboard::ScheduledDigestJob.perform_later(period: "daily")
|
|
9
|
+
#
|
|
10
|
+
# @example Schedule via rake
|
|
11
|
+
# rails error_dashboard:send_digest PERIOD=daily
|
|
12
|
+
class ScheduledDigestJob < ApplicationJob
|
|
13
|
+
queue_as :default
|
|
14
|
+
|
|
15
|
+
def perform(period: "daily", application_id: nil)
|
|
16
|
+
return unless RailsErrorDashboard.configuration.enable_scheduled_digests
|
|
17
|
+
|
|
18
|
+
recipients = effective_recipients
|
|
19
|
+
return if recipients.blank?
|
|
20
|
+
|
|
21
|
+
digest = Services::DigestBuilder.call(
|
|
22
|
+
period: period.to_sym,
|
|
23
|
+
application_id: application_id
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
DigestMailer.digest_summary(digest, recipients).deliver_now
|
|
27
|
+
rescue => e
|
|
28
|
+
Rails.logger.error("[RailsErrorDashboard] ScheduledDigestJob failed: #{e.class}: #{e.message}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def effective_recipients
|
|
34
|
+
config = RailsErrorDashboard.configuration
|
|
35
|
+
recipients = config.digest_recipients
|
|
36
|
+
recipients = config.notification_email_recipients if recipients.blank?
|
|
37
|
+
recipients.presence
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
class DigestMailer < ApplicationMailer
|
|
5
|
+
def digest_summary(digest, recipients)
|
|
6
|
+
@digest = digest
|
|
7
|
+
@dashboard_url = dashboard_base_url
|
|
8
|
+
|
|
9
|
+
mail(
|
|
10
|
+
to: recipients,
|
|
11
|
+
subject: "RED Digest — #{digest[:stats][:new_errors]} new errors (#{digest[:period_label]})"
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def dashboard_base_url
|
|
18
|
+
base = RailsErrorDashboard.configuration.dashboard_base_url || "http://localhost:3000"
|
|
19
|
+
mount_path = RailsErrorDashboard.configuration.engine_mount_path
|
|
20
|
+
"#{base}#{mount_path}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -16,7 +16,8 @@ module RailsErrorDashboard
|
|
|
16
16
|
|
|
17
17
|
def dashboard_url(error_log)
|
|
18
18
|
base_url = RailsErrorDashboard.configuration.dashboard_base_url || "http://localhost:3000"
|
|
19
|
-
|
|
19
|
+
mount_path = RailsErrorDashboard.configuration.engine_mount_path
|
|
20
|
+
"#{base_url}#{mount_path}/errors/#{error_log.id}"
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def truncate_subject(message)
|
|
@@ -1656,6 +1656,11 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
|
|
|
1656
1656
|
<i class="bi bi-rocket-takeoff"></i> Releases
|
|
1657
1657
|
<% end %>
|
|
1658
1658
|
</li>
|
|
1659
|
+
<li class="nav-item">
|
|
1660
|
+
<%= link_to user_impact_errors_path(nav_params), class: "nav-link #{request.path == user_impact_errors_path ? 'active' : ''}" do %>
|
|
1661
|
+
<i class="bi bi-people"></i> User Impact
|
|
1662
|
+
<% end %>
|
|
1663
|
+
</li>
|
|
1659
1664
|
<li class="nav-item">
|
|
1660
1665
|
<%= link_to settings_path(nav_params), class: "nav-link #{request.path == settings_path ? 'active' : ''}" do %>
|
|
1661
1666
|
<i class="bi bi-gear"></i> Settings
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 0; background-color: #f4f4f5; color: #18181b; }
|
|
8
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
9
|
+
.card { background: #ffffff; border-radius: 8px; padding: 24px; margin-bottom: 16px; border: 1px solid #e4e4e7; }
|
|
10
|
+
.header { text-align: center; padding: 24px 0; }
|
|
11
|
+
.header h1 { font-size: 24px; margin: 0 0 4px 0; color: #18181b; }
|
|
12
|
+
.header .subtitle { color: #71717a; font-size: 14px; }
|
|
13
|
+
.stat-grid { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; }
|
|
14
|
+
.stat-box { flex: 1 1 120px; text-align: center; padding: 16px 8px; background: #f9fafb; border-radius: 6px; min-width: 120px; }
|
|
15
|
+
.stat-value { font-size: 28px; font-weight: 700; line-height: 1.2; }
|
|
16
|
+
.stat-label { font-size: 12px; color: #71717a; margin-top: 4px; }
|
|
17
|
+
.text-danger { color: #ef4444; }
|
|
18
|
+
.text-success { color: #22c55e; }
|
|
19
|
+
.text-warning { color: #f59e0b; }
|
|
20
|
+
.text-muted { color: #71717a; }
|
|
21
|
+
.text-primary { color: #3b82f6; }
|
|
22
|
+
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; }
|
|
23
|
+
.badge-critical { background: #fef2f2; color: #ef4444; }
|
|
24
|
+
.badge-high { background: #fff7ed; color: #f97316; }
|
|
25
|
+
table { width: 100%; border-collapse: collapse; }
|
|
26
|
+
th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #e4e4e7; font-size: 14px; }
|
|
27
|
+
th { font-weight: 600; color: #71717a; font-size: 12px; text-transform: uppercase; }
|
|
28
|
+
a { color: #3b82f6; text-decoration: none; }
|
|
29
|
+
a:hover { text-decoration: underline; }
|
|
30
|
+
.comparison { display: flex; gap: 16px; justify-content: center; align-items: center; padding: 12px; background: #f9fafb; border-radius: 6px; }
|
|
31
|
+
.comparison .delta { font-size: 20px; font-weight: 700; }
|
|
32
|
+
.footer { text-align: center; padding: 20px; color: #a1a1aa; font-size: 12px; }
|
|
33
|
+
</style>
|
|
34
|
+
</head>
|
|
35
|
+
<body>
|
|
36
|
+
<div class="container">
|
|
37
|
+
<!-- Header -->
|
|
38
|
+
<div class="header">
|
|
39
|
+
<h1>RED Error Digest</h1>
|
|
40
|
+
<div class="subtitle"><%= @digest[:period_label] %> — <%= @digest[:generated_at].strftime('%B %d, %Y') %></div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- Summary Stats -->
|
|
44
|
+
<div class="card">
|
|
45
|
+
<div class="stat-grid">
|
|
46
|
+
<div class="stat-box">
|
|
47
|
+
<div class="stat-value text-primary"><%= @digest[:stats][:new_errors] %></div>
|
|
48
|
+
<div class="stat-label">New Errors</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="stat-box">
|
|
51
|
+
<div class="stat-value"><%= @digest[:stats][:total_occurrences] %></div>
|
|
52
|
+
<div class="stat-label">Occurrences</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="stat-box">
|
|
55
|
+
<div class="stat-value text-success"><%= @digest[:stats][:resolved] %></div>
|
|
56
|
+
<div class="stat-label">Resolved</div>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="stat-box">
|
|
59
|
+
<div class="stat-value text-warning"><%= @digest[:stats][:unresolved] %></div>
|
|
60
|
+
<div class="stat-label">Unresolved</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="stat-box">
|
|
63
|
+
<div class="stat-value text-danger"><%= @digest[:stats][:critical_high] %></div>
|
|
64
|
+
<div class="stat-label">Critical/High</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="stat-box">
|
|
67
|
+
<div class="stat-value"><%= @digest[:stats][:resolution_rate] %>%</div>
|
|
68
|
+
<div class="stat-label">Resolution Rate</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<!-- Comparison -->
|
|
74
|
+
<% if @digest[:comparison][:error_delta_percentage] %>
|
|
75
|
+
<div class="card">
|
|
76
|
+
<%
|
|
77
|
+
delta = @digest[:comparison][:error_delta]
|
|
78
|
+
pct = @digest[:comparison][:error_delta_percentage]
|
|
79
|
+
delta_class = delta > 0 ? "text-danger" : (delta < 0 ? "text-success" : "text-muted")
|
|
80
|
+
arrow = delta > 0 ? "▲" : (delta < 0 ? "▼" : "—")
|
|
81
|
+
%>
|
|
82
|
+
<div class="comparison">
|
|
83
|
+
<div>
|
|
84
|
+
<span class="text-muted">vs previous <%= @digest[:period] %>:</span>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="delta <%= delta_class %>">
|
|
87
|
+
<%== arrow %> <%= delta > 0 ? "+#{delta}" : delta %> errors
|
|
88
|
+
(<%= pct > 0 ? "+#{pct}" : pct %>%)
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
<% end %>
|
|
93
|
+
|
|
94
|
+
<!-- Critical / High Unresolved -->
|
|
95
|
+
<% if @digest[:critical_unresolved].any? %>
|
|
96
|
+
<div class="card">
|
|
97
|
+
<h3 style="margin: 0 0 12px 0; font-size: 16px;">Critical & High Unresolved</h3>
|
|
98
|
+
<table>
|
|
99
|
+
<thead>
|
|
100
|
+
<tr>
|
|
101
|
+
<th>Severity</th>
|
|
102
|
+
<th>Error</th>
|
|
103
|
+
<th></th>
|
|
104
|
+
</tr>
|
|
105
|
+
</thead>
|
|
106
|
+
<tbody>
|
|
107
|
+
<% @digest[:critical_unresolved].each do |error| %>
|
|
108
|
+
<tr>
|
|
109
|
+
<td>
|
|
110
|
+
<span class="badge badge-<%= error[:severity].to_s.downcase %>"><%= error[:severity].to_s.upcase %></span>
|
|
111
|
+
</td>
|
|
112
|
+
<td>
|
|
113
|
+
<strong><%= error[:error_type] %></strong><br>
|
|
114
|
+
<small class="text-muted"><%= error[:message] %></small>
|
|
115
|
+
</td>
|
|
116
|
+
<td>
|
|
117
|
+
<a href="<%= @dashboard_url %>/errors/<%= error[:id] %>">View</a>
|
|
118
|
+
</td>
|
|
119
|
+
</tr>
|
|
120
|
+
<% end %>
|
|
121
|
+
</tbody>
|
|
122
|
+
</table>
|
|
123
|
+
</div>
|
|
124
|
+
<% end %>
|
|
125
|
+
|
|
126
|
+
<!-- Top Errors -->
|
|
127
|
+
<% if @digest[:top_errors].any? %>
|
|
128
|
+
<div class="card">
|
|
129
|
+
<h3 style="margin: 0 0 12px 0; font-size: 16px;">Top Errors by Count</h3>
|
|
130
|
+
<table>
|
|
131
|
+
<thead>
|
|
132
|
+
<tr>
|
|
133
|
+
<th>Error Type</th>
|
|
134
|
+
<th>Count</th>
|
|
135
|
+
<th></th>
|
|
136
|
+
</tr>
|
|
137
|
+
</thead>
|
|
138
|
+
<tbody>
|
|
139
|
+
<% @digest[:top_errors].each do |error| %>
|
|
140
|
+
<tr>
|
|
141
|
+
<td>
|
|
142
|
+
<strong><%= error[:error_type] %></strong><br>
|
|
143
|
+
<small class="text-muted"><%= error[:message] %></small>
|
|
144
|
+
</td>
|
|
145
|
+
<td><%= error[:count] %></td>
|
|
146
|
+
<td>
|
|
147
|
+
<% if error[:id] %>
|
|
148
|
+
<a href="<%= @dashboard_url %>/errors/<%= error[:id] %>">View</a>
|
|
149
|
+
<% end %>
|
|
150
|
+
</td>
|
|
151
|
+
</tr>
|
|
152
|
+
<% end %>
|
|
153
|
+
</tbody>
|
|
154
|
+
</table>
|
|
155
|
+
</div>
|
|
156
|
+
<% end %>
|
|
157
|
+
|
|
158
|
+
<!-- CTA -->
|
|
159
|
+
<div class="card" style="text-align: center;">
|
|
160
|
+
<a href="<%= @dashboard_url %>" style="display: inline-block; padding: 10px 24px; background: #3b82f6; color: #ffffff; border-radius: 6px; font-weight: 600; text-decoration: none;">
|
|
161
|
+
Open Dashboard
|
|
162
|
+
</a>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<!-- Footer -->
|
|
166
|
+
<div class="footer">
|
|
167
|
+
Automated digest from <strong>RED</strong> (Rails Error Dashboard)<br>
|
|
168
|
+
<a href="<%= @dashboard_url %>/settings">Manage digest settings</a>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</body>
|
|
172
|
+
</html>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
==========================================
|
|
2
|
+
RED — Error Digest (<%= @digest[:period_label] %>)
|
|
3
|
+
==========================================
|
|
4
|
+
Generated: <%= @digest[:generated_at].strftime('%B %d, %Y at %I:%M %p %Z') %>
|
|
5
|
+
|
|
6
|
+
------------------------------------------
|
|
7
|
+
SUMMARY
|
|
8
|
+
------------------------------------------
|
|
9
|
+
New Errors: <%= @digest[:stats][:new_errors] %>
|
|
10
|
+
Total Occurrences: <%= @digest[:stats][:total_occurrences] %>
|
|
11
|
+
Resolved: <%= @digest[:stats][:resolved] %>
|
|
12
|
+
Unresolved: <%= @digest[:stats][:unresolved] %>
|
|
13
|
+
Critical/High: <%= @digest[:stats][:critical_high] %>
|
|
14
|
+
Resolution Rate: <%= @digest[:stats][:resolution_rate] %>%
|
|
15
|
+
|
|
16
|
+
<% if @digest[:comparison][:error_delta_percentage] %>
|
|
17
|
+
------------------------------------------
|
|
18
|
+
VS PREVIOUS PERIOD
|
|
19
|
+
------------------------------------------
|
|
20
|
+
Current: <%= @digest[:comparison][:current_count] %> errors
|
|
21
|
+
Previous: <%= @digest[:comparison][:previous_count] %> errors
|
|
22
|
+
Change: <%= @digest[:comparison][:error_delta] > 0 ? "+#{@digest[:comparison][:error_delta]}" : @digest[:comparison][:error_delta] %> (<%= @digest[:comparison][:error_delta_percentage] > 0 ? "+#{@digest[:comparison][:error_delta_percentage]}" : @digest[:comparison][:error_delta_percentage] %>%)
|
|
23
|
+
<% end %>
|
|
24
|
+
|
|
25
|
+
<% if @digest[:critical_unresolved].any? %>
|
|
26
|
+
------------------------------------------
|
|
27
|
+
CRITICAL / HIGH UNRESOLVED
|
|
28
|
+
------------------------------------------
|
|
29
|
+
<% @digest[:critical_unresolved].each do |error| %>
|
|
30
|
+
[<%= error[:severity].to_s.upcase %>] <%= error[:error_type] %>: <%= error[:message] %>
|
|
31
|
+
→ <%= @dashboard_url %>/errors/<%= error[:id] %>
|
|
32
|
+
<% end %>
|
|
33
|
+
<% end %>
|
|
34
|
+
|
|
35
|
+
<% if @digest[:top_errors].any? %>
|
|
36
|
+
------------------------------------------
|
|
37
|
+
TOP ERRORS BY COUNT
|
|
38
|
+
------------------------------------------
|
|
39
|
+
<% @digest[:top_errors].each_with_index do |error, i| %>
|
|
40
|
+
<%= i + 1 %>. <%= error[:error_type] %> (<%= error[:count] %>x): <%= error[:message] %>
|
|
41
|
+
→ <%= @dashboard_url %>/errors/<%= error[:id] %>
|
|
42
|
+
<% end %>
|
|
43
|
+
<% end %>
|
|
44
|
+
|
|
45
|
+
------------------------------------------
|
|
46
|
+
|
|
47
|
+
View the full dashboard:
|
|
48
|
+
<%= @dashboard_url %>
|
|
49
|
+
This is an automated digest from RED (Rails Error Dashboard).
|
|
@@ -55,14 +55,27 @@
|
|
|
55
55
|
|
|
56
56
|
<!-- Source code lines with syntax highlighting -->
|
|
57
57
|
<div class="source-code-content bg-white">
|
|
58
|
+
<%
|
|
59
|
+
# Find the error line number for highlighting
|
|
60
|
+
error_line_number = source_data[:lines].find { |l| l[:highlight] }&.dig(:number)
|
|
61
|
+
# Get start line number (first line in context)
|
|
62
|
+
start_line = source_data[:lines].first[:number]
|
|
63
|
+
# Coverage data (if active)
|
|
64
|
+
coverage_data = read_coverage_for_file(frame[:file_path])
|
|
65
|
+
%>
|
|
66
|
+
<% if coverage_data %>
|
|
67
|
+
<div class="px-3 py-1 bg-info bg-opacity-10 border-bottom">
|
|
68
|
+
<small class="text-info">
|
|
69
|
+
<i class="bi bi-broadcast"></i> Coverage active — <span class="text-success">green</span> = executed, <span class="text-secondary">gray</span> = not executed
|
|
70
|
+
</small>
|
|
71
|
+
</div>
|
|
72
|
+
<% end %>
|
|
58
73
|
<div class="code-block" style="max-height: 500px; overflow-y: auto; overflow-x: auto;">
|
|
59
|
-
<%
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
%>
|
|
65
|
-
<pre class="mb-0"><code class="language-<%= source_data[:language] || 'plaintext' %>" data-error-line="<%= error_line_number %>" data-start-line="<%= start_line %>"><%= source_data[:lines].map { |l| l[:content] }.join("\n") %></code></pre>
|
|
74
|
+
<% if coverage_data %>
|
|
75
|
+
<pre class="mb-0" style="position: relative;"><code class="language-<%= source_data[:language] || 'plaintext' %>" data-error-line="<%= error_line_number %>" data-start-line="<%= start_line %>"><% source_data[:lines].each_with_index do |l, i| %><% line_num = l[:number] %><% executed = coverage_data[line_num] %><% marker = executed == true ? "\u2713" : (executed == false ? "\u00b7" : " ") %><% color = executed == true ? "color: #22c55e;" : (executed == false ? "color: #9ca3af;" : "") %><span style="<%= color %> user-select: none;"><%= marker %></span> <%= l[:content] %><%= "\n" unless i == source_data[:lines].size - 1 %><% end %></code></pre>
|
|
76
|
+
<% else %>
|
|
77
|
+
<pre class="mb-0"><code class="language-<%= source_data[:language] || 'plaintext' %>" data-error-line="<%= error_line_number %>" data-start-line="<%= start_line %>"><%= source_data[:lines].map { |l| l[:content] }.join("\n") %></code></pre>
|
|
78
|
+
<% end %>
|
|
66
79
|
</div>
|
|
67
80
|
</div>
|
|
68
81
|
</div>
|
|
@@ -55,7 +55,10 @@
|
|
|
55
55
|
enable_pagerduty_notifications: { label: "PagerDuty", description: "Critical error escalation", type: :boolean },
|
|
56
56
|
pagerduty_integration_key: { label: "PagerDuty Integration Key", description: "PagerDuty API key", type: :string, show_if: :enable_pagerduty_notifications },
|
|
57
57
|
enable_webhook_notifications: { label: "Custom Webhooks", description: "Generic webhook endpoints", type: :boolean },
|
|
58
|
-
webhook_urls: { label: "Webhook URLs", description: "Custom webhook endpoints", type: :array, show_if: :enable_webhook_notifications }
|
|
58
|
+
webhook_urls: { label: "Webhook URLs", description: "Custom webhook endpoints", type: :array, show_if: :enable_webhook_notifications },
|
|
59
|
+
enable_scheduled_digests: { label: "Scheduled Digests", description: "Daily/weekly summary emails. Schedule via SolidQueue, Sidekiq, or cron", type: :boolean },
|
|
60
|
+
digest_frequency: { label: "Digest Frequency", description: "How often to send digest emails", type: :symbol, show_if: :enable_scheduled_digests },
|
|
61
|
+
digest_recipients: { label: "Digest Recipients", description: "Email addresses (falls back to notification recipients)", type: :array, show_if: :enable_scheduled_digests }
|
|
59
62
|
}
|
|
60
63
|
},
|
|
61
64
|
"Advanced Analytics Features" => {
|
|
@@ -133,7 +136,8 @@
|
|
|
133
136
|
enable_instance_variables: { label: "Instance Variable Capture", description: "Capture self's ivars at exception time", type: :boolean },
|
|
134
137
|
detect_swallowed_exceptions: { label: "Swallowed Exception Detection", description: "TracePoint(:raise) + (:rescue) — detect silently rescued exceptions (Ruby 3.3+)", type: :boolean },
|
|
135
138
|
enable_diagnostic_dump: { label: "Diagnostic Dump", description: "On-demand system state snapshots", type: :boolean },
|
|
136
|
-
enable_crash_capture: { label: "Process Crash Capture", description: "at_exit hook for unhandled crashes", type: :boolean }
|
|
139
|
+
enable_crash_capture: { label: "Process Crash Capture", description: "at_exit hook for unhandled crashes", type: :boolean },
|
|
140
|
+
enable_coverage_tracking: { label: "Code Path Coverage", description: "Diagnostic mode — enable via dashboard to see executed lines in source view (Ruby 3.2+)", type: :boolean }
|
|
137
141
|
}
|
|
138
142
|
},
|
|
139
143
|
"Event Tracking" => {
|
|
@@ -67,6 +67,27 @@
|
|
|
67
67
|
<div class="row g-4">
|
|
68
68
|
<!-- Error Information -->
|
|
69
69
|
<div class="col-md-8">
|
|
70
|
+
<% if RailsErrorDashboard.configuration.enable_coverage_tracking && RailsErrorDashboard.configuration.enable_source_code_integration %>
|
|
71
|
+
<div class="alert mb-3 py-2 d-flex justify-content-between align-items-center <%= RailsErrorDashboard::Services::CoverageTracker.active? ? 'alert-info' : 'alert-light border' %>">
|
|
72
|
+
<div>
|
|
73
|
+
<% if RailsErrorDashboard::Services::CoverageTracker.active? %>
|
|
74
|
+
<i class="bi bi-broadcast text-info"></i>
|
|
75
|
+
<strong>Coverage Active</strong> — expand source code to see executed lines
|
|
76
|
+
<% else %>
|
|
77
|
+
<i class="bi bi-code-square"></i>
|
|
78
|
+
<strong>Code Path Coverage</strong> — enable to see which lines were executed in production
|
|
79
|
+
<% end %>
|
|
80
|
+
</div>
|
|
81
|
+
<div>
|
|
82
|
+
<% if RailsErrorDashboard::Services::CoverageTracker.active? %>
|
|
83
|
+
<%= button_to "Disable Coverage", disable_coverage_errors_path, method: :post, class: "btn btn-sm btn-outline-secondary" %>
|
|
84
|
+
<% else %>
|
|
85
|
+
<%= button_to "Enable Coverage", enable_coverage_errors_path, method: :post, class: "btn btn-sm btn-info" %>
|
|
86
|
+
<% end %>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<% end %>
|
|
90
|
+
|
|
70
91
|
<%= render "error_info", error: @error %>
|
|
71
92
|
|
|
72
93
|
<%= render "local_variables", error: @error %>
|