rails_error_dashboard 0.5.10 → 0.5.12
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 +34 -1
- data/app/controllers/rails_error_dashboard/errors_controller.rb +31 -0
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +12 -0
- data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +40 -0
- data/app/mailers/rails_error_dashboard/digest_mailer.rb +23 -0
- data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +2 -1
- data/app/views/layouts/rails_error_dashboard.html.erb +5 -0
- data/app/views/rails_error_dashboard/digest_mailer/digest_summary.html.erb +172 -0
- data/app/views/rails_error_dashboard/digest_mailer/digest_summary.text.erb +49 -0
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +10 -10
- data/app/views/rails_error_dashboard/errors/_source_code.html.erb +20 -7
- data/app/views/rails_error_dashboard/errors/settings.html.erb +7 -3
- data/app/views/rails_error_dashboard/errors/show.html.erb +21 -0
- data/app/views/rails_error_dashboard/errors/user_impact.html.erb +172 -0
- data/config/routes.rb +3 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +173 -160
- data/lib/generators/rails_error_dashboard/install/templates/README +9 -18
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +15 -10
- data/lib/rails_error_dashboard/configuration.rb +83 -26
- data/lib/rails_error_dashboard/engine.rb +10 -0
- data/lib/rails_error_dashboard/error_reporter.rb +26 -0
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +7 -0
- data/lib/rails_error_dashboard/middleware/rate_limiter.rb +16 -12
- data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +2 -1
- data/lib/rails_error_dashboard/queries/user_impact_summary.rb +93 -0
- data/lib/rails_error_dashboard/services/coverage_tracker.rb +139 -0
- data/lib/rails_error_dashboard/services/digest_builder.rb +158 -0
- data/lib/rails_error_dashboard/services/notification_helpers.rb +2 -1
- data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +2 -1
- data/lib/rails_error_dashboard/value_objects/error_context.rb +5 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +3 -0
- data/lib/tasks/error_dashboard.rake +23 -0
- metadata +33 -9
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<% content_for :page_title, "User Impact" %>
|
|
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-people me-2"></i>
|
|
7
|
+
User Impact
|
|
8
|
+
</h1>
|
|
9
|
+
|
|
10
|
+
<div class="btn-group" role="group">
|
|
11
|
+
<%= link_to user_impact_errors_path(days: 7), class: "btn btn-sm #{@days == 7 ? 'btn-primary' : 'btn-outline-primary'}" do %>
|
|
12
|
+
7 Days
|
|
13
|
+
<% end %>
|
|
14
|
+
<%= link_to user_impact_errors_path(days: 30), class: "btn btn-sm #{@days == 30 ? 'btn-primary' : 'btn-outline-primary'}" do %>
|
|
15
|
+
30 Days
|
|
16
|
+
<% end %>
|
|
17
|
+
<%= link_to user_impact_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 @entries.empty? %>
|
|
24
|
+
<div class="text-center py-5">
|
|
25
|
+
<i class="bi bi-people display-1 text-muted mb-3"></i>
|
|
26
|
+
<h4 class="text-muted">No User Impact Data</h4>
|
|
27
|
+
<p class="text-muted">
|
|
28
|
+
No errors with user attribution were found 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 user impact is tracked:</h6>
|
|
33
|
+
<ul class="mb-0">
|
|
34
|
+
<li>Errors must have a <code>user_id</code> associated</li>
|
|
35
|
+
<li>Auto-detected via <code>CurrentAttributes</code> or <code>current_user</code></li>
|
|
36
|
+
<li>Or set <code>config.user_model = "User"</code> in your initializer</li>
|
|
37
|
+
</ul>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<% else %>
|
|
42
|
+
<!-- Summary Cards -->
|
|
43
|
+
<div class="row mb-4">
|
|
44
|
+
<div class="col-md-3">
|
|
45
|
+
<div class="card text-center">
|
|
46
|
+
<div class="card-body">
|
|
47
|
+
<div class="display-6 text-primary"><%= @summary[:total_error_types_with_users] %></div>
|
|
48
|
+
<small class="text-muted">Error Types with Users</small>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="col-md-3">
|
|
53
|
+
<div class="card text-center">
|
|
54
|
+
<div class="card-body">
|
|
55
|
+
<div class="display-6 text-danger"><%= @summary[:total_unique_users_affected] %></div>
|
|
56
|
+
<small class="text-muted">Total Users Affected</small>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="col-md-3">
|
|
61
|
+
<div class="card text-center">
|
|
62
|
+
<div class="card-body">
|
|
63
|
+
<div class="display-6"><code><%= @summary[:most_impactful] || "N/A" %></code></div>
|
|
64
|
+
<small class="text-muted">Most Impactful Error</small>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="col-md-3">
|
|
69
|
+
<div class="card text-center">
|
|
70
|
+
<div class="card-body">
|
|
71
|
+
<div class="display-6 text-info"><%= @summary[:total_users] ? number_with_delimiter(@summary[:total_users]) : "N/A" %></div>
|
|
72
|
+
<small class="text-muted">Total Users (for %)</small>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<!-- User Impact Table -->
|
|
79
|
+
<div class="card mb-4">
|
|
80
|
+
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
81
|
+
<h5 class="mb-0">
|
|
82
|
+
<i class="bi bi-bar-chart text-primary me-2"></i>
|
|
83
|
+
Errors Ranked by User Impact
|
|
84
|
+
<span class="badge bg-primary"><%= @summary[:total_error_types_with_users] %></span>
|
|
85
|
+
</h5>
|
|
86
|
+
<small class="text-muted"><%== @pagy.info_tag %></small>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="card-body p-0">
|
|
89
|
+
<div class="table-responsive">
|
|
90
|
+
<table class="table table-hover mb-0">
|
|
91
|
+
<thead class="table-light">
|
|
92
|
+
<tr>
|
|
93
|
+
<th width="40">#</th>
|
|
94
|
+
<th>Error Type</th>
|
|
95
|
+
<th width="120">Unique Users</th>
|
|
96
|
+
<th width="120">Occurrences</th>
|
|
97
|
+
<% if @summary[:total_users] %>
|
|
98
|
+
<th width="150">User Impact %</th>
|
|
99
|
+
<% end %>
|
|
100
|
+
<th width="100">Severity</th>
|
|
101
|
+
<th width="140">Last Seen</th>
|
|
102
|
+
<th width="80"></th>
|
|
103
|
+
</tr>
|
|
104
|
+
</thead>
|
|
105
|
+
<tbody>
|
|
106
|
+
<% @entries.each_with_index do |entry, index| %>
|
|
107
|
+
<tr>
|
|
108
|
+
<td><strong><%= (@pagy.from || 1) + index %></strong></td>
|
|
109
|
+
<td>
|
|
110
|
+
<strong><%= entry[:error_type] %></strong>
|
|
111
|
+
<br><small class="text-muted"><%= entry[:message] %></small>
|
|
112
|
+
</td>
|
|
113
|
+
<td>
|
|
114
|
+
<span class="badge bg-danger"><%= entry[:unique_users] %> users</span>
|
|
115
|
+
</td>
|
|
116
|
+
<td>
|
|
117
|
+
<span class="badge bg-secondary"><%= entry[:total_occurrences] %>x</span>
|
|
118
|
+
</td>
|
|
119
|
+
<% if @summary[:total_users] && entry[:impact_percentage] %>
|
|
120
|
+
<td>
|
|
121
|
+
<div class="d-flex align-items-center gap-2">
|
|
122
|
+
<div class="progress flex-grow-1" style="height: 18px;">
|
|
123
|
+
<%
|
|
124
|
+
pct = entry[:impact_percentage]
|
|
125
|
+
bar_class = pct > 10 ? "bg-danger" : (pct > 5 ? "bg-warning" : "bg-info")
|
|
126
|
+
%>
|
|
127
|
+
<div class="progress-bar <%= bar_class %>" style="width: <%= [ pct, 100 ].min %>%;">
|
|
128
|
+
<%= pct %>%
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</td>
|
|
133
|
+
<% end %>
|
|
134
|
+
<td>
|
|
135
|
+
<%
|
|
136
|
+
sev = entry[:severity].to_s
|
|
137
|
+
sev_class = case sev
|
|
138
|
+
when "critical" then "bg-danger"
|
|
139
|
+
when "high" then "bg-warning text-dark"
|
|
140
|
+
when "medium" then "bg-info"
|
|
141
|
+
else "bg-secondary"
|
|
142
|
+
end
|
|
143
|
+
%>
|
|
144
|
+
<span class="badge <%= sev_class %>"><%= sev.capitalize %></span>
|
|
145
|
+
</td>
|
|
146
|
+
<td><small><%= local_time_ago(entry[:last_seen]) %></small></td>
|
|
147
|
+
<td>
|
|
148
|
+
<% if entry[:id] %>
|
|
149
|
+
<%= link_to error_path(entry[:id]), class: "btn btn-sm btn-outline-primary" do %>
|
|
150
|
+
<i class="bi bi-eye"></i>
|
|
151
|
+
<% end %>
|
|
152
|
+
<% end %>
|
|
153
|
+
</td>
|
|
154
|
+
</tr>
|
|
155
|
+
<% end %>
|
|
156
|
+
</tbody>
|
|
157
|
+
</table>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="card-footer bg-white border-top d-flex justify-content-between align-items-center">
|
|
161
|
+
<small class="text-muted">
|
|
162
|
+
<i class="bi bi-info-circle"></i>
|
|
163
|
+
Ranked by unique users affected, not occurrence count.
|
|
164
|
+
An error hitting 1000 users once is worse than hitting 1 user 1000 times.
|
|
165
|
+
</small>
|
|
166
|
+
<div>
|
|
167
|
+
<%== @pagy.series_nav(:bootstrap) if @pagy.pages > 1 %>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
<% end %>
|
|
172
|
+
</div>
|
data/config/routes.rb
CHANGED
|
@@ -38,8 +38,11 @@ RailsErrorDashboard::Engine.routes.draw do
|
|
|
38
38
|
get :actioncable_health_summary
|
|
39
39
|
get :activestorage_health_summary
|
|
40
40
|
get :releases
|
|
41
|
+
get :user_impact
|
|
41
42
|
get :diagnostic_dumps
|
|
42
43
|
post :create_diagnostic_dump
|
|
44
|
+
post :enable_coverage
|
|
45
|
+
post :disable_coverage
|
|
43
46
|
post :batch_action
|
|
44
47
|
end
|
|
45
48
|
end
|
|
@@ -15,7 +15,7 @@ module RailsErrorDashboard
|
|
|
15
15
|
class_option :pagerduty, type: :boolean, default: false, desc: "Enable PagerDuty notifications"
|
|
16
16
|
class_option :webhooks, type: :boolean, default: false, desc: "Enable webhook notifications"
|
|
17
17
|
# Performance options
|
|
18
|
-
class_option :async_logging, type: :boolean, default:
|
|
18
|
+
class_option :async_logging, type: :boolean, default: true, desc: "Enable async error logging (default: true, uses Rails :async adapter — no extra infrastructure needed)"
|
|
19
19
|
class_option :error_sampling, type: :boolean, default: false, desc: "Enable error sampling (reduce volume)"
|
|
20
20
|
class_option :separate_database, type: :boolean, default: false, desc: "Use separate database for errors"
|
|
21
21
|
class_option :database, type: :string, default: nil, desc: "Database name to use for errors (e.g., 'error_dashboard')"
|
|
@@ -35,11 +35,13 @@ module RailsErrorDashboard
|
|
|
35
35
|
class_option :swallowed_exceptions, type: :boolean, default: false, desc: "Enable swallowed exception detection (Ruby 3.3+)"
|
|
36
36
|
class_option :crash_capture, type: :boolean, default: false, desc: "Enable process crash capture via at_exit hook"
|
|
37
37
|
class_option :diagnostic_dump, type: :boolean, default: false, desc: "Enable on-demand diagnostic dump"
|
|
38
|
+
class_option :quick, type: :boolean, default: false, desc: "Zero-prompt install with sensible defaults (~60 seconds to working dashboard)"
|
|
38
39
|
|
|
39
40
|
def welcome_message
|
|
40
41
|
say "\n"
|
|
41
42
|
say "=" * 70
|
|
42
|
-
say "
|
|
43
|
+
say " Rails Error Dashboard Installation", :cyan
|
|
44
|
+
say " Quick mode: zero prompts, working dashboard in ~60 seconds", :yellow if quick_mode?
|
|
43
45
|
say "=" * 70
|
|
44
46
|
say "\n"
|
|
45
47
|
say "Core features will be enabled automatically:", :green
|
|
@@ -52,7 +54,15 @@ module RailsErrorDashboard
|
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
def select_optional_features
|
|
55
|
-
return unless
|
|
57
|
+
return unless behavior == :invoke
|
|
58
|
+
|
|
59
|
+
if quick_mode?
|
|
60
|
+
@selected_features = build_quick_defaults
|
|
61
|
+
say " Using sensible defaults (analytics ON, notifications OFF, breadcrumbs OFF)", :green
|
|
62
|
+
return
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
return unless options[:interactive]
|
|
56
66
|
return unless $stdin.tty? # Skip interactive mode if not running in a terminal
|
|
57
67
|
|
|
58
68
|
say "Let's configure optional features...\n", :cyan
|
|
@@ -60,166 +70,116 @@ module RailsErrorDashboard
|
|
|
60
70
|
|
|
61
71
|
@selected_features = {}
|
|
62
72
|
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
category: "Notifications"
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
key: :email,
|
|
75
|
-
name: "Email Notifications",
|
|
76
|
-
description: "Send error alerts via email",
|
|
77
|
-
category: "Notifications"
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
key: :discord,
|
|
81
|
-
name: "Discord Notifications",
|
|
82
|
-
description: "Send error alerts to Discord",
|
|
83
|
-
category: "Notifications"
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
key: :pagerduty,
|
|
87
|
-
name: "PagerDuty Integration",
|
|
88
|
-
description: "Critical errors to PagerDuty",
|
|
89
|
-
category: "Notifications"
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
key: :webhooks,
|
|
93
|
-
name: "Generic Webhooks",
|
|
94
|
-
description: "Send data to custom endpoints",
|
|
95
|
-
category: "Notifications"
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
# === PERFORMANCE & SCALABILITY ===
|
|
99
|
-
{
|
|
100
|
-
key: :async_logging,
|
|
101
|
-
name: "Async Error Logging",
|
|
102
|
-
description: "Process errors in background jobs (faster responses)",
|
|
103
|
-
category: "Performance"
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
key: :error_sampling,
|
|
107
|
-
name: "Error Sampling",
|
|
108
|
-
description: "Log only % of non-critical errors (reduce volume)",
|
|
109
|
-
category: "Performance"
|
|
110
|
-
},
|
|
111
|
-
|
|
112
|
-
# === ADVANCED ANALYTICS ===
|
|
113
|
-
{
|
|
114
|
-
key: :baseline_alerts,
|
|
115
|
-
name: "Baseline Anomaly Alerts",
|
|
116
|
-
description: "Auto-detect unusual error rate spikes",
|
|
117
|
-
category: "Advanced Analytics"
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
key: :similar_errors,
|
|
121
|
-
name: "Fuzzy Error Matching",
|
|
122
|
-
description: "Find similar errors across different hashes",
|
|
123
|
-
category: "Advanced Analytics"
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
key: :co_occurring_errors,
|
|
127
|
-
name: "Co-occurring Errors",
|
|
128
|
-
description: "Detect errors that happen together",
|
|
129
|
-
category: "Advanced Analytics"
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
key: :error_cascades,
|
|
133
|
-
name: "Error Cascade Detection",
|
|
134
|
-
description: "Identify error chains (A causes B causes C)",
|
|
135
|
-
category: "Advanced Analytics"
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
key: :error_correlation,
|
|
139
|
-
name: "Error Correlation Analysis",
|
|
140
|
-
description: "Correlate with versions, users, time",
|
|
141
|
-
category: "Advanced Analytics"
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
key: :platform_comparison,
|
|
145
|
-
name: "Platform Comparison",
|
|
146
|
-
description: "Compare iOS vs Android vs Web health",
|
|
147
|
-
category: "Advanced Analytics"
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
key: :occurrence_patterns,
|
|
151
|
-
name: "Occurrence Pattern Detection",
|
|
152
|
-
description: "Detect cyclical patterns and bursts",
|
|
153
|
-
category: "Advanced Analytics"
|
|
154
|
-
},
|
|
155
|
-
|
|
156
|
-
# === DEVELOPER TOOLS ===
|
|
157
|
-
{
|
|
158
|
-
key: :source_code_integration,
|
|
159
|
-
name: "Source Code Integration (NEW!)",
|
|
160
|
-
description: "View source code directly in error details",
|
|
161
|
-
category: "Developer Tools"
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
key: :git_blame,
|
|
165
|
-
name: "Git Blame Integration (NEW!)",
|
|
166
|
-
description: "Show git blame info (author, commit, timestamp)",
|
|
167
|
-
category: "Developer Tools"
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
key: :breadcrumbs,
|
|
171
|
-
name: "Breadcrumbs (NEW!)",
|
|
172
|
-
description: "Capture request activity trail (SQL, controller, cache events)",
|
|
173
|
-
category: "Developer Tools"
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
key: :system_health,
|
|
177
|
-
name: "System Health Snapshot (NEW!)",
|
|
178
|
-
description: "Capture GC, memory, threads, connection pool at error time",
|
|
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"
|
|
198
|
-
}
|
|
199
|
-
]
|
|
200
|
-
|
|
201
|
-
features.each_with_index do |feature, index|
|
|
202
|
-
say "\n[#{index + 1}/#{features.length}] #{feature[:name]}", :cyan
|
|
203
|
-
say " #{feature[:description]}", :white
|
|
204
|
-
|
|
205
|
-
# Check if feature was passed via command line option
|
|
206
|
-
if options[feature[:key]]
|
|
207
|
-
@selected_features[feature[:key]] = true
|
|
208
|
-
say " ✓ Enabled (via --#{feature[:key]} flag)", :green
|
|
209
|
-
else
|
|
210
|
-
response = ask(" Enable? (y/N):", :yellow, limited_to: [ "y", "Y", "n", "N", "" ])
|
|
211
|
-
@selected_features[feature[:key]] = response.downcase == "y"
|
|
73
|
+
# =====================================================================
|
|
74
|
+
# QUESTION 1: Notifications (gated — one y/N opens 5 sub-questions)
|
|
75
|
+
# =====================================================================
|
|
76
|
+
notification_keys = %i[slack email discord pagerduty webhooks]
|
|
77
|
+
any_notification_cli_flag = notification_keys.any? { |k| options[k] }
|
|
78
|
+
|
|
79
|
+
say "[1/3] Notifications [background/dashboard only — zero request overhead]", :cyan
|
|
80
|
+
say " Alert your team via Slack, email, Discord, PagerDuty, or webhooks.", :white
|
|
212
81
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
82
|
+
if any_notification_cli_flag
|
|
83
|
+
# Individual CLI flags passed — respect them, skip the gate prompt
|
|
84
|
+
notification_keys.each { |k| @selected_features[k] = options[k] }
|
|
85
|
+
say " ✓ Using CLI flags for notification channels", :green
|
|
86
|
+
else
|
|
87
|
+
wants_notifications = ask(" Set up notifications? (y/N):", :yellow, limited_to: [ "y", "Y", "n", "N", "" ])
|
|
88
|
+
if wants_notifications.downcase == "y"
|
|
89
|
+
notification_channels = [
|
|
90
|
+
{ key: :slack, name: "Slack", hint: "SLACK_WEBHOOK_URL" },
|
|
91
|
+
{ key: :email, name: "Email", hint: "ERROR_NOTIFICATION_EMAILS" },
|
|
92
|
+
{ key: :discord, name: "Discord", hint: "DISCORD_WEBHOOK_URL" },
|
|
93
|
+
{ key: :pagerduty, name: "PagerDuty", hint: "PAGERDUTY_INTEGRATION_KEY" },
|
|
94
|
+
{ key: :webhooks, name: "Webhooks", hint: "WEBHOOK_URLS" }
|
|
95
|
+
]
|
|
96
|
+
notification_channels.each do |ch|
|
|
97
|
+
r = ask(" #{ch[:name]}? (ENV[#{ch[:hint]}]) (y/N):", :yellow, limited_to: [ "y", "Y", "n", "N", "" ])
|
|
98
|
+
@selected_features[ch[:key]] = r.downcase == "y"
|
|
217
99
|
end
|
|
100
|
+
else
|
|
101
|
+
notification_keys.each { |k| @selected_features[k] = false }
|
|
102
|
+
say " ✓ Notifications off (enable anytime in initializer)", :white
|
|
218
103
|
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# =====================================================================
|
|
107
|
+
# QUESTION 2: Advanced Analytics (grouped — defaults YES)
|
|
108
|
+
# =====================================================================
|
|
109
|
+
analytics_keys = %i[baseline_alerts similar_errors co_occurring_errors
|
|
110
|
+
error_cascades error_correlation platform_comparison occurrence_patterns]
|
|
111
|
+
any_analytics_cli_flag = analytics_keys.any? { |k| options[k] }
|
|
112
|
+
|
|
113
|
+
say "\n[2/3] Advanced Analytics (7 features) [background/dashboard only]", :cyan
|
|
114
|
+
say " Baseline alerts, fuzzy matching, co-occurring errors, cascade detection,", :white
|
|
115
|
+
say " correlation analysis, platform comparison, occurrence patterns.", :white
|
|
116
|
+
say " All run at query time or as background jobs — zero request-path overhead.", :white
|
|
117
|
+
|
|
118
|
+
if any_analytics_cli_flag
|
|
119
|
+
# Let create_initializer_file read options[] directly via the &.dig fallback
|
|
120
|
+
say " ✓ Using individual CLI flags for analytics", :white
|
|
121
|
+
else
|
|
122
|
+
response = ask(" Enable all? (Y/n):", :yellow, limited_to: [ "y", "Y", "n", "N", "" ])
|
|
123
|
+
analytics_on = response.downcase != "n"
|
|
124
|
+
analytics_keys.each { |k| @selected_features[k] = analytics_on }
|
|
125
|
+
say analytics_on ? " ✓ All 7 analytics features enabled" : " ✗ Analytics disabled (enable individually in initializer)", analytics_on ? :green : :white
|
|
126
|
+
end
|
|
219
127
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
128
|
+
# =====================================================================
|
|
129
|
+
# QUESTION 3: Advanced Options (gated — defaults NO)
|
|
130
|
+
# =====================================================================
|
|
131
|
+
advanced_keys = %i[async_logging error_sampling breadcrumbs system_health
|
|
132
|
+
source_code_integration git_blame swallowed_exceptions
|
|
133
|
+
crash_capture diagnostic_dump]
|
|
134
|
+
any_advanced_cli_flag = advanced_keys.any? { |k| options[k] }
|
|
135
|
+
|
|
136
|
+
say "\n[3/3] Advanced Options (performance tuning & diagnostics)", :cyan
|
|
137
|
+
say " Async logging, error sampling, breadcrumbs, system health,", :white
|
|
138
|
+
say " source code viewer, git blame, crash capture, and more.", :white
|
|
139
|
+
|
|
140
|
+
if any_advanced_cli_flag
|
|
141
|
+
# CLI flags passed — set them and skip the gate
|
|
142
|
+
advanced_features = [
|
|
143
|
+
{ key: :async_logging, name: "Async Logging", desc: "Process errors in background jobs [removes request-path overhead]" },
|
|
144
|
+
{ key: :error_sampling, name: "Error Sampling", desc: "Log only % of non-critical errors [error-time only]" },
|
|
145
|
+
{ key: :breadcrumbs, name: "Breadcrumbs", desc: "6 AS::Notifications subscribers: SQL, cache, jobs, mailers, Rack::Attack, ActionCable [request-path overhead]" },
|
|
146
|
+
{ key: :system_health, name: "System Health Snapshot", desc: "GC, memory, threads, connection pool at error time [error-time only]" },
|
|
147
|
+
{ key: :source_code_integration, name: "Source Code Integration", desc: "Inline source viewer in error details [dashboard only]" },
|
|
148
|
+
{ key: :git_blame, name: "Git Blame", desc: "Author, commit, timestamp per source line [dashboard only]" },
|
|
149
|
+
{ key: :swallowed_exceptions, name: "Swallowed Exception Detection", desc: "TracePoint(:rescue) catches silently rescued exceptions [request-path overhead, Ruby 3.3+]" },
|
|
150
|
+
{ key: :crash_capture, name: "Process Crash Capture", desc: "at_exit hook captures fatal crashes [error-time only]" },
|
|
151
|
+
{ key: :diagnostic_dump, name: "Diagnostic Dump", desc: "On-demand system snapshot via rake task [background/dashboard only]" }
|
|
152
|
+
]
|
|
153
|
+
advanced_features.each { |f| @selected_features[f[:key]] = options[f[:key]] }
|
|
154
|
+
say " ✓ Using CLI flags for advanced options", :white
|
|
155
|
+
else
|
|
156
|
+
wants_advanced = ask(" Configure advanced options? (y/N):", :yellow, limited_to: [ "y", "Y", "n", "N", "" ])
|
|
157
|
+
if wants_advanced.downcase == "y"
|
|
158
|
+
advanced_features = [
|
|
159
|
+
{ key: :async_logging, name: "Async Logging", desc: "Process errors in background jobs — removes overhead from request path [removes request-path overhead]" },
|
|
160
|
+
{ key: :error_sampling, name: "Error Sampling", desc: "Log only % of non-critical errors to reduce volume [error-time only]" },
|
|
161
|
+
{ key: :breadcrumbs, name: "Breadcrumbs", desc: "Subscribes 6 AS::Notifications: SQL, cache, jobs, mailers, Rack::Attack, ActionCable [request-path overhead]" },
|
|
162
|
+
{ key: :system_health, name: "System Health Snapshot", desc: "GC stats, memory (RSS/peak/swap), threads, connection pool at error time [error-time only]" },
|
|
163
|
+
{ key: :source_code_integration, name: "Source Code Integration", desc: "View source code inline in error details (+/- 7 lines context) [dashboard only]" },
|
|
164
|
+
{ key: :git_blame, name: "Git Blame Integration", desc: "Show author, commit, timestamp for each source line (requires git) [dashboard only]" },
|
|
165
|
+
{ key: :swallowed_exceptions, name: "Swallowed Exception Detection", desc: "Detect silently rescued exceptions via TracePoint(:rescue) [request-path overhead, Ruby 3.3+]" },
|
|
166
|
+
{ key: :crash_capture, name: "Process Crash Capture", desc: "Capture fatal crashes via at_exit hook, written to disk [error-time only]" },
|
|
167
|
+
{ key: :diagnostic_dump, name: "Diagnostic Dump", desc: "On-demand system state snapshot via rake task or dashboard button [background/dashboard only]" }
|
|
168
|
+
]
|
|
169
|
+
advanced_features.each do |f|
|
|
170
|
+
say "\n #{f[:name]}", :cyan
|
|
171
|
+
say " #{f[:desc]}", :white
|
|
172
|
+
r = ask(" Enable? (y/N):", :yellow, limited_to: [ "y", "Y", "n", "N", "" ])
|
|
173
|
+
@selected_features[f[:key]] = r.downcase == "y"
|
|
174
|
+
if @selected_features[f[:key]] && f[:key] == :swallowed_exceptions && RUBY_VERSION < "3.3"
|
|
175
|
+
say " ⚠ Requires Ruby 3.3+ (you have #{RUBY_VERSION}) — will activate after upgrade", :yellow
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
else
|
|
179
|
+
# Set all advanced keys to false EXCEPT async_logging which defaults ON via class_option.
|
|
180
|
+
# We do not set async_logging here so options[:async_logging] (default: true) wins in create_initializer_file.
|
|
181
|
+
(advanced_keys - [ :async_logging ]).each { |k| @selected_features[k] = false }
|
|
182
|
+
say " ✓ Using defaults (async logging ON, everything else OFF)", :white
|
|
223
183
|
end
|
|
224
184
|
end
|
|
225
185
|
|
|
@@ -247,6 +207,13 @@ module RailsErrorDashboard
|
|
|
247
207
|
# Skip if existing config already detected database mode
|
|
248
208
|
return if @existing_install_detected && @database_mode
|
|
249
209
|
|
|
210
|
+
# Quick mode: use shared DB, no prompt
|
|
211
|
+
if quick_mode?
|
|
212
|
+
@database_mode = :same
|
|
213
|
+
@application_name = detect_application_name
|
|
214
|
+
return
|
|
215
|
+
end
|
|
216
|
+
|
|
250
217
|
# Skip if not interactive or if --separate_database was passed via CLI
|
|
251
218
|
if options[:separate_database]
|
|
252
219
|
@database_mode = :separate
|
|
@@ -398,6 +365,12 @@ module RailsErrorDashboard
|
|
|
398
365
|
end
|
|
399
366
|
|
|
400
367
|
def add_route
|
|
368
|
+
routes_path = File.join(destination_root, "config", "routes.rb")
|
|
369
|
+
if File.exist?(routes_path) && File.read(routes_path).include?("RailsErrorDashboard::Engine")
|
|
370
|
+
say_status "skip", "route already exists (RailsErrorDashboard::Engine is already mounted)", :yellow
|
|
371
|
+
return
|
|
372
|
+
end
|
|
373
|
+
|
|
401
374
|
route "mount RailsErrorDashboard::Engine => '/red' # RED (Rails Error Dashboard) — also works at /error_dashboard"
|
|
402
375
|
end
|
|
403
376
|
|
|
@@ -415,6 +388,7 @@ module RailsErrorDashboard
|
|
|
415
388
|
say " ✓ Dashboard UI", :green
|
|
416
389
|
say " ✓ Real-time Updates", :green
|
|
417
390
|
say " ✓ Analytics", :green
|
|
391
|
+
say " ✓ Async Logging (Rails :async adapter — no extra infrastructure needed)", :green
|
|
418
392
|
|
|
419
393
|
# Count optional features enabled
|
|
420
394
|
enabled_count = 0
|
|
@@ -490,7 +464,16 @@ module RailsErrorDashboard
|
|
|
490
464
|
say " → Set DISCORD_WEBHOOK_URL in .env", :yellow if @enable_discord
|
|
491
465
|
say " → Set PAGERDUTY_INTEGRATION_KEY in .env", :yellow if @enable_pagerduty
|
|
492
466
|
say " → Set WEBHOOK_URLS in .env", :yellow if @enable_webhooks
|
|
493
|
-
|
|
467
|
+
if @enable_async_logging
|
|
468
|
+
# Check which async adapter is in use — only warn about external workers when needed
|
|
469
|
+
async_section = File.exist?(File.join(destination_root, "config/initializers/rails_error_dashboard.rb")) &&
|
|
470
|
+
File.read(File.join(destination_root, "config/initializers/rails_error_dashboard.rb"))
|
|
471
|
+
if async_section && (async_section.include?("async_adapter = :sidekiq") || async_section.include?("async_adapter = :solid_queue"))
|
|
472
|
+
say " → Ensure your background worker (Sidekiq/SolidQueue) is running for async logging", :yellow
|
|
473
|
+
else
|
|
474
|
+
say " ✓ Async logging uses Rails :async adapter — no extra process needed", :green
|
|
475
|
+
end
|
|
476
|
+
end
|
|
494
477
|
|
|
495
478
|
# Database-specific instructions
|
|
496
479
|
if @enable_separate_database
|
|
@@ -548,6 +531,36 @@ module RailsErrorDashboard
|
|
|
548
531
|
|
|
549
532
|
private
|
|
550
533
|
|
|
534
|
+
def quick_mode?
|
|
535
|
+
options[:quick]
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def build_quick_defaults
|
|
539
|
+
{
|
|
540
|
+
# Async ON (built-in Rails :async adapter, zero infrastructure)
|
|
541
|
+
async_logging: true,
|
|
542
|
+
# All analytics ON — run at query/background time, zero request-path overhead
|
|
543
|
+
baseline_alerts: true,
|
|
544
|
+
similar_errors: true,
|
|
545
|
+
co_occurring_errors: true,
|
|
546
|
+
error_cascades: true,
|
|
547
|
+
error_correlation: true,
|
|
548
|
+
platform_comparison: true,
|
|
549
|
+
occurrence_patterns: true,
|
|
550
|
+
# ON — small overhead, high insight value
|
|
551
|
+
breadcrumbs: true, # ~0.1ms per request — SQL, cache, job trail leading to each error
|
|
552
|
+
system_health: true, # ~1ms per error — GC, memory, threads at exact error moment
|
|
553
|
+
error_sampling: true, # 50% sampling on non-critical errors — halves storage, still shows patterns
|
|
554
|
+
# OFF — require credentials, config, or niche use case
|
|
555
|
+
slack: false, email: false, discord: false, pagerduty: false, webhooks: false,
|
|
556
|
+
source_code_integration: false,
|
|
557
|
+
git_blame: false,
|
|
558
|
+
swallowed_exceptions: false,
|
|
559
|
+
crash_capture: false,
|
|
560
|
+
diagnostic_dump: false
|
|
561
|
+
}
|
|
562
|
+
end
|
|
563
|
+
|
|
551
564
|
def detect_application_name
|
|
552
565
|
if defined?(Rails) && Rails.application
|
|
553
566
|
Rails.application.class.module_parent_name
|
|
@@ -1,33 +1,24 @@
|
|
|
1
1
|
===============================================================================
|
|
2
2
|
|
|
3
|
-
Rails Error Dashboard has been installed!
|
|
3
|
+
RED (Rails Error Dashboard) has been installed!
|
|
4
4
|
|
|
5
5
|
Next steps:
|
|
6
6
|
|
|
7
7
|
1. Run migrations:
|
|
8
8
|
rails db:migrate
|
|
9
9
|
|
|
10
|
-
2.
|
|
11
|
-
- Set USE_SEPARATE_ERROR_DB=true in your .env file
|
|
12
|
-
- Configure error_logs database in config/database.yml
|
|
13
|
-
- Run: rails db:create:error_logs
|
|
14
|
-
- Run: rails db:migrate:error_logs
|
|
15
|
-
|
|
16
|
-
3. Start your Rails server:
|
|
10
|
+
2. Start your Rails server:
|
|
17
11
|
rails server
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
http://localhost:3000/
|
|
13
|
+
3. Visit the dashboard:
|
|
14
|
+
http://localhost:3000/red
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Password: password
|
|
16
|
+
4. Change the default credentials in:
|
|
17
|
+
config/initializers/rails_error_dashboard.rb
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- Errors will be sent to your Slack channel automatically
|
|
19
|
+
For separate database setup, see:
|
|
20
|
+
https://github.com/AnjanJ/rails_error_dashboard/blob/main/docs/guides/CONFIGURATION.md
|
|
29
21
|
|
|
30
|
-
|
|
31
|
-
https://github.com/anjanjagirdar/rails_error_dashboard
|
|
22
|
+
Full docs: https://github.com/AnjanJ/rails_error_dashboard
|
|
32
23
|
|
|
33
24
|
===============================================================================
|