rails_error_dashboard 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +7 -1
- data/app/controllers/rails_error_dashboard/errors_controller.rb +21 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +7 -0
- data/app/views/rails_error_dashboard/errors/actioncable_health_summary.html.erb +132 -0
- data/config/routes.rb +1 -0
- data/lib/rails_error_dashboard/configuration.rb +13 -0
- data/lib/rails_error_dashboard/engine.rb +7 -0
- data/lib/rails_error_dashboard/queries/action_cable_summary.rb +96 -0
- data/lib/rails_error_dashboard/services/system_health_snapshot.rb +13 -0
- data/lib/rails_error_dashboard/subscribers/action_cable_subscriber.rb +107 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +2 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9cbc9542b74dde89d9ef8e71bc6d55dc6c15b1bc1d8c0decc3181f83a39ab085
|
|
4
|
+
data.tar.gz: 25402cf18e90cbe62350700875c61cd334ae2798838892b5a0861b8fd2864abc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8baf9b17c79ba1ec2ac2a2cb1fca9044a5ef995a69993321014dfbd9de1c75f1dd6084deb6a4a35a40ce8d25679bb5557ef2e5a198280dd8d1ba42f13adc8625
|
|
7
|
+
data.tar.gz: 1603904cb93c8d7902530e3768d62dd60c747138f40a9dad315c111c0d67c51e843859624eca512234ff0a123887ffdde1568dab15753d942725e998decdcb15
|
data/README.md
CHANGED
|
@@ -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
|

|
|
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
|
|
|
@@ -409,6 +409,27 @@ module RailsErrorDashboard
|
|
|
409
409
|
@pagy, @events = pagy(:offset, all_events, limit: params[:per_page] || 25)
|
|
410
410
|
end
|
|
411
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
|
+
|
|
412
433
|
def diagnostic_dumps
|
|
413
434
|
unless RailsErrorDashboard.configuration.enable_diagnostic_dump
|
|
414
435
|
flash[:alert] = "Diagnostic dumps are not enabled. Enable them in config/initializers/rails_error_dashboard.rb"
|
|
@@ -1664,6 +1664,13 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
|
|
|
1664
1664
|
<% end %>
|
|
1665
1665
|
</li>
|
|
1666
1666
|
<% end %>
|
|
1667
|
+
<% if RailsErrorDashboard.configuration.enable_actioncable_tracking && RailsErrorDashboard.configuration.enable_breadcrumbs %>
|
|
1668
|
+
<li class="nav-item">
|
|
1669
|
+
<%= link_to actioncable_health_summary_errors_path(nav_params), class: "nav-link #{request.path == actioncable_health_summary_errors_path ? 'active' : ''}" do %>
|
|
1670
|
+
<i class="bi bi-broadcast"></i> ActionCable
|
|
1671
|
+
<% end %>
|
|
1672
|
+
</li>
|
|
1673
|
+
<% end %>
|
|
1667
1674
|
<% if RailsErrorDashboard.configuration.enable_system_health %>
|
|
1668
1675
|
<li class="nav-item">
|
|
1669
1676
|
<%= link_to job_health_summary_errors_path(nav_params), class: "nav-link #{request.path == job_health_summary_errors_path ? 'active' : ''}" do %>
|
|
@@ -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>
|
data/config/routes.rb
CHANGED
|
@@ -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
|
|
@@ -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
|
|
@@ -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"
|
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
|
+
version: 0.5.1
|
|
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
|
|
@@ -382,6 +383,7 @@ files:
|
|
|
382
383
|
- lib/rails_error_dashboard/plugins/audit_log_plugin.rb
|
|
383
384
|
- lib/rails_error_dashboard/plugins/jira_integration_plugin.rb
|
|
384
385
|
- lib/rails_error_dashboard/plugins/metrics_plugin.rb
|
|
386
|
+
- lib/rails_error_dashboard/queries/action_cable_summary.rb
|
|
385
387
|
- lib/rails_error_dashboard/queries/analytics_stats.rb
|
|
386
388
|
- lib/rails_error_dashboard/queries/baseline_stats.rb
|
|
387
389
|
- lib/rails_error_dashboard/queries/cache_health_summary.rb
|
|
@@ -445,6 +447,7 @@ files:
|
|
|
445
447
|
- lib/rails_error_dashboard/services/system_health_snapshot.rb
|
|
446
448
|
- lib/rails_error_dashboard/services/variable_serializer.rb
|
|
447
449
|
- lib/rails_error_dashboard/services/webhook_payload_builder.rb
|
|
450
|
+
- lib/rails_error_dashboard/subscribers/action_cable_subscriber.rb
|
|
448
451
|
- lib/rails_error_dashboard/subscribers/breadcrumb_subscriber.rb
|
|
449
452
|
- lib/rails_error_dashboard/subscribers/rack_attack_subscriber.rb
|
|
450
453
|
- lib/rails_error_dashboard/value_objects/error_context.rb
|
|
@@ -462,7 +465,7 @@ metadata:
|
|
|
462
465
|
bug_tracker_uri: https://github.com/AnjanJ/rails_error_dashboard/issues
|
|
463
466
|
funding_uri: https://buymeacoffee.com/anjanj
|
|
464
467
|
post_install_message: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n
|
|
465
|
-
\ Rails Error Dashboard v0.
|
|
468
|
+
\ Rails Error Dashboard v0.5.1\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F195
|
|
466
469
|
First time? Quick start:\n rails generate rails_error_dashboard:install\n rails
|
|
467
470
|
db:migrate\n # Add to config/routes.rb:\n mount RailsErrorDashboard::Engine
|
|
468
471
|
=> '/error_dashboard'\n\n\U0001F504 Upgrading from v0.1.x?\n rails db:migrate\n
|