rails_error_dashboard 0.5.11 → 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/app/views/rails_error_dashboard/errors/_error_row.html.erb +10 -10
- data/app/views/rails_error_dashboard/errors/settings.html.erb +1 -1
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +167 -160
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +15 -10
- data/lib/rails_error_dashboard/configuration.rb +38 -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/value_objects/error_context.rb +5 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c512995247150c6d418591b4ea85b40778b37c5b484666cf2af2226cbad9da7
|
|
4
|
+
data.tar.gz: 72283fdf63f2ea4fac3cdde62098bc247c4262862fbd8dd2ef80b5b804fcd626
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a66093c81993d0af82915235a2da1bbb5d0ba6779a9882d2e87bb90b4d98012d9d3f4be91703c570ac9253cd346d17f53a5eb8e21f9cc6bf44728e72ec23ef1
|
|
7
|
+
data.tar.gz: '06290bf609e62810e613530eceff8dcc86936a8b39f7e27c5ac0b8bd2ba5090f05e2df7ed4b86f1ca6d732fa849cfeea6d3fd361f4ef79e9a304fd010239f8ad'
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<td onclick="event.stopPropagation();">
|
|
3
3
|
<input type="checkbox" class="error-checkbox form-check-input" value="<%= error.id %>" data-error-id="<%= error.id %>">
|
|
4
4
|
</td>
|
|
5
|
-
<td onclick="window.location='<%= error_path(error) %>';">
|
|
5
|
+
<td onclick="window.location='<%= rails_error_dashboard.error_path(error) %>';">
|
|
6
6
|
<% severity = error.severity %>
|
|
7
7
|
<% if severity == :critical %>
|
|
8
8
|
<span class="badge bg-danger" data-bs-toggle="tooltip" title="Critical severity - requires immediate attention">CRITICAL</span>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<br><small class="text-muted" data-bs-toggle="tooltip" title="Priority score (0-100) based on severity, frequency, recency, and user impact">P<%= error.priority_score %></small>
|
|
18
18
|
<% end %>
|
|
19
19
|
</td>
|
|
20
|
-
<td onclick="window.location='<%= error_path(error) %>';">
|
|
20
|
+
<td onclick="window.location='<%= rails_error_dashboard.error_path(error) %>';">
|
|
21
21
|
<code class="text-danger" data-bs-toggle="tooltip" title="<%= error.error_type %>"><%= error.error_type.split('::').last %></code>
|
|
22
22
|
<% if error.recent? %>
|
|
23
23
|
<span class="badge bg-success ms-1" data-bs-toggle="tooltip" title="Error occurred within the last hour">NEW</span>
|
|
@@ -26,29 +26,29 @@
|
|
|
26
26
|
<br><small class="badge bg-light text-dark" data-bs-toggle="tooltip" title="App version when error occurred">v<%= error.app_version %></small>
|
|
27
27
|
<% end %>
|
|
28
28
|
</td>
|
|
29
|
-
<td onclick="window.location='<%= error_path(error) %>';">
|
|
29
|
+
<td onclick="window.location='<%= rails_error_dashboard.error_path(error) %>';">
|
|
30
30
|
<div class="text-truncate" style="max-width: 300px;" title="<%= error.message %>">
|
|
31
31
|
<%= error.message %>
|
|
32
32
|
</div>
|
|
33
33
|
</td>
|
|
34
|
-
<td onclick="window.location='<%= error_path(error) %>';">
|
|
34
|
+
<td onclick="window.location='<%= rails_error_dashboard.error_path(error) %>';">
|
|
35
35
|
<span class="badge bg-primary"><%= error.occurrence_count %>x</span>
|
|
36
36
|
</td>
|
|
37
|
-
<td onclick="window.location='<%= error_path(error) %>';">
|
|
37
|
+
<td onclick="window.location='<%= rails_error_dashboard.error_path(error) %>';">
|
|
38
38
|
<small>
|
|
39
39
|
<strong>First:</strong> <%= local_time(error.first_seen_at, format: :short) %><br>
|
|
40
40
|
<strong>Last:</strong> <%= local_time(error.last_seen_at, format: :short) %>
|
|
41
41
|
</small>
|
|
42
42
|
</td>
|
|
43
43
|
<% if local_assigns[:show_application] %>
|
|
44
|
-
<td onclick="window.location='<%= error_path(error) %>';">
|
|
44
|
+
<td onclick="window.location='<%= rails_error_dashboard.error_path(error) %>';">
|
|
45
45
|
<span class="badge bg-info">
|
|
46
46
|
<%= error.application&.name || 'Unknown' %>
|
|
47
47
|
</span>
|
|
48
48
|
</td>
|
|
49
49
|
<% end %>
|
|
50
50
|
<% if local_assigns[:show_platform] %>
|
|
51
|
-
<td onclick="window.location='<%= error_path(error) %>';">
|
|
51
|
+
<td onclick="window.location='<%= rails_error_dashboard.error_path(error) %>';">
|
|
52
52
|
<% if error.platform == 'iOS' %>
|
|
53
53
|
<span class="badge badge-ios"><i class="bi bi-apple"></i> iOS</span>
|
|
54
54
|
<% elsif error.platform == 'Android' %>
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
<% end %>
|
|
61
61
|
</td>
|
|
62
62
|
<% end %>
|
|
63
|
-
<td onclick="window.location='<%= error_path(error) %>';">
|
|
63
|
+
<td onclick="window.location='<%= rails_error_dashboard.error_path(error) %>';">
|
|
64
64
|
<% if error.user_id %>
|
|
65
65
|
<small data-bs-toggle="tooltip" title="User affected by this error">User #<%= error.user_id %></small>
|
|
66
66
|
<% if error.respond_to?(:user_impact_percentage) %>
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
<small class="text-muted" data-bs-toggle="tooltip" title="No specific user affected">-</small>
|
|
74
74
|
<% end %>
|
|
75
75
|
</td>
|
|
76
|
-
<td onclick="window.location='<%= error_path(error) %>';">
|
|
76
|
+
<td onclick="window.location='<%= rails_error_dashboard.error_path(error) %>';">
|
|
77
77
|
<% if error.resolved? %>
|
|
78
78
|
<i class="bi bi-check-circle-fill text-success"
|
|
79
79
|
data-bs-toggle="tooltip"
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
<% end %>
|
|
96
96
|
</td>
|
|
97
97
|
<td onclick="event.stopPropagation();">
|
|
98
|
-
<%= link_to error_path(error), class: "btn btn-sm btn-outline-primary" do %>
|
|
98
|
+
<%= link_to rails_error_dashboard.error_path(error), class: "btn btn-sm btn-outline-primary" do %>
|
|
99
99
|
<i class="bi bi-eye"></i>
|
|
100
100
|
<% end %>
|
|
101
101
|
</td>
|
|
@@ -205,7 +205,7 @@
|
|
|
205
205
|
should_show = show_if_key.nil? || @config.send(show_if_key)
|
|
206
206
|
next unless should_show
|
|
207
207
|
|
|
208
|
-
value = @config.send(setting_key)
|
|
208
|
+
value = @config.send(setting_meta[:method] || setting_key)
|
|
209
209
|
%>
|
|
210
210
|
<div class="col-md-6 mb-3">
|
|
211
211
|
<div class="d-flex justify-content-between align-items-center">
|
|
@@ -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
|
|
219
105
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
127
|
+
|
|
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
|
|
@@ -421,6 +388,7 @@ module RailsErrorDashboard
|
|
|
421
388
|
say " ✓ Dashboard UI", :green
|
|
422
389
|
say " ✓ Real-time Updates", :green
|
|
423
390
|
say " ✓ Analytics", :green
|
|
391
|
+
say " ✓ Async Logging (Rails :async adapter — no extra infrastructure needed)", :green
|
|
424
392
|
|
|
425
393
|
# Count optional features enabled
|
|
426
394
|
enabled_count = 0
|
|
@@ -496,7 +464,16 @@ module RailsErrorDashboard
|
|
|
496
464
|
say " → Set DISCORD_WEBHOOK_URL in .env", :yellow if @enable_discord
|
|
497
465
|
say " → Set PAGERDUTY_INTEGRATION_KEY in .env", :yellow if @enable_pagerduty
|
|
498
466
|
say " → Set WEBHOOK_URLS in .env", :yellow if @enable_webhooks
|
|
499
|
-
|
|
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
|
|
500
477
|
|
|
501
478
|
# Database-specific instructions
|
|
502
479
|
if @enable_separate_database
|
|
@@ -554,6 +531,36 @@ module RailsErrorDashboard
|
|
|
554
531
|
|
|
555
532
|
private
|
|
556
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
|
+
|
|
557
564
|
def detect_application_name
|
|
558
565
|
if defined?(Rails) && Rails.application
|
|
559
566
|
Rails.application.class.module_parent_name
|
|
@@ -138,17 +138,19 @@ RailsErrorDashboard.configure do |config|
|
|
|
138
138
|
|
|
139
139
|
<% if @enable_async_logging -%>
|
|
140
140
|
# Async Error Logging - ENABLED
|
|
141
|
-
# Errors
|
|
141
|
+
# Errors are logged in background jobs — zero impact on request response time.
|
|
142
|
+
# Default adapter is :async (Rails built-in, no extra infrastructure needed).
|
|
143
|
+
# Swap to :sidekiq or :solid_queue when you have a background worker running.
|
|
142
144
|
config.async_logging = true
|
|
143
|
-
config.async_adapter = :
|
|
145
|
+
config.async_adapter = :async # Options: :async (built-in), :sidekiq, :solid_queue
|
|
144
146
|
# To disable: Set config.async_logging = false
|
|
145
147
|
|
|
146
148
|
<% else -%>
|
|
147
149
|
# Async Error Logging - DISABLED
|
|
148
|
-
# Errors are logged synchronously (
|
|
149
|
-
# To enable: Set config.async_logging = true
|
|
150
|
+
# Errors are logged synchronously (adds ~1-5ms to requests that trigger errors)
|
|
151
|
+
# To enable: Set config.async_logging = true
|
|
150
152
|
config.async_logging = false
|
|
151
|
-
# config.async_adapter = :
|
|
153
|
+
# config.async_adapter = :async # Options: :async (built-in), :sidekiq, :solid_queue
|
|
152
154
|
|
|
153
155
|
<% end -%>
|
|
154
156
|
# Backtrace size limiting (100 lines is industry standard: Rollbar, Airbrake, Bugsnag)
|
|
@@ -156,15 +158,18 @@ RailsErrorDashboard.configure do |config|
|
|
|
156
158
|
|
|
157
159
|
<% if @enable_error_sampling -%>
|
|
158
160
|
# Error Sampling - ENABLED
|
|
159
|
-
#
|
|
160
|
-
# Critical errors are ALWAYS logged regardless of
|
|
161
|
-
|
|
161
|
+
# Samples non-critical errors to reduce storage volume.
|
|
162
|
+
# Critical and high severity errors are ALWAYS logged at 100% regardless of this setting.
|
|
163
|
+
# 0.5 = log 50% of non-critical occurrences — halves storage while keeping
|
|
164
|
+
# occurrence counts meaningful and error patterns visible.
|
|
165
|
+
# Tune lower (e.g. 0.1) if one error is flooding the DB; set to 1.0 to log everything.
|
|
166
|
+
config.sampling_rate = 0.5 # 50% of non-critical errors
|
|
162
167
|
# To disable: Set config.sampling_rate = 1.0 (100%)
|
|
163
168
|
|
|
164
169
|
<% else -%>
|
|
165
170
|
# Error Sampling - DISABLED
|
|
166
|
-
# All errors are logged (100% sampling rate)
|
|
167
|
-
# To enable: Set config.sampling_rate < 1.0 (e.g., 0.1 for 10%)
|
|
171
|
+
# All errors are logged (100% sampling rate).
|
|
172
|
+
# To enable: Set config.sampling_rate < 1.0 (e.g., 0.5 for 50%, 0.1 for 10%)
|
|
168
173
|
config.sampling_rate = 1.0
|
|
169
174
|
|
|
170
175
|
<% end -%>
|
|
@@ -511,44 +511,56 @@ module RailsErrorDashboard
|
|
|
511
511
|
@enable_activestorage_tracking = false
|
|
512
512
|
end
|
|
513
513
|
|
|
514
|
+
# Skip credential/service-dependent validations during Docker builds.
|
|
515
|
+
# SECRET_KEY_BASE_DUMMY=1 means no credentials or external services available.
|
|
516
|
+
build_env = ENV["SECRET_KEY_BASE_DUMMY"].present?
|
|
517
|
+
|
|
514
518
|
# Validate issue tracking configuration
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
519
|
+
unless build_env
|
|
520
|
+
if enable_issue_tracking && effective_issue_tracker_token.blank?
|
|
521
|
+
warnings << "enable_issue_tracking is true but no token configured. " \
|
|
522
|
+
"Set issue_tracker_token or RED_BOT_TOKEN env var. " \
|
|
523
|
+
"Tip: Create a dedicated RED (Rails Error Dashboard) bot account on your platform."
|
|
524
|
+
end
|
|
520
525
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
526
|
+
if enable_issue_tracking && effective_issue_tracker_provider.nil?
|
|
527
|
+
warnings << "enable_issue_tracking is true but provider could not be detected. " \
|
|
528
|
+
"Set issue_tracker_provider or git_repository_url."
|
|
529
|
+
end
|
|
524
530
|
end
|
|
525
531
|
|
|
526
|
-
# Validate crash capture path
|
|
527
|
-
if enable_crash_capture && crash_capture_path
|
|
532
|
+
# Validate crash capture path — auto-create if missing
|
|
533
|
+
if enable_crash_capture && crash_capture_path && !build_env
|
|
528
534
|
unless Dir.exist?(crash_capture_path)
|
|
529
|
-
|
|
535
|
+
begin
|
|
536
|
+
FileUtils.mkdir_p(crash_capture_path)
|
|
537
|
+
rescue => e
|
|
538
|
+
errors << "crash_capture_path '#{crash_capture_path}' could not be created: #{e.message}"
|
|
539
|
+
end
|
|
530
540
|
end
|
|
531
541
|
end
|
|
532
542
|
|
|
533
|
-
# Validate notification dependencies
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
543
|
+
# Validate notification dependencies (skip during builds — credentials unavailable)
|
|
544
|
+
unless build_env
|
|
545
|
+
if enable_slack_notifications && (slack_webhook_url.nil? || slack_webhook_url.strip.empty?)
|
|
546
|
+
errors << "slack_webhook_url is required when enable_slack_notifications is true"
|
|
547
|
+
end
|
|
537
548
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
549
|
+
if enable_email_notifications && notification_email_recipients.empty?
|
|
550
|
+
errors << "notification_email_recipients is required when enable_email_notifications is true"
|
|
551
|
+
end
|
|
541
552
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
553
|
+
if enable_discord_notifications && (discord_webhook_url.nil? || discord_webhook_url.strip.empty?)
|
|
554
|
+
errors << "discord_webhook_url is required when enable_discord_notifications is true"
|
|
555
|
+
end
|
|
545
556
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
557
|
+
if enable_pagerduty_notifications && (pagerduty_integration_key.nil? || pagerduty_integration_key.strip.empty?)
|
|
558
|
+
errors << "pagerduty_integration_key is required when enable_pagerduty_notifications is true"
|
|
559
|
+
end
|
|
549
560
|
|
|
550
|
-
|
|
551
|
-
|
|
561
|
+
if enable_webhook_notifications && webhook_urls.empty?
|
|
562
|
+
errors << "webhook_urls is required when enable_webhook_notifications is true"
|
|
563
|
+
end
|
|
552
564
|
end
|
|
553
565
|
|
|
554
566
|
# Validate separate database configuration
|
|
@@ -61,6 +61,16 @@ module RailsErrorDashboard
|
|
|
61
61
|
|
|
62
62
|
# Subscribe to Rails error reporter
|
|
63
63
|
config.after_initialize do
|
|
64
|
+
# Skip all runtime features during Docker asset precompilation.
|
|
65
|
+
# SECRET_KEY_BASE_DUMMY=1 signals a build environment — no database,
|
|
66
|
+
# no credentials, no external services are available. Activating
|
|
67
|
+
# TracePoint hooks, error subscribers, or background jobs here causes
|
|
68
|
+
# infinite retry loops and connection failures (issues #1-5).
|
|
69
|
+
if ENV["SECRET_KEY_BASE_DUMMY"].present?
|
|
70
|
+
Rails.logger.info "[Rails Error Dashboard] Build environment detected (SECRET_KEY_BASE_DUMMY) — skipping runtime features."
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
|
|
64
74
|
if RailsErrorDashboard.configuration.enable_error_subscriber
|
|
65
75
|
Rails.error.subscribe(RailsErrorDashboard::ErrorReporter.new)
|
|
66
76
|
end
|
|
@@ -24,6 +24,32 @@ module RailsErrorDashboard
|
|
|
24
24
|
|
|
25
25
|
# CRITICAL: Wrap entire process in rescue to ensure failures don't break the app
|
|
26
26
|
begin
|
|
27
|
+
# Enrich context with request data from Thread.current when available.
|
|
28
|
+
# Rails internals (ActionDispatch::Executor) report errors with
|
|
29
|
+
# source: "application.action_dispatch" but pass NO request object,
|
|
30
|
+
# resulting in placeholder values ("Rails Application", "{}", etc.).
|
|
31
|
+
# Our middleware stores the Rack env in Thread.current so we can
|
|
32
|
+
# build a proper request here — fixing issue #106.
|
|
33
|
+
if context[:request].nil? && Thread.current[:rails_error_dashboard_request_env]
|
|
34
|
+
env = Thread.current[:rails_error_dashboard_request_env]
|
|
35
|
+
context = context.merge(request: ActionDispatch::Request.new(env))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Skip duplicate reports from our own middleware when the subscriber
|
|
39
|
+
# already captured this error with full request context above.
|
|
40
|
+
# Without this, async logging enqueues two jobs for one exception —
|
|
41
|
+
# and non-deterministic job ordering can overwrite good data.
|
|
42
|
+
if source == "rack.middleware" &&
|
|
43
|
+
Thread.current[:rails_error_dashboard_reported_errors]&.include?(error.object_id)
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Track that we've reported this error (for dedup with middleware)
|
|
48
|
+
if Thread.current[:rails_error_dashboard_request_env]
|
|
49
|
+
Thread.current[:rails_error_dashboard_reported_errors] ||= Set.new
|
|
50
|
+
Thread.current[:rails_error_dashboard_reported_errors].add(error.object_id)
|
|
51
|
+
end
|
|
52
|
+
|
|
27
53
|
# Extract context information
|
|
28
54
|
error_context = ValueObjects::ErrorContext.new(context, source)
|
|
29
55
|
|
|
@@ -23,6 +23,11 @@ module RailsErrorDashboard
|
|
|
23
23
|
# Record request start time for duration calculation
|
|
24
24
|
env["rails_error_dashboard.request_start"] = Time.now.to_f
|
|
25
25
|
|
|
26
|
+
# Store request env in thread-local so the ErrorReporter subscriber
|
|
27
|
+
# can access request context when Rails.error.report fires from
|
|
28
|
+
# Rails internals (e.g., ActionDispatch::ShowExceptions)
|
|
29
|
+
Thread.current[:rails_error_dashboard_request_env] = env
|
|
30
|
+
|
|
26
31
|
# Initialize breadcrumb buffer for this request
|
|
27
32
|
if RailsErrorDashboard.configuration.enable_breadcrumbs
|
|
28
33
|
RailsErrorDashboard::Services::BreadcrumbCollector.init_buffer
|
|
@@ -52,6 +57,8 @@ module RailsErrorDashboard
|
|
|
52
57
|
raise exception
|
|
53
58
|
ensure
|
|
54
59
|
# CRITICAL: Always clean up thread-local storage (Puma reuses threads)
|
|
60
|
+
Thread.current[:rails_error_dashboard_request_env] = nil
|
|
61
|
+
Thread.current[:rails_error_dashboard_reported_errors] = nil
|
|
55
62
|
RailsErrorDashboard::Services::BreadcrumbCollector.clear_buffer
|
|
56
63
|
end
|
|
57
64
|
end
|
|
@@ -110,6 +110,11 @@ module RailsErrorDashboard
|
|
|
110
110
|
# Additional context (from mobile apps, etc.)
|
|
111
111
|
params.merge!(@context[:additional_context]) if @context[:additional_context]
|
|
112
112
|
|
|
113
|
+
# Pre-serialized params (from async logging or double-ErrorContext path).
|
|
114
|
+
# LogError creates a second ErrorContext from error_context.to_h which
|
|
115
|
+
# has :request_params as a JSON string but no :request object.
|
|
116
|
+
return @context[:request_params] if params.empty? && @context[:request_params].present?
|
|
117
|
+
|
|
113
118
|
params.to_json
|
|
114
119
|
end
|
|
115
120
|
|
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.5.
|
|
4
|
+
version: 0.5.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anjan Jagirdar
|
|
@@ -496,7 +496,7 @@ metadata:
|
|
|
496
496
|
funding_uri: https://github.com/sponsors/AnjanJ
|
|
497
497
|
post_install_message: |
|
|
498
498
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
499
|
-
RED (Rails Error Dashboard) v0.5.
|
|
499
|
+
RED (Rails Error Dashboard) v0.5.12
|
|
500
500
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
501
501
|
|
|
502
502
|
First install:
|