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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +34 -1
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +31 -0
  4. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +12 -0
  5. data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +40 -0
  6. data/app/mailers/rails_error_dashboard/digest_mailer.rb +23 -0
  7. data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +2 -1
  8. data/app/views/layouts/rails_error_dashboard.html.erb +5 -0
  9. data/app/views/rails_error_dashboard/digest_mailer/digest_summary.html.erb +172 -0
  10. data/app/views/rails_error_dashboard/digest_mailer/digest_summary.text.erb +49 -0
  11. data/app/views/rails_error_dashboard/errors/_source_code.html.erb +20 -7
  12. data/app/views/rails_error_dashboard/errors/settings.html.erb +6 -2
  13. data/app/views/rails_error_dashboard/errors/show.html.erb +21 -0
  14. data/app/views/rails_error_dashboard/errors/user_impact.html.erb +172 -0
  15. data/config/routes.rb +3 -0
  16. data/lib/generators/rails_error_dashboard/install/install_generator.rb +6 -0
  17. data/lib/generators/rails_error_dashboard/install/templates/README +9 -18
  18. data/lib/rails_error_dashboard/configuration.rb +45 -0
  19. data/lib/rails_error_dashboard/middleware/rate_limiter.rb +16 -12
  20. data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +2 -1
  21. data/lib/rails_error_dashboard/queries/user_impact_summary.rb +93 -0
  22. data/lib/rails_error_dashboard/services/coverage_tracker.rb +139 -0
  23. data/lib/rails_error_dashboard/services/digest_builder.rb +158 -0
  24. data/lib/rails_error_dashboard/services/notification_helpers.rb +2 -1
  25. data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +2 -1
  26. data/lib/rails_error_dashboard/version.rb +1 -1
  27. data/lib/rails_error_dashboard.rb +3 -0
  28. data/lib/tasks/error_dashboard.rake +23 -0
  29. metadata +33 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3004ee5b49b622c064ceb8103c6abb2212f1a0cc269471a7281b49338213240
4
- data.tar.gz: a8e27db79b78052909acc3ce04ab4875b31148b6e50b631b46bfe67eb0562fd3
3
+ metadata.gz: e91844bfccd6f88a89ad933cb4f100bb79e941f5b43a78155b6b21a46a58fffb
4
+ data.tar.gz: fffcde71a4ed1859cf70ef17ed7eca18531ac9d6be9f17fb3d1a9deceba4e0ca
5
5
  SHA512:
6
- metadata.gz: c2dfe54a84c8a7049e6b51956d3512c6c18f8d37cabdd5f655bbdb046b6ed7ce2b287886f80a36a7bba85839090309eb81e823e29b5b766036f9b7ba726f7fef
7
- data.tar.gz: fdcf6aea6b7f568da4a525330e63de6121aa087926ffd8982c631ec90efa5282081d428c7fd9069b4ed923f31a1c1445483c443f4b845d4677648b10c4d56fc4
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/error_dashboard
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
- "#{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)
@@ -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 ? "&#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).
@@ -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>
@@ -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 %>