rails_error_dashboard 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -6
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +36 -0
  4. data/app/models/rails_error_dashboard/error_log.rb +16 -0
  5. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +3 -0
  6. data/app/views/rails_error_dashboard/errors/_modals.html.erb +35 -0
  7. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +29 -0
  8. data/app/views/rails_error_dashboard/errors/actioncable_health_summary.html.erb +132 -0
  9. data/app/views/rails_error_dashboard/errors/index.html.erb +7 -0
  10. data/app/views/rails_error_dashboard/errors/show.html.erb +6 -1
  11. data/config/routes.rb +3 -0
  12. data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +9 -0
  13. data/db/migrate/20260323000001_add_muted_to_error_logs.rb +14 -0
  14. data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +55 -0
  15. data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +55 -0
  16. data/lib/rails_error_dashboard/commands/log_error.rb +17 -17
  17. data/lib/rails_error_dashboard/commands/mute_error.rb +40 -0
  18. data/lib/rails_error_dashboard/commands/unmute_error.rb +30 -0
  19. data/lib/rails_error_dashboard/configuration.rb +13 -0
  20. data/lib/rails_error_dashboard/engine.rb +7 -0
  21. data/lib/rails_error_dashboard/queries/action_cable_summary.rb +96 -0
  22. data/lib/rails_error_dashboard/queries/errors_list.rb +11 -0
  23. data/lib/rails_error_dashboard/services/system_health_snapshot.rb +13 -0
  24. data/lib/rails_error_dashboard/subscribers/action_cable_subscriber.rb +107 -0
  25. data/lib/rails_error_dashboard/version.rb +1 -1
  26. data/lib/rails_error_dashboard.rb +6 -0
  27. metadata +10 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af7cf1b00931ba21cc10e507128ce90183cd2c0c707e47b16c2833ada7108fa7
4
- data.tar.gz: e59567ed413e58003c45336006239562eeffdfd11330515a1faef711b7143f46
3
+ metadata.gz: af41ac0baefaf436b68c2db7ac1598a15f409cb76efbbf2eb8fb2daa9ceaab92
4
+ data.tar.gz: 22073a8675b855c06622dc34ae649608af129f57f5c391c9562c20bd95db53ec
5
5
  SHA512:
6
- metadata.gz: 7446d9a051e2fa804b5090294217c0a678123fd8b1eb8720fff4b5ef8bf0bde48f87245333119b9b1c4101e036f3665b85b33b867ba8b2666182d6e2f7768cd7
7
- data.tar.gz: 9b7d7140f5acebbc5c15c218716a81e03309e7590880b9648e5ac9b35276ecccd210fbfbc4837ae8bd88a75f936b12873f81f170b9c85b9da8997d5641602126
6
+ metadata.gz: badaacf9fb78a5d93ee3d545d93877cc59c8685f8800567ad6f84979cec365baebd3f3193b905bbf73b67f99c70308c2480043bfb78c38f0a1b149637f1595c4
7
+ data.tar.gz: 548db884ddfcb084293083c3e8b787bdc50d0fd2128b8f68bdc315f578199ce9c36ef805a038657d437864055ee45e9f5bfc41ef6386be6e5b3c750be40ed238
data/README.md CHANGED
@@ -62,7 +62,7 @@ gem 'rails_error_dashboard'
62
62
 
63
63
  ### Core (Always Enabled)
64
64
 
65
- Error capture from controllers, jobs, and middleware. Beautiful Bootstrap 5 dashboard with dark/light mode, search, filtering, and real-time updates. Analytics with trend charts, severity breakdown, and spike detection. Workflow management with assignment, priority, snooze, comments, and batch operations. Security via HTTP Basic Auth or custom lambda (Devise, Warden, session-based). Exception cause chains, enriched HTTP context, custom fingerprinting, CurrentAttributes integration, auto-reopen on recurrence, and sensitive data filtering — all built in.
65
+ Error capture from controllers, jobs, and middleware. Beautiful Bootstrap 5 dashboard with dark/light mode, search, filtering, and real-time updates. Analytics with trend charts, severity breakdown, and spike detection. Workflow management with assignment, priority, snooze, mute/unmute (notification suppression), comments, and batch operations. Security via HTTP Basic Auth or custom lambda (Devise, Warden, session-based). Exception cause chains, enriched HTTP context, custom fingerprinting, CurrentAttributes integration, auto-reopen on recurrence, and sensitive data filtering — all built in.
66
66
 
67
67
  ### Optional Features
68
68
 
@@ -116,7 +116,7 @@ Requires breadcrumbs to be enabled.
116
116
  </details>
117
117
 
118
118
  <details>
119
- <summary><strong>Operational Health Panels — Jobs, Database, Cache</strong></summary>
119
+ <summary><strong>Operational Health Panels — Jobs, Database, Cache, ActionCable</strong></summary>
120
120
 
121
121
  **Job Health** — Auto-detects Sidekiq, SolidQueue, or GoodJob. Per-error table with adapter badge, failed count (color-coded), sorted worst-first.
122
122
 
@@ -130,6 +130,12 @@ Requires breadcrumbs to be enabled.
130
130
 
131
131
  ![Cache Health](docs/images/cache-health.png)
132
132
 
