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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -7
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +41 -0
  4. data/app/controllers/rails_error_dashboard/webhooks_controller.rb +2 -1
  5. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +12 -0
  6. data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +40 -0
  7. data/app/mailers/rails_error_dashboard/digest_mailer.rb +23 -0
  8. data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +2 -1
  9. data/app/views/layouts/rails_error_dashboard.html.erb +10 -0
  10. data/app/views/rails_error_dashboard/digest_mailer/digest_summary.html.erb +172 -0
  11. data/app/views/rails_error_dashboard/digest_mailer/digest_summary.text.erb +49 -0
  12. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +14 -1
  13. data/app/views/rails_error_dashboard/errors/_source_code.html.erb +20 -7
  14. data/app/views/rails_error_dashboard/errors/releases.html.erb +284 -0
  15. data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +15 -0
  16. data/app/views/rails_error_dashboard/errors/settings.html.erb +37 -1
  17. data/app/views/rails_error_dashboard/errors/show.html.erb +21 -0
  18. data/app/views/rails_error_dashboard/errors/user_impact.html.erb +172 -0
  19. data/config/routes.rb +4 -0
  20. data/lib/generators/rails_error_dashboard/install/install_generator.rb +6 -0
  21. data/lib/generators/rails_error_dashboard/install/templates/README +9 -18
  22. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +32 -2
  23. data/lib/rails_error_dashboard/configuration.rb +55 -13
  24. data/lib/rails_error_dashboard/middleware/rate_limiter.rb +16 -12
  25. data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +2 -1
  26. data/lib/rails_error_dashboard/queries/release_timeline.rb +181 -0
  27. data/lib/rails_error_dashboard/queries/user_impact_summary.rb +93 -0
  28. data/lib/rails_error_dashboard/services/coverage_tracker.rb +139 -0
  29. data/lib/rails_error_dashboard/services/digest_builder.rb +158 -0
  30. data/lib/rails_error_dashboard/services/notification_helpers.rb +2 -1
  31. data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +6 -7
  32. data/lib/rails_error_dashboard/version.rb +1 -1
  33. data/lib/rails_error_dashboard.rb +4 -0
  34. data/lib/tasks/error_dashboard.rake +23 -0
  35. metadata +35 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 18faae83cf7bba23409732000c1cb230ba7ef067f23a454084445f754df4be0d
4
- data.tar.gz: ad82e112e173f8247cf31b214b040ba82e4035040bbdf14d8c53a5782db36bb1
3
+ metadata.gz: e91844bfccd6f88a89ad933cb4f100bb79e941f5b43a78155b6b21a46a58fffb
4
+ data.tar.gz: fffcde71a4ed1859cf70ef17ed7eca18531ac9d6be9f17fb3d1a9deceba4e0ca
5
5
  SHA512:
6
- metadata.gz: 0d0032aeb56ccc44a8c8966da338f75c37e71002534fd0bbf3e019cfe0582df37638e50863e8d4a77212731c1190de214d521521df2e986a23d36a575748d5d9
7
- data.tar.gz: 4862e358faa4a87f1091e33cf08c0e654a7e959b8af551a1223e3c9d098d383d503342aa8c2fb4d66f20c95fc4864276df631be6b93bcdd22ef50269516ef997
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
- Create, link, and manage issues directly from error detail pages. Supports GitHub, GitLab, and Codeberg/Gitea/Forgejo with provider auto-detection.
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
- - **Manual:** "Create Issue" button + "Link Existing Issue" URL input
156
- - **Auto-create:** On first occurrence and/or severity threshold — configurable
157
- - **Lifecycle sync:** Resolve → close issue, recur → reopen + comment (throttled)
158
- - **Two-way webhooks:** Issue closed/reopened on platform syncs to dashboard
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
- # Provider and repo auto-detected from git_repository_url
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/error_dashboard
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
- unless RailsErrorDashboard.configuration.enable_issue_webhooks
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
- "#{base_url}/error_dashboard/errors/#{error_log.id}"
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 ? "&#9650;" : (delta < 0 ? "&#9660;" : "&#8212;")
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 &amp; 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 (hidden when issue tracking enabled — platform is source of truth) -->
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
- # Find the error line number for highlighting
61
- error_line_number = source_data[:lines].find { |l| l[:highlight] }&.dig(:number)
62
- # Get start line number (first line in context)
63
- start_line = source_data[:lines].first[:number]
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>