rails_error_dashboard 0.5.9 → 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 +60 -7
- data/app/controllers/rails_error_dashboard/errors_controller.rb +41 -0
- data/app/controllers/rails_error_dashboard/webhooks_controller.rb +2 -1
- 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 +10 -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/_sidebar_metadata.html.erb +14 -1
- data/app/views/rails_error_dashboard/errors/_source_code.html.erb +20 -7
- data/app/views/rails_error_dashboard/errors/releases.html.erb +284 -0
- data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +15 -0
- data/app/views/rails_error_dashboard/errors/settings.html.erb +37 -1
- 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 +4 -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/generators/rails_error_dashboard/install/templates/initializer.rb +32 -2
- data/lib/rails_error_dashboard/configuration.rb +55 -13
- 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/release_timeline.rb +181 -0
- 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 +6 -7
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +4 -0
- data/lib/tasks/error_dashboard.rake +23 -0
- metadata +35 -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
|
@@ -150,23 +150,65 @@ config.enable_activestorage_tracking = true # requires enable_breadcrumbs = tru
|
|
|
150
150
|
<details>
|
|
151
151
|
<summary><strong>Issue Tracking — GitHub, GitLab, Codeberg</strong></summary>
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
One switch connects errors to your issue tracker. Platform becomes the source of truth — status, assignees, labels, and comments are mirrored live in the dashboard.
|
|
154
154
|
|
|
155
|
-
- **
|
|
156
|
-
- **Auto-create:**
|
|
157
|
-
- **Lifecycle sync:** Resolve → close
|
|
158
|
-
- **
|
|
155
|
+
- **Create & link:** "Create Issue" button or paste an existing URL
|
|
156
|
+
- **Auto-create:** New errors auto-create issues. Critical/high severity always creates
|
|
157
|
+
- **Lifecycle sync:** Resolve → close, recur → reopen + comment, all via background jobs
|
|
158
|
+
- **Platform mirror:** Issue state, assignees (with avatars), labels (with colors), and comments displayed in the dashboard. Workflow controls (Resolve, Assign, Priority) replaced by platform state
|
|
159
|
+
- **Two-way webhooks:** Issue closed/reopened on platform syncs back to dashboard
|
|
159
160
|
- **RED branding:** Issues show "Created by RED (Rails Error Dashboard)"
|
|
160
161
|
|
|
161
162
|
```ruby
|
|
162
163
|
config.enable_issue_tracking = true
|
|
163
164
|
config.issue_tracker_token = ENV["RED_BOT_TOKEN"]
|
|
164
|
-
#
|
|
165
|
+
# That's it — provider and repo auto-detected from git_repository_url
|
|
165
166
|
```
|
|
166
167
|
|
|
167
168
|
[Complete documentation →](docs/guides/CONFIGURATION.md)
|
|
168
169
|
</details>
|
|
169
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
|
+
|
|
193
|
+
<details>
|
|
194
|
+
<summary><strong>Release Tracking</strong></summary>
|
|
195
|
+
|
|
196
|
+
Dedicated Releases page at `/errors/releases` shows a timeline of all deploys/versions with health stats. Answers: "Did this deploy introduce new errors?" and "Is this release stable?"
|
|
197
|
+
|
|
198
|
+
- **Release timeline:** Every version seen, sorted newest-first, with error counts, unique types, and time range
|
|
199
|
+
- **"New in this release":** Errors whose fingerprint first appeared in each version — flagged with a red badge
|
|
200
|
+
- **Stability indicators:** Green (at or below average), yellow (1-2x), red (>2x average error rate)
|
|
201
|
+
- **Release comparison:** Delta and percentage change vs the previous release
|
|
202
|
+
- **Current release:** Highlighted card with live health stats
|
|
203
|
+
- **Zero config:** Works automatically when `app_version` or `git_sha` is set (via config, `APP_VERSION`, `GIT_SHA`, `HEROKU_SLUG_COMMIT`, or `RENDER_GIT_COMMIT` env vars)
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
config.app_version = "1.2.0" # or set APP_VERSION env var
|
|
207
|
+
config.git_sha = ENV["GIT_SHA"] # auto-detected on Heroku/Render
|
|
208
|
+
config.git_repository_url = "https://github.com/user/repo" # enables SHA links
|
|
209
|
+
```
|
|
210
|
+
</details>
|
|
211
|
+
|
|
170
212
|
<details>
|
|
171
213
|
<summary><strong>Source Code Integration + Git Blame</strong></summary>
|
|
172
214
|
|
|
@@ -180,6 +222,17 @@ config.enable_git_blame = true
|
|
|
180
222
|
[Complete documentation →](docs/SOURCE_CODE_INTEGRATION.md)
|
|
181
223
|
</details>
|
|
182
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
|
+
|
|
183
236
|
<details>
|
|
184
237
|
<summary><strong>Error Replay — Copy as cURL / RSpec / LLM Markdown</strong></summary>
|
|
185
238
|
|
|
@@ -350,7 +403,7 @@ The installer guides you through optional feature selection — notifications, p
|
|
|
350
403
|
### 3. Visit your dashboard
|
|
351
404
|
|
|
352
405
|
```
|
|
353
|
-
http://localhost:3000/
|
|
406
|
+
http://localhost:3000/red
|
|
354
407
|
```
|
|
355
408
|
|
|
356
409
|
Default credentials: `gandalf` / `youshallnotpass`
|
|
@@ -276,6 +276,26 @@ module RailsErrorDashboard
|
|
|
276
276
|
@platform_specific_errors = correlation.platform_specific_errors
|
|
277
277
|
end
|
|
278
278
|
|
|
279
|
+
def releases
|
|
280
|
+
days = (params[:days] || 30).to_i
|
|
281
|
+
@days = days
|
|
282
|
+
result = Queries::ReleaseTimeline.call(days, application_id: @current_application_id)
|
|
283
|
+
all_releases = result[:releases]
|
|
284
|
+
@summary = result[:summary]
|
|
285
|
+
|
|
286
|
+
@pagy, @releases = pagy(:offset, all_releases, limit: params[:per_page] || 25)
|
|
287
|
+
end
|
|
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
|
+
|
|
279
299
|
def deprecations
|
|
280
300
|
unless RailsErrorDashboard.configuration.enable_breadcrumbs
|
|
281
301
|
flash[:alert] = "Breadcrumbs are not enabled. Enable them in config/initializers/rails_error_dashboard.rb"
|
|
@@ -519,6 +539,27 @@ module RailsErrorDashboard
|
|
|
519
539
|
redirect_to diagnostic_dumps_errors_path
|
|
520
540
|
end
|
|
521
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
|
+
|
|
522
563
|
def settings
|
|
523
564
|
@config = RailsErrorDashboard.configuration
|
|
524
565
|
end
|
|
@@ -43,7 +43,8 @@ module RailsErrorDashboard
|
|
|
43
43
|
private
|
|
44
44
|
|
|
45
45
|
def verify_webhook_enabled
|
|
46
|
-
|
|
46
|
+
config = RailsErrorDashboard.configuration
|
|
47
|
+
unless config.enable_issue_tracking && config.issue_webhook_secret.present?
|
|
47
48
|
head :not_found
|
|
48
49
|
end
|
|
49
50
|
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)
|
|
@@ -1651,6 +1651,16 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
|
|
|
1651
1651
|
<i class="bi bi-diagram-3"></i> Correlation
|
|
1652
1652
|
<% end %>
|
|
1653
1653
|
</li>
|
|
1654
|
+
<li class="nav-item">
|
|
1655
|
+
<%= link_to releases_errors_path(nav_params), class: "nav-link #{request.path == releases_errors_path ? 'active' : ''}" do %>
|
|
1656
|
+
<i class="bi bi-rocket-takeoff"></i> Releases
|
|
1657
|
+
<% end %>
|
|
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>
|
|
1654
1664
|
<li class="nav-item">
|
|
1655
1665
|
<%= link_to settings_path(nav_params), class: "nav-link #{request.path == settings_path ? 'active' : ''}" do %>
|
|
1656
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).
|
|
@@ -71,8 +71,21 @@
|
|
|
71
71
|
<% end %>
|
|
72
72
|
</div>
|
|
73
73
|
|
|
74
|
+
<% if RailsErrorDashboard.configuration.enable_issue_tracking %>
|
|
75
|
+
<div class="mb-3">
|
|
76
|
+
<small class="text-muted" style="font-size: 0.78em;">
|
|
77
|
+
<i class="bi bi-info-circle me-1"></i>
|
|
78
|
+
Status, assignment, and priority are managed via your
|
|
79
|
+
<% if error.respond_to?(:external_issue_url) && error.external_issue_url.present? %>
|
|
80
|
+
<a href="<%= error.external_issue_url %>" target="_blank" rel="noopener">linked issue</a>.
|
|
81
|
+
<% else %>
|
|
82
|
+
issue tracker. Link an issue to see details.
|
|
83
|
+
<% end %>
|
|
84
|
+
</small>
|
|
85
|
+
</div>
|
|
86
|
+
<% end %>
|
|
74
87
|
<% unless RailsErrorDashboard.configuration.enable_issue_tracking %>
|
|
75
|
-
<!-- Workflow Status
|
|
88
|
+
<!-- Workflow Status -->
|
|
76
89
|
<div class="mb-3">
|
|
77
90
|
<small class="metadata-label d-block mb-1">Workflow Status</small>
|
|
78
91
|
<% if error.respond_to?(:status) %>
|
|
@@ -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>
|