133
+ **ActionCable Health** — Track WebSocket channel actions, transmissions, subscription confirmations, and rejections. Dashboard page at `/errors/actioncable_health_summary` with channel breakdown sorted by rejections. System health snapshot captures live connection count and adapter. No error tracker surfaces this alongside HTTP errors.
134
+
135
+ ```ruby
136
+ config.enable_actioncable_tracking = true # requires enable_breadcrumbs = true
137
+ ```
138
+
133
139
  [Complete documentation →](docs/FEATURES.md#job-health-page)
134
140
  </details>
135
141
 
@@ -434,22 +440,24 @@ Available as open source under the [MIT License](https://opensource.org/licenses
434
440
 
435
441
  ## Acknowledgments
436
442
 
437
- Built with [Rails](https://rubyonrails.org/) · UI by [Bootstrap 5](https://getbootstrap.com/) · Charts by [Chart.js](https://www.chartjs.org/) · Pagination by [Pagy](https://github.com/ddnexus/pagy)
443
+ Built with [Rails](https://rubyonrails.org/) · UI by [Bootstrap 5](https://getbootstrap.com/) · Charts by [Chart.js](https://www.chartjs.org/) · Pagination by [Pagy](https://github.com/ddnexus/pagy) · Docs theme by [Jekyll VitePress Theme](https://jekyll-vitepress.dev/) by [@crmne](https://github.com/crmne)
438
444
 
439
445
  ## Contributors
440
446
 
441
447
  [![Contributors](https://contrib.rocks/image?repo=AnjanJ/rails_error_dashboard)](https://github.com/AnjanJ/rails_error_dashboard/graphs/contributors)
442
448
 
443
- Special thanks to [@bonniesimon](https://github.com/bonniesimon), [@gundestrup](https://github.com/gundestrup), [@midwire](https://github.com/midwire), and [@RafaelTurtle](https://github.com/RafaelTurtle). See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the full list.
449
+ Special thanks to [@bonniesimon](https://github.com/bonniesimon), [@gundestrup](https://github.com/gundestrup), [@midwire](https://github.com/midwire), [@RafaelTurtle](https://github.com/RafaelTurtle), and [@j4rs](https://github.com/j4rs). See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the full list.
444
450
 
445
451
  ---
446
452
 
447
453
  ## Support
448
454
 
449
- If this gem saves you some headaches (or some money on error tracking SaaS), consider [buying me a coffee](https://buymeacoffee.com/anjanj). It keeps the project going and lets me know people are finding it useful.
455
+ If this gem saves you some headaches (or some money on error tracking SaaS), consider buying me a coffee. It keeps the project going and lets me know people are finding it useful.
456
+
457
+ <a href="https://www.buymeacoffee.com/anjanj" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" width="200"></a>
450
458
 
451
459
  ---
452
460
 
453
- **Made with care by [Anjan](https://www.anjan.dev) for the Rails community**
461
+ **Made with ❤️ by [Anjan](https://anjan.dev)**
454
462
 
455
463
  *One Gem to rule them all, One Gem to find them, One Gem to bring them all, and in the dashboard bind them.*
@@ -19,6 +19,7 @@ module RailsErrorDashboard
19
19
  assignee_name
20
20
  priority_level
21
21
  hide_snoozed
22
+ hide_muted
22
23
  reopened
23
24
  sort_by
24
25
  sort_direction
@@ -121,6 +122,16 @@ module RailsErrorDashboard
121
122
  redirect_to error_path(@error)
122
123
  end
123
124
 
125
+ def mute
126
+ @error = Commands::MuteError.call(params[:id], muted_by: params[:muted_by], reason: params[:reason])
127
+ redirect_to error_path(@error)
128
+ end
129
+
130
+ def unmute
131
+ @error = Commands::UnmuteError.call(params[:id])
132
+ redirect_to error_path(@error)
133
+ end
134
+
124
135
  def update_status
125
136
  result = Commands::UpdateErrorStatus.call(params[:id], status: params[:status], comment: params[:comment])
126
137
  redirect_to error_path(result[:error])
@@ -200,6 +211,10 @@ module RailsErrorDashboard
200
211
  resolved_by_name: params[:resolved_by_name],
201
212
  resolution_comment: params[:resolution_comment]
202
213
  )
214
+ when "mute"
215
+ Commands::BatchMuteErrors.call(error_ids, muted_by: params[:muted_by])
216
+ when "unmute"
217
+ Commands::BatchUnmuteErrors.call(error_ids)
203
218
  when "delete"
204
219
  Commands::BatchDeleteErrors.call(error_ids)
205
220
  else
@@ -394,6 +409,27 @@ module RailsErrorDashboard
394
409
  @pagy, @events = pagy(:offset, all_events, limit: params[:per_page] || 25)
395
410
  end
396
411
 
412
+ def actioncable_health_summary
413
+ unless RailsErrorDashboard.configuration.enable_actioncable_tracking &&
414
+ RailsErrorDashboard.configuration.enable_breadcrumbs
415
+ flash[:alert] = "ActionCable tracking is not enabled. Enable enable_actioncable_tracking and enable_breadcrumbs in config/initializers/rails_error_dashboard.rb"
416
+ redirect_to errors_path
417
+ return
418
+ end
419
+
420
+ days = (params[:days] || 30).to_i
421
+ @days = days
422
+ result = Queries::ActionCableSummary.call(days, application_id: @current_application_id)
423
+ all_channels = result[:channels]
424
+
425
+ # Summary stats (computed before pagination)
426
+ @unique_channels = all_channels.size
427
+ @total_events = all_channels.sum { |c| c[:total_events] }
428
+ @total_rejections = all_channels.sum { |c| c[:rejection_count] }
429
+
430
+ @pagy, @channels = pagy(:offset, all_channels, limit: params[:per_page] || 25)
431
+ end
432
+
397
433
  def diagnostic_dumps
398
434
  unless RailsErrorDashboard.configuration.enable_diagnostic_dump
399
435
  flash[:alert] = "Diagnostic dumps are not enabled. Enable them in config/initializers/rails_error_dashboard.rb"
@@ -71,6 +71,8 @@ module RailsErrorDashboard
71
71
  scope :unassigned, -> { where(assigned_to: nil) }
72
72
  scope :by_assignee, ->(name) { where(assigned_to: name) }
73
73
  scope :by_priority, ->(level) { where(priority_level: level) }
74
+ scope :muted, -> { where(muted: true) }
75
+ scope :unmuted, -> { where(muted: false) }
74
76
 
75
77
  # Set defaults and tracking
76
78
  before_validation :set_defaults, on: :create
@@ -160,6 +162,20 @@ module RailsErrorDashboard
160
162
  snoozed_until.present? && snoozed_until >= Time.current
161
163
  end
162
164
 
165
+ # Mute query — checks column existence for backward compatibility
166
+ def muted?
167
+ self.class.column_names.include?("muted") && muted == true
168
+ end
169
+
170
+ # Mute/unmute convenience methods — delegate to Commands
171
+ def mute!(mute_data = {})
172
+ Commands::MuteError.call(id, **mute_data)
173
+ end
174
+
175
+ def unmute!
176
+ Commands::UnmuteError.call(id)
177
+ end
178
+
163
179
  # Priority methods
164
180
  def priority_label
165
181
  priority_data = PRIORITY_LEVELS[priority_level]
@@ -90,6 +90,9 @@
90
90
  <% if error.reopened? %>
91
91
  <i class="bi bi-arrow-counterclockwise text-warning ms-1" data-bs-toggle="tooltip" title="Reopened"></i>
92
92
  <% end %>
93
+ <% if error.respond_to?(:muted?) && error.muted? %>
94
+ <i class="bi bi-bell-slash text-secondary ms-1" data-bs-toggle="tooltip" title="Muted - notifications silenced"></i>
95
+ <% end %>
93
96
  </td>
94
97
  <td onclick="event.stopPropagation();">
95
98
  <%= link_to error_path(error), class: "btn btn-sm btn-outline-primary" do %>
@@ -137,3 +137,38 @@
137
137
  </div>
138
138
  </div>
139
139
  </div>
140
+
141
+ <!-- Mute Notifications Modal -->
142
+ <div class="modal fade" id="muteModal" tabindex="-1" aria-labelledby="muteModalLabel" aria-hidden="true">
143
+ <div class="modal-dialog">
144
+ <div class="modal-content">
145
+ <%= form_with url: mute_error_path(error), method: :post do |f| %>
146
+ <div class="modal-header">
147
+ <h5 class="modal-title" id="muteModalLabel">
148
+ <i class="bi bi-bell-slash"></i> Mute Notifications
149
+ </h5>
150
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
151
+ </div>
152
+ <div class="modal-body">
153
+ <p class="text-muted">
154
+ Muted errors still appear in the dashboard but will not trigger any notifications
155
+ (Slack, email, Discord, PagerDuty, webhooks).
156
+ </p>
157
+ <div class="mb-3">
158
+ <label for="muted_by" class="form-label">Your Name (Optional)</label>
159
+ <%= text_field_tag :muted_by, nil, class: "form-control", placeholder: "e.g., John Doe" %>
160
+ </div>
161
+ <div class="mb-3">
162
+ <label for="reason" class="form-label">Reason (Optional)</label>
163
+ <%= text_area_tag :reason, nil, class: "form-control", rows: 3, placeholder: "Why are you muting this error?" %>
164
+ <small class="text-muted">Reason will be added as a comment</small>
165
+ </div>
166
+ </div>
167
+ <div class="modal-footer">
168
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
169
+ <%= submit_tag "Mute Notifications", class: "btn btn-secondary", data: { action: "click->loading#click" } %>
170
+ </div>
171
+ <% end %>
172
+ </div>
173
+ </div>
174
+ </div>
@@ -172,6 +172,35 @@
172
172
  </div>
173
173
  <% end %>
174
174
 
175
+ <!-- Mute Notifications -->
176
+ <% if error.respond_to?(:muted?) %>
177
+ <div class="mb-3">
178
+ <small class="metadata-label d-block mb-1">Notifications</small>
179
+ <% if error.muted? %>
180
+ <div class="alert alert-secondary py-2 mb-2">
181
+ <i class="bi bi-bell-slash"></i>
182
+ <strong>Muted</strong><br>
183
+ <% if error.muted_by.present? %>
184
+ <small>By <%= error.muted_by %></small><br>
185
+ <% end %>
186
+ <% if error.muted_reason.present? %>
187
+ <small><em><%= error.muted_reason %></em></small><br>
188
+ <% end %>
189
+ <% if error.muted_at.present? %>
190
+ <small>Since <%= local_time(error.muted_at, format: :short) %></small>
191
+ <% end %>
192
+ </div>
193
+ <%= button_to unmute_error_path(error), method: :post, class: "btn btn-sm btn-outline-primary" do %>
194
+ <i class="bi bi-bell"></i> Unmute
195
+ <% end %>
196
+ <% else %>
197
+ <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#muteModal">
198
+ <i class="bi bi-bell-slash"></i> Mute
199
+ </button>
200
+ <% end %>
201
+ </div>
202
+ <% end %>
203
+
175
204
  <% if error.resolved? && error.resolved_by_name.present? %>
176
205
  <div class="mb-3">
177
206
  <small class="metadata-label d-block mb-1">Resolved By</small>
@@ -0,0 +1,132 @@
1
+ <% content_for :page_title, "ActionCable Health" %>
2
+
3
+ <div class="container-fluid py-4">
4
+ <div class="d-flex justify-content-between align-items-center mb-4">
5
+ <h1 class="h3 mb-0">
6
+ <i class="bi bi-broadcast me-2"></i>
7
+ ActionCable Health
8
+ </h1>
9
+
10
+ <div class="btn-group" role="group">
11
+ <%= link_to actioncable_health_summary_errors_path(days: 7), class: "btn btn-sm #{@days == 7 ? 'btn-primary' : 'btn-outline-primary'}" do %>
12
+ 7 Days
13
+ <% end %>
14
+ <%= link_to actioncable_health_summary_errors_path(days: 30), class: "btn btn-sm #{@days == 30 ? 'btn-primary' : 'btn-outline-primary'}" do %>
15
+ 30 Days
16
+ <% end %>
17
+ <%= link_to actioncable_health_summary_errors_path(days: 90), class: "btn btn-sm #{@days == 90 ? 'btn-primary' : 'btn-outline-primary'}" do %>
18
+ 90 Days
19
+ <% end %>
20
+ </div>
21
+ </div>
22
+
23
+ <% if @unique_channels == 0 %>
24
+ <div class="text-center py-5">
25
+ <i class="bi bi-broadcast display-1 text-success mb-3"></i>
26
+ <h4 class="text-muted">No ActionCable Events Found</h4>
27
+ <p class="text-muted">
28
+ No ActionCable channel actions, transmissions, or subscription events were detected in error breadcrumbs over the last <%= @days %> days.
29
+ </p>
30
+ <div class="card mx-auto" style="max-width: 500px;">
31
+ <div class="card-body text-start">
32
+ <h6>How ActionCable tracking works:</h6>
33
+ <ul class="mb-0">
34
+ <li>Breadcrumbs must be enabled (<code>enable_breadcrumbs = true</code>)</li>
35
+ <li>ActionCable tracking must be enabled (<code>enable_actioncable_tracking = true</code>)</li>
36
+ <li>ActionCable must be configured in your app</li>
37
+ <li>Channel actions, transmissions, and subscription events are captured as breadcrumbs during requests that produce errors</li>
38
+ </ul>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ <% else %>
43
+ <div class="row mb-4">
44
+ <div class="col-md-4">
45
+ <div class="card text-center">
46
+ <div class="card-body">
47
+ <div class="display-6 text-primary"><%= @unique_channels %></div>
48
+ <small class="text-muted">Active Channels</small>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ <div class="col-md-4">
53
+ <div class="card text-center">
54
+ <div class="card-body">
55
+ <div class="display-6 text-info"><%= @total_events %></div>
56
+ <small class="text-muted">Total Events</small>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ <div class="col-md-4">
61
+ <div class="card text-center">
62
+ <div class="card-body">
63
+ <div class="display-6 <%= @total_rejections > 0 ? 'text-danger' : 'text-success' %>"><%= @total_rejections %></div>
64
+ <small class="text-muted">Subscription Rejections</small>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ <div class="card mb-4">
71
+ <div class="card-header bg-white d-flex justify-content-between align-items-center">
72
+ <h5 class="mb-0">
73
+ <i class="bi bi-broadcast text-primary me-2"></i>
74
+ ActionCable Events by Channel
75
+ <span class="badge bg-primary"><%= @unique_channels %></span>
76
+ </h5>
77
+ <small class="text-muted"><%== @pagy.info_tag %></small>
78
+ </div>
79
+ <div class="card-body p-0">
80
+ <div class="table-responsive">
81
+ <table class="table table-hover mb-0">
82
+ <thead class="table-light">
83
+ <tr>
84
+ <th>Channel</th>
85
+ <th width="100">Actions</th>
86
+ <th width="120">Transmissions</th>
87
+ <th width="120">Subscriptions</th>
88
+ <th width="100">Rejections</th>
89
+ <th width="80">Errors</th>
90
+ <th width="140">Last Seen</th>
91
+ </tr>
92
+ </thead>
93
+ <tbody>
94
+ <% @channels.each do |channel| %>
95
+ <tr>
96
+ <td><code><%= channel[:channel] %></code></td>
97
+ <td><%= channel[:perform_count] %></td>
98
+ <td><%= channel[:transmit_count] %></td>
99
+ <td><span class="text-success"><%= channel[:subscription_count] %></span></td>
100
+ <td>
101
+ <% if channel[:rejection_count] > 0 %>
102
+ <span class="badge bg-danger"><%= channel[:rejection_count] %></span>
103
+ <% else %>
104
+ <span class="text-muted">0</span>
105
+ <% end %>
106
+ </td>
107
+ <td><%= channel[:error_count] %></td>
108
+ <td><%= local_time_ago(channel[:last_seen]) %></td>
109
+ </tr>
110
+ <% end %>
111
+ </tbody>
112
+ </table>
113
+ </div>
114
+ </div>
115
+ <div class="card-footer bg-white border-top d-flex justify-content-between align-items-center">
116
+ <div>
117
+ <small class="text-muted">
118
+ <i class="bi bi-lightbulb text-warning"></i> ActionCable events are captured when they coincide with errors. High rejection counts may indicate authentication or authorization issues.
119
+ </small>
120
+ <small class="ms-3">
121
+ <a href="https://guides.rubyonrails.org/action_cable_overview.html" target="_blank" rel="noopener" class="text-decoration-none">
122
+ <i class="bi bi-book"></i> ActionCable Guide <i class="bi bi-box-arrow-up-right" style="font-size: 0.7em;"></i>
123
+ </a>
124
+ </small>
125
+ </div>
126
+ <div>
127
+ <%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ <% end %>
132
+ </div>
@@ -360,6 +360,13 @@
360
360
  <%= label_tag :hide_snoozed, "Hide snoozed", class: "form-check-label" %>
361
361
  </div>
362
362
  </div>
363
+
364
+ <div class="col-auto">
365
+ <div class="form-check">
366
+ <%= check_box_tag :hide_muted, "1", params[:hide_muted] == "1", class: "form-check-input" %>
367
+ <%= label_tag :hide_muted, "Hide muted", class: "form-check-label" %>
368
+ </div>
369
+ </div>
363
370
  </div>
364
371
  </div>
365
372
 
@@ -28,10 +28,15 @@
28
28
  <% end %>
29
29
  </h2>
30
30
  </div>
31
- <div class="d-flex gap-2">
31
+ <div class="d-flex gap-2 align-items-center">
32
32
  <button type="button" class="btn btn-outline-secondary" onclick="downloadErrorJSON(event)" title="Download error details as JSON">
33
33
  <i class="bi bi-download"></i> Export JSON
34
34
  </button>
35
+ <% if @error.respond_to?(:muted?) && @error.muted? %>
36
+ <button type="button" class="btn btn-secondary" disabled>
37
+ <i class="bi bi-bell-slash"></i> Muted
38
+ </button>
39
+ <% end %>
35
40
  <% if @error.resolved? %>
36
41
  <span class="badge bg-success fs-6">
37
42
  <i class="bi bi-check-circle"></i> Resolved
data/config/routes.rb CHANGED
@@ -15,6 +15,8 @@ RailsErrorDashboard::Engine.routes.draw do
15
15
  post :update_priority
16
16
  post :snooze
17
17
  post :unsnooze
18
+ post :mute
19
+ post :unmute
18
20
  post :update_status
19
21
  post :add_comment
20
22
  end
@@ -29,6 +31,7 @@ RailsErrorDashboard::Engine.routes.draw do
29
31
  get :database_health_summary
30
32
  get :swallowed_exceptions
31
33
  get :rack_attack_summary
34
+ get :actioncable_health_summary
32
35
  get :diagnostic_dumps
33
36
  post :create_diagnostic_dump
34
37
  post :batch_action
@@ -95,6 +95,12 @@ class CreateRailsErrorDashboardCompleteSchema < ActiveRecord::Migration[7.0]
95
95
  # Instance variable capture (from 20260306000002)
96
96
  t.text :instance_variables
97
97
 
98
+ # Mute notifications (from 20260323000001)
99
+ t.boolean :muted, default: false, null: false
100
+ t.datetime :muted_at
101
+ t.string :muted_by
102
+ t.string :muted_reason
103
+
98
104
  t.timestamps
99
105
  end
100
106
 
@@ -133,6 +139,9 @@ class CreateRailsErrorDashboardCompleteSchema < ActiveRecord::Migration[7.0]
133
139
  add_index :rails_error_dashboard_error_logs, [ :application_id, :occurred_at ], name: "index_error_logs_on_app_occurred"
134
140
  add_index :rails_error_dashboard_error_logs, [ :application_id, :resolved ], name: "index_error_logs_on_app_resolved"
135
141
 
142
+ # Mute index (from 20260323000001)
143
+ add_index :rails_error_dashboard_error_logs, :muted
144
+
136
145
  # Workflow indexes (from 20251229111223)
137
146
  add_index :rails_error_dashboard_error_logs, [ :assigned_to, :status, :occurred_at ], name: "index_error_logs_on_assignment_workflow"
138
147
  add_index :rails_error_dashboard_error_logs, [ :priority_level, :resolved, :occurred_at ], name: "index_error_logs_on_priority_resolution"
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddMutedToErrorLogs < ActiveRecord::Migration[7.0]
4
+ def change
5
+ return if column_exists?(:rails_error_dashboard_error_logs, :muted)
6
+
7
+ add_column :rails_error_dashboard_error_logs, :muted, :boolean, default: false, null: false
8
+ add_column :rails_error_dashboard_error_logs, :muted_at, :datetime
9
+ add_column :rails_error_dashboard_error_logs, :muted_by, :string
10
+ add_column :rails_error_dashboard_error_logs, :muted_reason, :string
11
+
12
+ add_index :rails_error_dashboard_error_logs, :muted
13
+ end
14
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Commands
5
+ # Command: Mute multiple errors at once
6
+ class BatchMuteErrors
7
+ def self.call(error_ids, muted_by: nil)
8
+ new(error_ids, muted_by).call
9
+ end
10
+
11
+ def initialize(error_ids, muted_by = nil)
12
+ @error_ids = Array(error_ids).compact
13
+ @muted_by = muted_by
14
+ end
15
+
16
+ def call
17
+ return { success: false, count: 0, errors: [ "No error IDs provided" ] } if @error_ids.empty?
18
+
19
+ errors = ErrorLog.where(id: @error_ids)
20
+
21
+ muted_count = 0
22
+ failed_ids = []
23
+ muted_errors = []
24
+
25
+ errors.each do |error|
26
+ begin
27
+ error.update!(
28
+ muted: true,
29
+ muted_at: Time.current,
30
+ muted_by: @muted_by
31
+ )
32
+ muted_count += 1
33
+ muted_errors << error
34
+ rescue => e
35
+ failed_ids << error.id
36
+ RailsErrorDashboard::Logger.error("Failed to mute error #{error.id}: #{e.message}")
37
+ end
38
+ end
39
+
40
+ PluginRegistry.dispatch(:on_errors_batch_muted, muted_errors) if muted_errors.any?
41
+
42
+ {
43
+ success: failed_ids.empty?,
44
+ count: muted_count,
45
+ total: @error_ids.size,
46
+ failed_ids: failed_ids,
47
+ errors: failed_ids.empty? ? [] : [ "Failed to mute #{failed_ids.size} error(s)" ]
48
+ }
49
+ rescue => e
50
+ RailsErrorDashboard::Logger.error("Batch mute failed: #{e.message}")
51
+ { success: false, count: 0, total: @error_ids.size, errors: [ e.message ] }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Commands
5
+ # Command: Unmute multiple errors at once
6
+ class BatchUnmuteErrors
7
+ def self.call(error_ids)
8
+ new(error_ids).call
9
+ end
10
+
11
+ def initialize(error_ids)
12
+ @error_ids = Array(error_ids).compact
13
+ end
14
+
15
+ def call
16
+ return { success: false, count: 0, errors: [ "No error IDs provided" ] } if @error_ids.empty?
17
+
18
+ errors = ErrorLog.where(id: @error_ids)
19
+
20
+ unmuted_count = 0
21
+ failed_ids = []
22
+ unmuted_errors = []
23
+
24
+ errors.each do |error|
25
+ begin
26
+ error.update!(
27
+ muted: false,
28
+ muted_at: nil,
29
+ muted_by: nil,
30
+ muted_reason: nil
31
+ )
32
+ unmuted_count += 1
33
+ unmuted_errors << error
34
+ rescue => e
35
+ failed_ids << error.id
36
+ RailsErrorDashboard::Logger.error("Failed to unmute error #{error.id}: #{e.message}")
37
+ end
38
+ end
39
+
40
+ PluginRegistry.dispatch(:on_errors_batch_unmuted, unmuted_errors) if unmuted_errors.any?
41
+
42
+ {
43
+ success: failed_ids.empty?,
44
+ count: unmuted_count,
45
+ total: @error_ids.size,
46
+ failed_ids: failed_ids,
47
+ errors: failed_ids.empty? ? [] : [ "Failed to unmute #{failed_ids.size} error(s)" ]
48
+ }
49
+ rescue => e
50
+ RailsErrorDashboard::Logger.error("Batch unmute failed: #{e.message}")
51
+ { success: false, count: 0, total: @error_ids.size, errors: [ e.message ] }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -266,31 +266,20 @@ module RailsErrorDashboard
266
266
  end
267
267
  end
268
268
 
269
- # Send notifications for new errors and reopened errors (with throttling)
269
+ # Send notifications for new errors and reopened errors (with throttling).
270
+ # Muted errors skip notification dispatch but still fire plugin events.
270
271
  if error_log.occurrence_count == 1
271
- # Brand new error — notify if severity meets minimum
272
- if Services::NotificationThrottler.severity_meets_minimum?(error_log)
273
- Services::ErrorNotificationDispatcher.call(error_log)
274
- Services::NotificationThrottler.record_notification(error_log)
275
- end
272
+ maybe_notify(error_log) { Services::NotificationThrottler.severity_meets_minimum?(error_log) }
276
273
  PluginRegistry.dispatch(:on_error_logged, error_log)
277
274
  trigger_callbacks(error_log)
278
275
  emit_instrumentation_events(error_log)
279
276
  elsif error_log.just_reopened
280
- # Reopened error — notify if meets severity + not in cooldown
281
- if Services::NotificationThrottler.should_notify?(error_log)
282
- Services::ErrorNotificationDispatcher.call(error_log)
283
- Services::NotificationThrottler.record_notification(error_log)
284
- end
277
+ maybe_notify(error_log) { Services::NotificationThrottler.should_notify?(error_log) }
285
278
  PluginRegistry.dispatch(:on_error_reopened, error_log)
286
279
  trigger_callbacks(error_log)
287
280
  emit_instrumentation_events(error_log)
288
281
  else
289
- # Recurring unresolved error — check threshold milestones
290
- if Services::NotificationThrottler.threshold_reached?(error_log)
291
- Services::ErrorNotificationDispatcher.call(error_log)
292
- Services::NotificationThrottler.record_notification(error_log)
293
- end
282
+ maybe_notify(error_log) { Services::NotificationThrottler.threshold_reached?(error_log) }
294
283
  PluginRegistry.dispatch(:on_error_recurred, error_log)
295
284
  end
296
285
 
@@ -310,6 +299,16 @@ module RailsErrorDashboard
310
299
 
311
300
  private
312
301
 
302
+ # Dispatch notification if error is not muted and the throttle check passes.
303
+ # Muted errors skip notifications but still fire plugin events/callbacks.
304
+ def maybe_notify(error_log)
305
+ return if error_log.muted?
306
+ return unless yield
307
+
308
+ Services::ErrorNotificationDispatcher.call(error_log)
309
+ Services::NotificationThrottler.record_notification(error_log)
310
+ end
311
+
313
312
  # Find or create application for multi-app support
314
313
  def find_or_create_application
315
314
  app_name = RailsErrorDashboard.configuration.application_name ||
@@ -369,8 +368,9 @@ module RailsErrorDashboard
369
368
  def check_baseline_anomaly(error_log)
370
369
  config = RailsErrorDashboard.configuration
371
370
 
372
- # Return early if baseline alerts are disabled
371
+ # Return early if baseline alerts are disabled or error is muted
373
372
  return unless config.enable_baseline_alerts
373
+ return if error_log.muted?
374
374
  return unless defined?(Queries::BaselineStats)
375
375
  return unless defined?(BaselineAlertJob)
376
376
 
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Commands
5
+ # Command: Mute notifications for an error
6
+ # Muted errors still appear in the dashboard but do not trigger any notifications.
7
+ class MuteError
8
+ def self.call(error_id, muted_by: nil, reason: nil)
9
+ new(error_id, muted_by, reason).call
10
+ end
11
+
12
+ def initialize(error_id, muted_by, reason)
13
+ @error_id = error_id
14
+ @muted_by = muted_by
15
+ @reason = reason
16
+ end
17
+
18
+ def call
19
+ error = ErrorLog.find(@error_id)
20
+
21
+ if @reason.present?
22
+ error.comments.create!(
23
+ author_name: @muted_by || "System",
24
+ body: "Muted notifications: #{@reason}"
25
+ )
26
+ end
27
+
28
+ error.update!(
29
+ muted: true,
30
+ muted_at: Time.current,
31
+ muted_by: @muted_by,
32
+ muted_reason: @reason
33
+ )
34
+
35
+ PluginRegistry.dispatch(:on_error_muted, error)
36
+ error
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Commands
5
+ # Command: Unmute notifications for an error
6
+ # Restores normal notification behavior for the error.
7
+ class UnmuteError
8
+ def self.call(error_id)
9
+ new(error_id).call
10
+ end
11
+
12
+ def initialize(error_id)
13
+ @error_id = error_id
14
+ end
15
+
16
+ def call
17
+ error = ErrorLog.find(@error_id)
18
+ error.update!(
19
+ muted: false,
20
+ muted_at: nil,
21
+ muted_by: nil,
22
+ muted_reason: nil
23
+ )
24
+
25
+ PluginRegistry.dispatch(:on_error_unmuted, error)
26
+ error
27
+ end
28
+ end
29
+ end
30
+ end
@@ -158,6 +158,9 @@ module RailsErrorDashboard
158
158
  # Rack Attack event tracking (requires enable_breadcrumbs = true)
159
159
  attr_accessor :enable_rack_attack_tracking # Master switch (default: false)
160
160
 
161
+ # ActionCable event tracking (requires enable_breadcrumbs = true)
162
+ attr_accessor :enable_actioncable_tracking # Master switch (default: false)
163
+
161
164
  # Notification callbacks (managed via helper methods, not set directly)
162
165
  attr_reader :notification_callbacks
163
166
 
@@ -300,6 +303,9 @@ module RailsErrorDashboard
300
303
  # Rack Attack event tracking defaults - OFF by default (opt-in, requires breadcrumbs)
301
304
  @enable_rack_attack_tracking = false
302
305
 
306
+ # ActionCable event tracking defaults - OFF by default (opt-in, requires breadcrumbs)
307
+ @enable_actioncable_tracking = false
308
+
303
309
  # Internal logging defaults - SILENT by default
304
310
  @enable_internal_logging = false # Opt-in for debugging
305
311
  @log_level = :silent # Silent by default, use :debug, :info, :warn, :error, or :silent
@@ -440,6 +446,13 @@ module RailsErrorDashboard
440
446
  @enable_rack_attack_tracking = false
441
447
  end
442
448
 
449
+ # Validate actioncable tracking requires breadcrumbs
450
+ if enable_actioncable_tracking && !enable_breadcrumbs
451
+ warnings << "enable_actioncable_tracking requires enable_breadcrumbs = true. " \
452
+ "ActionCable tracking has been auto-disabled."
453
+ @enable_actioncable_tracking = false
454
+ end
455
+
443
456
  # Validate crash capture path (must exist if custom path specified)
444
457
  if enable_crash_capture && crash_capture_path
445
458
  unless Dir.exist?(crash_capture_path)
@@ -77,6 +77,13 @@ module RailsErrorDashboard
77
77
  RailsErrorDashboard::Subscribers::RackAttackSubscriber.subscribe!
78
78
  end
79
79
 
80
+ # Subscribe to ActionCable AS::Notifications events (requires breadcrumbs + ActionCable)
81
+ if RailsErrorDashboard.configuration.enable_actioncable_tracking &&
82
+ RailsErrorDashboard.configuration.enable_breadcrumbs &&
83
+ defined?(ActionCable)
84
+ RailsErrorDashboard::Subscribers::ActionCableSubscriber.subscribe!
85
+ end
86
+
80
87
  # Enable TracePoint(:raise) for local variable and/or instance variable capture
81
88
  if RailsErrorDashboard.configuration.enable_local_variables ||
82
89
  RailsErrorDashboard.configuration.enable_instance_variables
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Queries
5
+ # Query: Aggregate ActionCable events from breadcrumbs across all errors
6
+ # Scans error_logs breadcrumbs JSON, filters for "action_cable" category crumbs,
7
+ # and groups by channel name with counts by event type.
8
+ class ActionCableSummary
9
+ def self.call(days = 30, application_id: nil)
10
+ new(days, application_id: application_id).call
11
+ end
12
+
13
+ def initialize(days = 30, application_id: nil)
14
+ @days = days
15
+ @application_id = application_id
16
+ @start_date = days.days.ago
17
+ end
18
+
19
+ def call
20
+ {
21
+ channels: aggregated_channels
22
+ }
23
+ end
24
+
25
+ private
26
+
27
+ def base_query
28
+ scope = ErrorLog.where("occurred_at >= ?", @start_date)
29
+ .where.not(breadcrumbs: nil)
30
+ scope = scope.where(application_id: @application_id) if @application_id.present?
31
+ scope
32
+ end
33
+
34
+ def aggregated_channels
35
+ results = {}
36
+
37
+ base_query.select(:id, :breadcrumbs, :occurred_at).find_each(batch_size: 500) do |error_log|
38
+ crumbs = parse_breadcrumbs(error_log.breadcrumbs)
39
+ next if crumbs.empty?
40
+
41
+ ac_crumbs = crumbs.select { |c| c["c"] == "action_cable" }
42
+ next if ac_crumbs.empty?
43
+
44
+ ac_crumbs.each do |crumb|
45
+ meta = crumb["meta"] || {}
46
+ channel = meta["channel"].to_s.presence || "Unknown"
47
+ event_type = meta["event_type"].to_s
48
+
49
+ results[channel] ||= {
50
+ channel: channel,
51
+ perform_count: 0,
52
+ transmit_count: 0,
53
+ subscription_count: 0,
54
+ rejection_count: 0,
55
+ error_ids: [],
56
+ last_seen: nil
57
+ }
58
+
59
+ entry = results[channel]
60
+
61
+ case event_type
62
+ when "perform_action"
63
+ entry[:perform_count] += 1
64
+ when "transmit"
65
+ entry[:transmit_count] += 1
66
+ when "transmit_subscription_confirmation"
67
+ entry[:subscription_count] += 1
68
+ when "transmit_subscription_rejection"
69
+ entry[:rejection_count] += 1
70
+ end
71
+
72
+ entry[:error_ids] << error_log.id
73
+ entry[:last_seen] = [ entry[:last_seen], error_log.occurred_at ].compact.max
74
+ end
75
+ end
76
+
77
+ results.values.each do |r|
78
+ r[:error_ids] = r[:error_ids].uniq
79
+ r[:error_count] = r[:error_ids].size
80
+ r[:total_events] = r[:perform_count] + r[:transmit_count] + r[:subscription_count] + r[:rejection_count]
81
+ end
82
+ results.values.sort_by { |r| [ -r[:rejection_count], -r[:total_events] ] }
83
+ rescue => e
84
+ Rails.logger.error("[RailsErrorDashboard] ActionCableSummary query failed: #{e.class}: #{e.message}")
85
+ []
86
+ end
87
+
88
+ def parse_breadcrumbs(raw)
89
+ return [] if raw.blank?
90
+ JSON.parse(raw)
91
+ rescue JSON::ParserError
92
+ []
93
+ end
94
+ end
95
+ end
96
+ end
@@ -39,6 +39,7 @@ module RailsErrorDashboard
39
39
  query = filter_by_assignment(query)
40
40
  query = filter_by_priority(query)
41
41
  query = filter_by_snoozed(query)
42
+ query = filter_by_muted(query)
42
43
  query = filter_by_reopened(query)
43
44
  query
44
45
  end
@@ -196,6 +197,16 @@ module RailsErrorDashboard
196
197
  end
197
198
  end
198
199
 
200
+ def filter_by_muted(query)
201
+ return query unless ErrorLog.column_names.include?("muted")
202
+
203
+ if @filters[:hide_muted] == "1" || @filters[:hide_muted] == true
204
+ query.unmuted
205
+ else
206
+ query
207
+ end
208
+ end
209
+
199
210
  def filter_by_reopened(query)
200
211
  return query unless @filters[:reopened] == "true"
201
212
  return query unless ErrorLog.column_names.include?("reopened_at")
@@ -36,6 +36,7 @@ module RailsErrorDashboard
36
36
  job_queue: job_queue_stats,
37
37
  ruby_vm: ruby_vm_stats,
38
38
  yjit: yjit_stats,
39
+ actioncable: actioncable_stats,
39
40
  captured_at: Time.current.iso8601
40
41
  }
41
42
  end
@@ -154,6 +155,18 @@ module RailsErrorDashboard
154
155
  nil
155
156
  end
156
157
 
158
+ # ActionCable connection stats — read-only, <0.1ms
159
+ def actioncable_stats
160
+ return nil unless defined?(ActionCable) && defined?(ActionCable::Server)
161
+ server = ActionCable.server
162
+ {
163
+ connections: server.connections.count,
164
+ adapter: server.pubsub&.class&.name&.demodulize
165
+ }
166
+ rescue => e
167
+ nil
168
+ end
169
+
157
170
  # RubyVM::YJIT.runtime_stats — JIT compilation health
158
171
  # Cherry-picks diagnostic keys (full hash has 30+ entries)
159
172
  def yjit_stats
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Subscribers
5
+ # Registers ActiveSupport::Notifications subscribers for ActionCable events.
6
+ #
7
+ # ActionCable emits:
8
+ # - perform_action.action_cable — channel action executed
9
+ # - transmit.action_cable — data transmitted to subscriber
10
+ # - transmit_subscription_confirmation.action_cable — subscription confirmed
11
+ # - transmit_subscription_rejection.action_cable — subscription rejected
12
+ #
13
+ # Each event is captured as a breadcrumb with category "action_cable",
14
+ # allowing correlation between WebSocket events and error spikes.
15
+ #
16
+ # SAFETY RULES (HOST_APP_SAFETY.md):
17
+ # - Every subscriber wrapped in rescue => e; nil
18
+ # - Never raise from subscriber callbacks
19
+ # - Skip if buffer is nil (not in a request context)
20
+ class ActionCableSubscriber
21
+ EVENTS = %w[
22
+ perform_action.action_cable
23
+ transmit.action_cable
24
+ transmit_subscription_confirmation.action_cable
25
+ transmit_subscription_rejection.action_cable
26
+ ].freeze
27
+
28
+ # Event subscriptions managed by this class
29
+ @subscriptions = []
30
+
31
+ class << self
32
+ attr_reader :subscriptions
33
+
34
+ # Register all ActionCable event subscribers
35
+ # @return [Array] Array of subscription objects
36
+ def subscribe!
37
+ @subscriptions = []
38
+
39
+ EVENTS.each do |event_name|
40
+ @subscriptions << subscribe_event(event_name)
41
+ end
42
+
43
+ @subscriptions
44
+ end
45
+
46
+ # Remove all ActionCable subscribers
47
+ def unsubscribe!
48
+ @subscriptions.each do |sub|
49
+ ActiveSupport::Notifications.unsubscribe(sub) if sub
50
+ rescue => e
51
+ nil
52
+ end
53
+ @subscriptions = []
54
+ end
55
+
56
+ private
57
+
58
+ def subscribe_event(event_name)
59
+ ActiveSupport::Notifications.subscribe(event_name) do |*args|
60
+ event = ActiveSupport::Notifications::Event.new(*args)
61
+ handle_action_cable(event, event_name)
62
+ rescue => e
63
+ nil
64
+ end
65
+ end
66
+
67
+ def handle_action_cable(event, event_name)
68
+ return unless Services::BreadcrumbCollector.current_buffer
69
+
70
+ payload = event.payload || {}
71
+ channel = payload[:channel_class] || payload[:channel] || "Unknown"
72
+ channel = channel.to_s
73
+
74
+ event_type = event_name.split(".").first # "perform_action", "transmit", etc.
75
+ action = payload[:action].to_s
76
+
77
+ message = build_message(event_type, channel, action)
78
+
79
+ metadata = {
80
+ channel: channel,
81
+ event_type: event_type
82
+ }
83
+ metadata[:action] = action if action.present?
84
+
85
+ duration_ms = event.duration if event.respond_to?(:duration)
86
+
87
+ Services::BreadcrumbCollector.add("action_cable", message, duration_ms: duration_ms, metadata: metadata)
88
+ end
89
+
90
+ def build_message(event_type, channel, action)
91
+ case event_type
92
+ when "perform_action"
93
+ action.present? ? "perform: #{channel}##{action}" : "perform: #{channel}"
94
+ when "transmit"
95
+ "transmit: #{channel}"
96
+ when "transmit_subscription_confirmation"
97
+ "subscribed: #{channel}"
98
+ when "transmit_subscription_rejection"
99
+ "rejected: #{channel}"
100
+ else
101
+ "#{event_type}: #{channel}"
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.4.1"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -64,7 +64,9 @@ require "rails_error_dashboard/services/crash_capture"
64
64
  require "rails_error_dashboard/services/diagnostic_dump_generator"
65
65
  require "rails_error_dashboard/subscribers/breadcrumb_subscriber"
66
66
  require "rails_error_dashboard/subscribers/rack_attack_subscriber"
67
+ require "rails_error_dashboard/subscribers/action_cable_subscriber"
67
68
  require "rails_error_dashboard/queries/co_occurring_errors"
69
+ require "rails_error_dashboard/queries/action_cable_summary"
68
70
  require "rails_error_dashboard/queries/error_cascades"
69
71
  require "rails_error_dashboard/queries/baseline_stats"
70
72
  require "rails_error_dashboard/queries/platform_comparison"
@@ -78,6 +80,10 @@ require "rails_error_dashboard/commands/unassign_error"
78
80
  require "rails_error_dashboard/commands/update_error_priority"
79
81
  require "rails_error_dashboard/commands/snooze_error"
80
82
  require "rails_error_dashboard/commands/unsnooze_error"
83
+ require "rails_error_dashboard/commands/mute_error"
84
+ require "rails_error_dashboard/commands/unmute_error"
85
+ require "rails_error_dashboard/commands/batch_mute_errors"
86
+ require "rails_error_dashboard/commands/batch_unmute_errors"
81
87
  require "rails_error_dashboard/commands/update_error_status"
82
88
  require "rails_error_dashboard/commands/add_error_comment"
83
89
  require "rails_error_dashboard/commands/increment_cascade_detection"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_error_dashboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anjan Jagirdar
@@ -290,6 +290,7 @@ files:
290
290
  - app/views/rails_error_dashboard/errors/_stats.html.erb
291
291
  - app/views/rails_error_dashboard/errors/_timeline.html.erb
292
292
  - app/views/rails_error_dashboard/errors/_user_errors_table.html.erb
293
+ - app/views/rails_error_dashboard/errors/actioncable_health_summary.html.erb
293
294
  - app/views/rails_error_dashboard/errors/analytics.html.erb
294
295
  - app/views/rails_error_dashboard/errors/cache_health_summary.html.erb
295
296
  - app/views/rails_error_dashboard/errors/correlation.html.erb
@@ -338,6 +339,7 @@ files:
338
339
  - db/migrate/20260306000002_add_instance_variables_to_error_logs.rb
339
340
  - db/migrate/20260306000003_create_rails_error_dashboard_swallowed_exceptions.rb
340
341
  - db/migrate/20260307000001_create_rails_error_dashboard_diagnostic_dumps.rb
342
+ - db/migrate/20260323000001_add_muted_to_error_logs.rb
341
343
  - lib/generators/rails_error_dashboard/install/install_generator.rb
342
344
  - lib/generators/rails_error_dashboard/install/templates/README
343
345
  - lib/generators/rails_error_dashboard/install/templates/initializer.rb
@@ -348,16 +350,20 @@ files:
348
350
  - lib/rails_error_dashboard/commands/add_error_comment.rb
349
351
  - lib/rails_error_dashboard/commands/assign_error.rb
350
352
  - lib/rails_error_dashboard/commands/batch_delete_errors.rb
353
+ - lib/rails_error_dashboard/commands/batch_mute_errors.rb
351
354
  - lib/rails_error_dashboard/commands/batch_resolve_errors.rb
355
+ - lib/rails_error_dashboard/commands/batch_unmute_errors.rb
352
356
  - lib/rails_error_dashboard/commands/calculate_cascade_probability.rb
353
357
  - lib/rails_error_dashboard/commands/find_or_create_application.rb
354
358
  - lib/rails_error_dashboard/commands/find_or_increment_error.rb
355
359
  - lib/rails_error_dashboard/commands/flush_swallowed_exceptions.rb
356
360
  - lib/rails_error_dashboard/commands/increment_cascade_detection.rb
357
361
  - lib/rails_error_dashboard/commands/log_error.rb
362
+ - lib/rails_error_dashboard/commands/mute_error.rb
358
363
  - lib/rails_error_dashboard/commands/resolve_error.rb
359
364
  - lib/rails_error_dashboard/commands/snooze_error.rb
360
365
  - lib/rails_error_dashboard/commands/unassign_error.rb
366
+ - lib/rails_error_dashboard/commands/unmute_error.rb
361
367
  - lib/rails_error_dashboard/commands/unsnooze_error.rb
362
368
  - lib/rails_error_dashboard/commands/update_error_priority.rb
363
369
  - lib/rails_error_dashboard/commands/update_error_status.rb
@@ -377,6 +383,7 @@ files:
377
383
  - lib/rails_error_dashboard/plugins/audit_log_plugin.rb
378
384
  - lib/rails_error_dashboard/plugins/jira_integration_plugin.rb
379
385
  - lib/rails_error_dashboard/plugins/metrics_plugin.rb
386
+ - lib/rails_error_dashboard/queries/action_cable_summary.rb
380
387
  - lib/rails_error_dashboard/queries/analytics_stats.rb
381
388
  - lib/rails_error_dashboard/queries/baseline_stats.rb
382
389
  - lib/rails_error_dashboard/queries/cache_health_summary.rb
@@ -440,6 +447,7 @@ files:
440
447
  - lib/rails_error_dashboard/services/system_health_snapshot.rb
441
448
  - lib/rails_error_dashboard/services/variable_serializer.rb
442
449
  - lib/rails_error_dashboard/services/webhook_payload_builder.rb
450
+ - lib/rails_error_dashboard/subscribers/action_cable_subscriber.rb
443
451
  - lib/rails_error_dashboard/subscribers/breadcrumb_subscriber.rb
444
452
  - lib/rails_error_dashboard/subscribers/rack_attack_subscriber.rb
445
453
  - lib/rails_error_dashboard/value_objects/error_context.rb
@@ -457,7 +465,7 @@ metadata:
457
465
  bug_tracker_uri: https://github.com/AnjanJ/rails_error_dashboard/issues
458
466
  funding_uri: https://buymeacoffee.com/anjanj
459
467
  post_install_message: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n
460
- \ Rails Error Dashboard v0.4.1\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F195
468
+ \ Rails Error Dashboard v0.5.0\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F195
461
469
  First time? Quick start:\n rails generate rails_error_dashboard:install\n rails
462
470
  db:migrate\n # Add to config/routes.rb:\n mount RailsErrorDashboard::Engine
463
471
  => '/error_dashboard'\n\n\U0001F504 Upgrading from v0.1.x?\n rails db:migrate\n