rails_error_dashboard 0.3.1 → 0.4.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.
- checksums.yaml +4 -4
- data/README.md +160 -861
- data/app/controllers/rails_error_dashboard/errors_controller.rb +89 -0
- data/app/jobs/rails_error_dashboard/swallowed_exception_flush_job.rb +32 -0
- data/app/models/rails_error_dashboard/diagnostic_dump.rb +14 -0
- data/app/models/rails_error_dashboard/swallowed_exception.rb +38 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +21 -0
- data/app/views/rails_error_dashboard/errors/_instance_variables.html.erb +55 -0
- data/app/views/rails_error_dashboard/errors/_local_variables.html.erb +46 -0
- data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +182 -0
- data/app/views/rails_error_dashboard/errors/rack_attack_summary.html.erb +133 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +4 -0
- data/app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb +126 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +33 -0
- data/db/migrate/20260306000001_add_local_variables_to_error_logs.rb +13 -0
- data/db/migrate/20260306000002_add_instance_variables_to_error_logs.rb +7 -0
- data/db/migrate/20260306000003_create_rails_error_dashboard_swallowed_exceptions.rb +34 -0
- data/db/migrate/20260307000001_create_rails_error_dashboard_diagnostic_dumps.rb +17 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +32 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +47 -0
- data/lib/rails_error_dashboard/commands/flush_swallowed_exceptions.rb +103 -0
- data/lib/rails_error_dashboard/commands/log_error.rb +68 -0
- data/lib/rails_error_dashboard/configuration.rb +122 -0
- data/lib/rails_error_dashboard/engine.rb +24 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +32 -11
- data/lib/rails_error_dashboard/queries/rack_attack_summary.rb +90 -0
- data/lib/rails_error_dashboard/queries/swallowed_exception_summary.rb +97 -0
- data/lib/rails_error_dashboard/services/breadcrumb_collector.rb +12 -0
- data/lib/rails_error_dashboard/services/crash_capture.rb +234 -0
- data/lib/rails_error_dashboard/services/diagnostic_dump_generator.rb +98 -0
- data/lib/rails_error_dashboard/services/local_variable_capturer.rb +207 -0
- data/lib/rails_error_dashboard/services/swallowed_exception_tracker.rb +277 -0
- data/lib/rails_error_dashboard/services/variable_serializer.rb +326 -0
- data/lib/rails_error_dashboard/subscribers/rack_attack_subscriber.rb +94 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +9 -0
- data/lib/tasks/error_dashboard.rake +34 -0
- metadata +23 -2
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<% content_for :page_title, "Swallowed Exceptions" %>
|
|
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-eye-slash me-2"></i>
|
|
7
|
+
Swallowed Exceptions
|
|
8
|
+
</h1>
|
|
9
|
+
|
|
10
|
+
<div class="btn-group" role="group">
|
|
11
|
+
<%= link_to swallowed_exceptions_errors_path(days: 7), class: "btn btn-sm #{@days == 7 ? 'btn-primary' : 'btn-outline-primary'}" do %>
|
|
12
|
+
7 Days
|
|
13
|
+
<% end %>
|
|
14
|
+
<%= link_to swallowed_exceptions_errors_path(days: 30), class: "btn btn-sm #{@days == 30 ? 'btn-primary' : 'btn-outline-primary'}" do %>
|
|
15
|
+
30 Days
|
|
16
|
+
<% end %>
|
|
17
|
+
<%= link_to swallowed_exceptions_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_count == 0 %>
|
|
24
|
+
<div class="text-center py-5">
|
|
25
|
+
<i class="bi bi-check-circle display-1 text-success mb-3"></i>
|
|
26
|
+
<h4 class="text-muted">No Swallowed Exceptions Detected</h4>
|
|
27
|
+
<p class="text-muted">
|
|
28
|
+
No exceptions with a rescue ratio above <%= (RailsErrorDashboard.configuration.swallowed_exception_threshold * 100).round %>% were detected in 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 swallowed exceptions are detected:</h6>
|
|
33
|
+
<ul class="mb-0">
|
|
34
|
+
<li>TracePoint(:raise) counts every exception raised</li>
|
|
35
|
+
<li>TracePoint(:rescue) counts every exception rescued</li>
|
|
36
|
+
<li>High rescue/raise ratio = likely swallowed (silently rescued)</li>
|
|
37
|
+
<li>Requires Ruby 3.3+ and <code>detect_swallowed_exceptions = true</code></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-danger"><%= @unique_count %></div>
|
|
48
|
+
<small class="text-muted">Swallowed Patterns</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-warning"><%= number_with_delimiter(@total_rescue_count) %></div>
|
|
56
|
+
<small class="text-muted">Total Rescues</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 text-info"><%= number_with_delimiter(@total_raise_count) %></div>
|
|
64
|
+
<small class="text-muted">Total Raises</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-eye-slash text-danger me-2"></i>
|
|
74
|
+
Swallowed Exception Patterns
|
|
75
|
+
<span class="badge bg-danger"><%= @unique_count %></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>Exception Class</th>
|
|
85
|
+
<th width="250">Raise Location</th>
|
|
86
|
+
<th width="250">Rescue Location</th>
|
|
87
|
+
<th width="100">Raises</th>
|
|
88
|
+
<th width="100">Rescues</th>
|
|
89
|
+
<th width="100">Ratio</th>
|
|
90
|
+
<th width="140">Last Seen</th>
|
|
91
|
+
</tr>
|
|
92
|
+
</thead>
|
|
93
|
+
<tbody>
|
|
94
|
+
<% @entries.each do |entry| %>
|
|
95
|
+
<tr>
|
|
96
|
+
<td><code style="font-size: 0.85em;"><%= entry[:exception_class] %></code></td>
|
|
97
|
+
<td><small class="text-muted text-break"><%= entry[:raise_location] %></small></td>
|
|
98
|
+
<td><small class="text-muted text-break"><%= entry[:rescue_location] || "Unknown" %></small></td>
|
|
99
|
+
<td><span class="badge bg-info text-dark"><%= number_with_delimiter(entry[:raise_count]) %></span></td>
|
|
100
|
+
<td><span class="badge bg-warning text-dark"><%= number_with_delimiter(entry[:rescue_count]) %></span></td>
|
|
101
|
+
<td>
|
|
102
|
+
<% ratio_pct = (entry[:rescue_ratio] * 100).round(1) %>
|
|
103
|
+
<span class="badge <%= ratio_pct >= 99 ? 'bg-danger' : ratio_pct >= 95 ? 'bg-warning text-dark' : 'bg-secondary' %>">
|
|
104
|
+
<%= ratio_pct %>%
|
|
105
|
+
</span>
|
|
106
|
+
</td>
|
|
107
|
+
<td><%= local_time_ago(entry[:last_seen]) %></td>
|
|
108
|
+
</tr>
|
|
109
|
+
<% end %>
|
|
110
|
+
</tbody>
|
|
111
|
+
</table>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="card-footer bg-white border-top d-flex justify-content-between align-items-center">
|
|
115
|
+
<div>
|
|
116
|
+
<small class="text-muted">
|
|
117
|
+
<i class="bi bi-lightbulb text-warning"></i> Swallowed exceptions are raised then silently rescued. They may hide real bugs.
|
|
118
|
+
</small>
|
|
119
|
+
</div>
|
|
120
|
+
<div>
|
|
121
|
+
<%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
<% end %>
|
|
126
|
+
</div>
|
data/config/routes.rb
CHANGED
|
@@ -27,6 +27,10 @@ RailsErrorDashboard::Engine.routes.draw do
|
|
|
27
27
|
get :cache_health_summary
|
|
28
28
|
get :job_health_summary
|
|
29
29
|
get :database_health_summary
|
|
30
|
+
get :swallowed_exceptions
|
|
31
|
+
get :rack_attack_summary
|
|
32
|
+
get :diagnostic_dumps
|
|
33
|
+
post :create_diagnostic_dump
|
|
30
34
|
post :batch_action
|
|
31
35
|
end
|
|
32
36
|
end
|
|
@@ -89,6 +89,12 @@ class CreateRailsErrorDashboardCompleteSchema < ActiveRecord::Migration[7.0]
|
|
|
89
89
|
# System health snapshot (from 20260304000001)
|
|
90
90
|
t.text :system_health
|
|
91
91
|
|
|
92
|
+
# Local variable capture (from 20260306000001)
|
|
93
|
+
t.text :local_variables
|
|
94
|
+
|
|
95
|
+
# Instance variable capture (from 20260306000002)
|
|
96
|
+
t.text :instance_variables
|
|
97
|
+
|
|
92
98
|
t.timestamps
|
|
93
99
|
end
|
|
94
100
|
|
|
@@ -191,6 +197,32 @@ class CreateRailsErrorDashboardCompleteSchema < ActiveRecord::Migration[7.0]
|
|
|
191
197
|
add_index :rails_error_dashboard_error_comments, :error_log_id
|
|
192
198
|
add_index :rails_error_dashboard_error_comments, [ :error_log_id, :created_at ], name: "index_error_comments_on_error_and_time"
|
|
193
199
|
|
|
200
|
+
# Create swallowed_exceptions table (from 20260306000003)
|
|
201
|
+
create_table :rails_error_dashboard_swallowed_exceptions do |t|
|
|
202
|
+
t.string :exception_class, null: false
|
|
203
|
+
t.string :raise_location, null: false, limit: 500
|
|
204
|
+
t.string :rescue_location, limit: 500
|
|
205
|
+
t.datetime :period_hour, null: false
|
|
206
|
+
t.integer :raise_count, null: false, default: 0
|
|
207
|
+
t.integer :rescue_count, null: false, default: 0
|
|
208
|
+
t.datetime :last_seen_at
|
|
209
|
+
t.bigint :application_id
|
|
210
|
+
t.timestamps
|
|
211
|
+
end
|
|
212
|
+
add_index :rails_error_dashboard_swallowed_exceptions,
|
|
213
|
+
[ :exception_class, :period_hour ],
|
|
214
|
+
name: "index_swallowed_exceptions_on_class_and_hour"
|
|
215
|
+
add_index :rails_error_dashboard_swallowed_exceptions,
|
|
216
|
+
:period_hour,
|
|
217
|
+
name: "index_swallowed_exceptions_on_period_hour"
|
|
218
|
+
add_index :rails_error_dashboard_swallowed_exceptions,
|
|
219
|
+
[ :application_id, :period_hour ],
|
|
220
|
+
name: "index_swallowed_exceptions_on_app_and_hour"
|
|
221
|
+
add_index :rails_error_dashboard_swallowed_exceptions,
|
|
222
|
+
[ :exception_class, :raise_location, :rescue_location, :period_hour, :application_id ],
|
|
223
|
+
unique: true,
|
|
224
|
+
name: "index_swallowed_exceptions_upsert_key"
|
|
225
|
+
|
|
194
226
|
# PostgreSQL-specific indexes (BRIN + functional for time-series optimization)
|
|
195
227
|
if ActiveRecord::Base.connection.adapter_name.downcase == "postgresql"
|
|
196
228
|
execute <<-SQL
|
|
@@ -221,6 +253,7 @@ class CreateRailsErrorDashboardCompleteSchema < ActiveRecord::Migration[7.0]
|
|
|
221
253
|
end
|
|
222
254
|
|
|
223
255
|
def down
|
|
256
|
+
drop_table :rails_error_dashboard_swallowed_exceptions, if_exists: true
|
|
224
257
|
drop_table :rails_error_dashboard_error_comments, if_exists: true
|
|
225
258
|
drop_table :rails_error_dashboard_cascade_patterns, if_exists: true
|
|
226
259
|
drop_table :rails_error_dashboard_error_baselines, if_exists: true
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddLocalVariablesToErrorLogs < ActiveRecord::Migration[7.0]
|
|
4
|
+
def up
|
|
5
|
+
return if column_exists?(:rails_error_dashboard_error_logs, :local_variables)
|
|
6
|
+
|
|
7
|
+
add_column :rails_error_dashboard_error_logs, :local_variables, :text
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def down
|
|
11
|
+
remove_column :rails_error_dashboard_error_logs, :local_variables if column_exists?(:rails_error_dashboard_error_logs, :local_variables)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRailsErrorDashboardSwallowedExceptions < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :rails_error_dashboard_swallowed_exceptions do |t|
|
|
6
|
+
t.string :exception_class, null: false
|
|
7
|
+
t.string :raise_location, null: false, limit: 500
|
|
8
|
+
t.string :rescue_location, limit: 500
|
|
9
|
+
t.datetime :period_hour, null: false
|
|
10
|
+
t.integer :raise_count, null: false, default: 0
|
|
11
|
+
t.integer :rescue_count, null: false, default: 0
|
|
12
|
+
t.datetime :last_seen_at
|
|
13
|
+
t.bigint :application_id
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :rails_error_dashboard_swallowed_exceptions,
|
|
18
|
+
[ :exception_class, :period_hour ],
|
|
19
|
+
name: "index_swallowed_exceptions_on_class_and_hour"
|
|
20
|
+
|
|
21
|
+
add_index :rails_error_dashboard_swallowed_exceptions,
|
|
22
|
+
:period_hour,
|
|
23
|
+
name: "index_swallowed_exceptions_on_period_hour"
|
|
24
|
+
|
|
25
|
+
add_index :rails_error_dashboard_swallowed_exceptions,
|
|
26
|
+
[ :application_id, :period_hour ],
|
|
27
|
+
name: "index_swallowed_exceptions_on_app_and_hour"
|
|
28
|
+
|
|
29
|
+
add_index :rails_error_dashboard_swallowed_exceptions,
|
|
30
|
+
[ :exception_class, :raise_location, :rescue_location, :period_hour, :application_id ],
|
|
31
|
+
unique: true,
|
|
32
|
+
name: "index_swallowed_exceptions_upsert_key"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateRailsErrorDashboardDiagnosticDumps < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :rails_error_dashboard_diagnostic_dumps do |t|
|
|
6
|
+
t.references :application, null: false,
|
|
7
|
+
foreign_key: { to_table: :rails_error_dashboard_applications }
|
|
8
|
+
t.text :dump_data, null: false
|
|
9
|
+
t.string :note
|
|
10
|
+
t.datetime :captured_at, null: false
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
add_index :rails_error_dashboard_diagnostic_dumps, :captured_at,
|
|
15
|
+
name: "index_diagnostic_dumps_on_captured_at"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -32,6 +32,9 @@ module RailsErrorDashboard
|
|
|
32
32
|
class_option :git_blame, type: :boolean, default: false, desc: "Enable git blame integration (NEW!)"
|
|
33
33
|
class_option :breadcrumbs, type: :boolean, default: false, desc: "Enable breadcrumbs (request activity trail)"
|
|
34
34
|
class_option :system_health, type: :boolean, default: false, desc: "Enable system health snapshot at error time"
|
|
35
|
+
class_option :swallowed_exceptions, type: :boolean, default: false, desc: "Enable swallowed exception detection (Ruby 3.3+)"
|
|
36
|
+
class_option :crash_capture, type: :boolean, default: false, desc: "Enable process crash capture via at_exit hook"
|
|
37
|
+
class_option :diagnostic_dump, type: :boolean, default: false, desc: "Enable on-demand diagnostic dump"
|
|
35
38
|
|
|
36
39
|
def welcome_message
|
|
37
40
|
say "\n"
|
|
@@ -174,6 +177,24 @@ module RailsErrorDashboard
|
|
|
174
177
|
name: "System Health Snapshot (NEW!)",
|
|
175
178
|
description: "Capture GC, memory, threads, connection pool at error time",
|
|
176
179
|
category: "Developer Tools"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
key: :swallowed_exceptions,
|
|
183
|
+
name: "Swallowed Exception Detection (Ruby 3.3+)",
|
|
184
|
+
description: "Detect exceptions being silently rescued via TracePoint(:rescue)",
|
|
185
|
+
category: "Developer Tools"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
key: :crash_capture,
|
|
189
|
+
name: "Process Crash Capture",
|
|
190
|
+
description: "Capture fatal crashes via at_exit hook (written to disk, imported on next boot)",
|
|
191
|
+
category: "Developer Tools"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
key: :diagnostic_dump,
|
|
195
|
+
name: "Diagnostic Dump",
|
|
196
|
+
description: "On-demand system state snapshot (rake task + dashboard page)",
|
|
197
|
+
category: "Developer Tools"
|
|
177
198
|
}
|
|
178
199
|
]
|
|
179
200
|
|
|
@@ -195,6 +216,11 @@ module RailsErrorDashboard
|
|
|
195
216
|
say " ✗ Disabled", :white
|
|
196
217
|
end
|
|
197
218
|
end
|
|
219
|
+
|
|
220
|
+
if @selected_features[feature[:key]] && feature[:key] == :swallowed_exceptions && RUBY_VERSION < "3.3"
|
|
221
|
+
say " ⚠ Warning: This feature requires Ruby 3.3+ (you have #{RUBY_VERSION})", :yellow
|
|
222
|
+
say " The feature will be included in your config but won't activate until you upgrade Ruby.", :white
|
|
223
|
+
end
|
|
198
224
|
end
|
|
199
225
|
|
|
200
226
|
say "\n"
|
|
@@ -303,6 +329,9 @@ module RailsErrorDashboard
|
|
|
303
329
|
@enable_git_blame = @selected_features&.dig(:git_blame) || options[:git_blame]
|
|
304
330
|
@enable_breadcrumbs = @selected_features&.dig(:breadcrumbs) || options[:breadcrumbs]
|
|
305
331
|
@enable_system_health = @selected_features&.dig(:system_health) || options[:system_health]
|
|
332
|
+
@enable_swallowed_exceptions = @selected_features&.dig(:swallowed_exceptions) || options[:swallowed_exceptions]
|
|
333
|
+
@enable_crash_capture = @selected_features&.dig(:crash_capture) || options[:crash_capture]
|
|
334
|
+
@enable_diagnostic_dump = @selected_features&.dig(:diagnostic_dump) || options[:diagnostic_dump]
|
|
306
335
|
|
|
307
336
|
template "initializer.rb", "config/initializers/rails_error_dashboard.rb"
|
|
308
337
|
end
|
|
@@ -405,6 +434,9 @@ module RailsErrorDashboard
|
|
|
405
434
|
developer_tools_features << "Git Blame" if @enable_git_blame
|
|
406
435
|
developer_tools_features << "Breadcrumbs" if @enable_breadcrumbs
|
|
407
436
|
developer_tools_features << "System Health" if @enable_system_health
|
|
437
|
+
developer_tools_features << "Swallowed Exception Detection" if @enable_swallowed_exceptions
|
|
438
|
+
developer_tools_features << "Process Crash Capture" if @enable_crash_capture
|
|
439
|
+
developer_tools_features << "Diagnostic Dump" if @enable_diagnostic_dump
|
|
408
440
|
|
|
409
441
|
if developer_tools_features.any?
|
|
410
442
|
say "\nDeveloper Tools:", :cyan
|
|
@@ -356,7 +356,54 @@ RailsErrorDashboard.configure do |config|
|
|
|
356
356
|
config.enable_system_health = false
|
|
357
357
|
|
|
358
358
|
<% end -%>
|
|
359
|
+
<% if @enable_swallowed_exceptions -%>
|
|
360
|
+
# Swallowed Exception Detection - ENABLED
|
|
361
|
+
# Requires Ruby 3.3+ — detects exceptions that are raised then silently rescued
|
|
362
|
+
# Uses TracePoint(:rescue), which was added in Ruby 3.3 (Feature #19572)
|
|
363
|
+
config.detect_swallowed_exceptions = true
|
|
364
|
+
config.swallowed_exception_threshold = 0.95 # Rescue ratio to flag (95%+)
|
|
365
|
+
# config.swallowed_exception_flush_interval = 60 # Seconds between DB flushes
|
|
366
|
+
# config.swallowed_exception_max_cache_size = 1000 # Max entries per thread
|
|
367
|
+
# config.swallowed_exception_ignore_classes = [] # App-specific exceptions to skip
|
|
368
|
+
# To disable: Set config.detect_swallowed_exceptions = false
|
|
359
369
|
|
|
370
|
+
<% else -%>
|
|
371
|
+
# Swallowed Exception Detection - DISABLED
|
|
372
|
+
# Requires Ruby 3.3+ (TracePoint(:rescue) not available before 3.3)
|
|
373
|
+
# To enable: Set config.detect_swallowed_exceptions = true
|
|
374
|
+
config.detect_swallowed_exceptions = false
|
|
375
|
+
# config.swallowed_exception_threshold = 0.95
|
|
376
|
+
|
|
377
|
+
<% end -%>
|
|
378
|
+
<% if @enable_diagnostic_dump -%>
|
|
379
|
+
# Diagnostic Dump - ENABLED
|
|
380
|
+
# On-demand system state snapshot via rake task or dashboard button
|
|
381
|
+
config.enable_diagnostic_dump = true
|
|
382
|
+
# To disable: Set config.enable_diagnostic_dump = false
|
|
383
|
+
|
|
384
|
+
<% else -%>
|
|
385
|
+
# Diagnostic Dump - DISABLED
|
|
386
|
+
# On-demand system state snapshot (rake task + dashboard page)
|
|
387
|
+
# To enable: Set config.enable_diagnostic_dump = true
|
|
388
|
+
config.enable_diagnostic_dump = false
|
|
389
|
+
|
|
390
|
+
<% end -%>
|
|
391
|
+
<% if @enable_crash_capture -%>
|
|
392
|
+
# Process Crash Capture - ENABLED
|
|
393
|
+
# Captures fatal crashes via at_exit hook. Crash data is written to disk as JSON
|
|
394
|
+
# and imported into the database on next boot. Zero runtime overhead.
|
|
395
|
+
config.enable_crash_capture = true
|
|
396
|
+
# config.crash_capture_path = "/tmp/my_app_crashes" # Default: Dir.tmpdir
|
|
397
|
+
# To disable: Set config.enable_crash_capture = false
|
|
398
|
+
|
|
399
|
+
<% else -%>
|
|
400
|
+
# Process Crash Capture - DISABLED
|
|
401
|
+
# Captures fatal crashes via at_exit hook (written to disk, imported on next boot)
|
|
402
|
+
# To enable: Set config.enable_crash_capture = true
|
|
403
|
+
config.enable_crash_capture = false
|
|
404
|
+
# config.crash_capture_path = "/tmp/my_app_crashes"
|
|
405
|
+
|
|
406
|
+
<% end -%>
|
|
360
407
|
# Repository settings (auto-detected from git remote, optional override)
|
|
361
408
|
# config.repository_url = ENV["REPOSITORY_URL"] # e.g., "https://github.com/user/repo"
|
|
362
409
|
# config.repository_branch = ENV.fetch("REPOSITORY_BRANCH", "main") # Default branch
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Commands
|
|
5
|
+
# Command: Upsert swallowed exception raise/rescue counts into the database.
|
|
6
|
+
#
|
|
7
|
+
# Receives snapshot hashes from SwallowedExceptionTracker and merges them
|
|
8
|
+
# into hourly-bucketed rows. Uses find_or_initialize_by + increment for
|
|
9
|
+
# cross-database compatibility (no raw SQL upsert).
|
|
10
|
+
#
|
|
11
|
+
# raise_counts keys: "ClassName|path:line"
|
|
12
|
+
# rescue_counts keys: "ClassName|raise_path:line->rescue_path:line"
|
|
13
|
+
class FlushSwallowedExceptions
|
|
14
|
+
def self.call(raise_counts:, rescue_counts:)
|
|
15
|
+
new(raise_counts: raise_counts, rescue_counts: rescue_counts).call
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(raise_counts:, rescue_counts:)
|
|
19
|
+
@raise_counts = raise_counts
|
|
20
|
+
@rescue_counts = rescue_counts
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call
|
|
24
|
+
period = Time.current.beginning_of_hour
|
|
25
|
+
app_id = current_application_id
|
|
26
|
+
|
|
27
|
+
# Process raise counts
|
|
28
|
+
@raise_counts.each do |key, count|
|
|
29
|
+
class_name, location = key.split("|", 2)
|
|
30
|
+
next if class_name.blank? || location.blank?
|
|
31
|
+
|
|
32
|
+
upsert_raise(class_name, location, period, app_id, count)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Process rescue counts
|
|
36
|
+
@rescue_counts.each do |key, count|
|
|
37
|
+
class_name, locations = key.split("|", 2)
|
|
38
|
+
next if class_name.blank? || locations.blank?
|
|
39
|
+
|
|
40
|
+
raise_loc, rescue_loc = locations.split("->", 2)
|
|
41
|
+
next if raise_loc.blank?
|
|
42
|
+
|
|
43
|
+
upsert_rescue(class_name, raise_loc, rescue_loc, period, app_id, count)
|
|
44
|
+
end
|
|
45
|
+
rescue => e
|
|
46
|
+
RailsErrorDashboard::Logger.debug(
|
|
47
|
+
"[RailsErrorDashboard] FlushSwallowedExceptions failed: #{e.class} - #{e.message}"
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def upsert_raise(class_name, location, period, app_id, count)
|
|
54
|
+
record = SwallowedException.find_or_initialize_by(
|
|
55
|
+
exception_class: truncate(class_name, 255),
|
|
56
|
+
raise_location: truncate(location, 500),
|
|
57
|
+
rescue_location: nil,
|
|
58
|
+
period_hour: period,
|
|
59
|
+
application_id: app_id
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
record.raise_count = (record.raise_count || 0) + count
|
|
63
|
+
record.last_seen_at = Time.current
|
|
64
|
+
record.save!
|
|
65
|
+
rescue => e
|
|
66
|
+
RailsErrorDashboard::Logger.debug(
|
|
67
|
+
"[RailsErrorDashboard] FlushSwallowedExceptions.upsert_raise failed for #{class_name}: #{e.message}"
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def upsert_rescue(class_name, raise_loc, rescue_loc, period, app_id, count)
|
|
72
|
+
record = SwallowedException.find_or_initialize_by(
|
|
73
|
+
exception_class: truncate(class_name, 255),
|
|
74
|
+
raise_location: truncate(raise_loc, 500),
|
|
75
|
+
rescue_location: rescue_loc.present? ? truncate(rescue_loc, 500) : nil,
|
|
76
|
+
period_hour: period,
|
|
77
|
+
application_id: app_id
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
record.rescue_count = (record.rescue_count || 0) + count
|
|
81
|
+
record.last_seen_at = Time.current
|
|
82
|
+
record.save!
|
|
83
|
+
rescue => e
|
|
84
|
+
RailsErrorDashboard::Logger.debug(
|
|
85
|
+
"[RailsErrorDashboard] FlushSwallowedExceptions.upsert_rescue failed for #{class_name}: #{e.message}"
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def current_application_id
|
|
90
|
+
app_name = RailsErrorDashboard.configuration.application_name
|
|
91
|
+
return nil unless app_name.present?
|
|
92
|
+
|
|
93
|
+
Application.find_by(name: app_name)&.id
|
|
94
|
+
rescue => e
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def truncate(str, max)
|
|
99
|
+
str.to_s.truncate(max, omission: "")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -37,6 +37,34 @@ module RailsErrorDashboard
|
|
|
37
37
|
context = context.merge(_serialized_system_health: Services::SystemHealthSnapshot.capture)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Capture local variables NOW (TracePoint attaches to exception, must extract before job dispatch)
|
|
41
|
+
if RailsErrorDashboard.configuration.enable_local_variables
|
|
42
|
+
begin
|
|
43
|
+
raw_locals = Services::LocalVariableCapturer.extract(exception)
|
|
44
|
+
if raw_locals.is_a?(Hash) && raw_locals.any?
|
|
45
|
+
context = context.merge(_serialized_local_variables: Services::VariableSerializer.call(raw_locals))
|
|
46
|
+
end
|
|
47
|
+
rescue => e
|
|
48
|
+
RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] Async local variable serialization failed: #{e.message}")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Capture instance variables NOW (same reason — attached to exception object)
|
|
53
|
+
if RailsErrorDashboard.configuration.enable_instance_variables
|
|
54
|
+
begin
|
|
55
|
+
raw_ivars = Services::LocalVariableCapturer.extract_instance_vars(exception)
|
|
56
|
+
if raw_ivars.is_a?(Hash) && raw_ivars.any?
|
|
57
|
+
context = context.merge(_serialized_instance_variables: Services::VariableSerializer.call(
|
|
58
|
+
raw_ivars,
|
|
59
|
+
max_count: RailsErrorDashboard.configuration.instance_variable_max_count,
|
|
60
|
+
additional_filter_patterns: RailsErrorDashboard.configuration.instance_variable_filter_patterns
|
|
61
|
+
))
|
|
62
|
+
end
|
|
63
|
+
rescue => e
|
|
64
|
+
RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] Async instance variable serialization failed: #{e.message}")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
40
68
|
# Enqueue the async job using ActiveJob
|
|
41
69
|
# The queue adapter (:sidekiq, :solid_queue, :async) is configured separately
|
|
42
70
|
AsyncErrorLoggingJob.perform_later(exception_data, context)
|
|
@@ -179,6 +207,46 @@ module RailsErrorDashboard
|
|
|
179
207
|
attributes[:system_health] = health_data.to_json
|
|
180
208
|
end
|
|
181
209
|
|
|
210
|
+
# Capture local variables (if enabled and column exists)
|
|
211
|
+
if ErrorLog.column_names.include?("local_variables") && RailsErrorDashboard.configuration.enable_local_variables
|
|
212
|
+
begin
|
|
213
|
+
# Sync path: extract from exception ivar
|
|
214
|
+
raw_locals = Services::LocalVariableCapturer.extract(@exception)
|
|
215
|
+
# Async path fallback: use pre-serialized locals from call_async context
|
|
216
|
+
raw_locals ||= @context[:_serialized_local_variables]
|
|
217
|
+
if raw_locals.is_a?(Hash) && raw_locals.any?
|
|
218
|
+
serialized = raw_locals == @context[:_serialized_local_variables] ? raw_locals : Services::VariableSerializer.call(raw_locals)
|
|
219
|
+
attributes[:local_variables] = serialized.to_json
|
|
220
|
+
end
|
|
221
|
+
rescue => e
|
|
222
|
+
RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] Local variable serialization failed: #{e.message}")
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Capture instance variables (if enabled and column exists)
|
|
227
|
+
if ErrorLog.column_names.include?("instance_variables") && RailsErrorDashboard.configuration.enable_instance_variables
|
|
228
|
+
begin
|
|
229
|
+
# Sync path: extract from exception ivar
|
|
230
|
+
raw_ivars = Services::LocalVariableCapturer.extract_instance_vars(@exception)
|
|
231
|
+
# Async path fallback: use pre-serialized ivars from call_async context
|
|
232
|
+
raw_ivars ||= @context[:_serialized_instance_variables]
|
|
233
|
+
if raw_ivars.is_a?(Hash) && raw_ivars.any?
|
|
234
|
+
serialized = if raw_ivars == @context[:_serialized_instance_variables]
|
|
235
|
+
raw_ivars
|
|
236
|
+
else
|
|
237
|
+
Services::VariableSerializer.call(
|
|
238
|
+
raw_ivars,
|
|
239
|
+
max_count: RailsErrorDashboard.configuration.instance_variable_max_count,
|
|
240
|
+
additional_filter_patterns: RailsErrorDashboard.configuration.instance_variable_filter_patterns
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
attributes[:instance_variables] = serialized.to_json
|
|
244
|
+
end
|
|
245
|
+
rescue => e
|
|
246
|
+
RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] Instance variable serialization failed: #{e.message}")
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
182
250
|
# Find existing error or create new one
|
|
183
251
|
# This ensures accurate occurrence tracking
|
|
184
252
|
error_log = ErrorLog.find_or_increment_by_hash(error_hash, attributes.merge(error_hash: error_hash))
|