rails_error_dashboard 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +96 -14
- data/app/controllers/rails_error_dashboard/errors_controller.rb +139 -1
- data/app/helpers/rails_error_dashboard/application_helper.rb +25 -0
- data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +18 -6
- data/app/views/layouts/rails_error_dashboard.html.erb +157 -1
- data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +236 -0
- data/app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb +70 -0
- data/app/views/rails_error_dashboard/errors/_discussion.html.erb +107 -0
- data/app/views/rails_error_dashboard/errors/_error_cascades.html.erb +138 -0
- data/app/views/rails_error_dashboard/errors/_error_info.html.erb +190 -0
- data/app/views/rails_error_dashboard/errors/_modals.html.erb +139 -0
- data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_request_context.html.erb +108 -0
- data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +156 -0
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +352 -0
- data/app/views/rails_error_dashboard/errors/_similar_errors.html.erb +75 -0
- data/app/views/rails_error_dashboard/errors/_timeline.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/cache_health_summary.html.erb +143 -0
- data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +450 -0
- data/app/views/rails_error_dashboard/errors/deprecations.html.erb +129 -0
- data/app/views/rails_error_dashboard/errors/job_health_summary.html.erb +152 -0
- data/app/views/rails_error_dashboard/errors/n_plus_one_summary.html.erb +134 -0
- data/app/views/rails_error_dashboard/errors/settings.html.erb +17 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +20 -1132
- data/config/routes.rb +5 -0
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +6 -0
- data/db/migrate/20260303000001_add_breadcrumbs_to_error_logs.rb +9 -0
- data/db/migrate/20260304000001_add_system_health_to_error_logs.rb +12 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +31 -3
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +67 -5
- data/lib/rails_error_dashboard/commands/log_error.rb +33 -0
- data/lib/rails_error_dashboard/configuration.rb +45 -3
- data/lib/rails_error_dashboard/engine.rb +6 -1
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +8 -0
- data/lib/rails_error_dashboard/queries/cache_health_summary.rb +72 -0
- data/lib/rails_error_dashboard/queries/database_health_summary.rb +82 -0
- data/lib/rails_error_dashboard/queries/deprecation_warnings.rb +80 -0
- data/lib/rails_error_dashboard/queries/job_health_summary.rb +101 -0
- data/lib/rails_error_dashboard/queries/n_plus_one_summary.rb +83 -0
- data/lib/rails_error_dashboard/services/breadcrumb_collector.rb +182 -0
- data/lib/rails_error_dashboard/services/cache_analyzer.rb +76 -0
- data/lib/rails_error_dashboard/services/curl_generator.rb +80 -0
- data/lib/rails_error_dashboard/services/database_health_inspector.rb +168 -0
- data/lib/rails_error_dashboard/services/n_plus_one_detector.rb +74 -0
- data/lib/rails_error_dashboard/services/rspec_generator.rb +145 -0
- data/lib/rails_error_dashboard/services/system_health_snapshot.rb +145 -0
- data/lib/rails_error_dashboard/subscribers/breadcrumb_subscriber.rb +210 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +24 -0
- data/lib/tasks/error_dashboard.rake +68 -2
- metadata +33 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f95521c0c769dd28fdd0934540282c64f32a8d23a8c6be9a63805326384b1c01
|
|
4
|
+
data.tar.gz: 3c66324e1e40e8fa0b474a6566babe67eb831927269f37a0108e77db6c928749
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ca327e64bea48cc347e96969db0d2da76c4ffe347ba5aad41c8576b6f18b5696fdd64d6df8f910c2b8df4a64dafc2f75f925f9aa9610b20346535d5e05ffbfc
|
|
7
|
+
data.tar.gz: 5188358be68c4831caff5808db350058f334bc9d9e3310acd4bccab8ebef667785035b7481fd2f46ed5bc12e793f4b481f44ae15765babf410ea9292dedfb3d4
|
data/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
[](https://rubygems.org/gems/rails_error_dashboard)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://github.com/AnjanJ/rails_error_dashboard/actions)
|
|
7
|
+
[](https://buymeacoffee.com/anjanj)
|
|
7
8
|
|
|
8
9
|
## Self-hosted Rails error monitoring — free, forever.
|
|
9
10
|
|
|
@@ -41,10 +42,30 @@ Experience the full dashboard with 480+ realistic Rails errors, LOTR-themed demo
|
|
|
41
42
|
|
|
42
43
|

|
|
43
44
|
|
|
45
|
+
**Deprecation Warnings** — Aggregate deprecation warnings across all errors with occurrence counts and affected error links.
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
**N+1 Query Patterns** — Cross-error N+1 detection grouped by SQL fingerprint.
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
**Cache Health** — Per-error cache performance sorted worst-first.
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
**Job Health** — Background job queue stats across errors, sorted by failed count.
|
|
58
|
+
|
|
59
|
+

|
|
60
|
+
|
|
61
|
+
**Database Health** — Connection pool utilization, PostgreSQL table stats, and per-error stress scores.
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
|
|
44
65
|
---
|
|
45
66
|
|
|
46
67
|
### ⚠️ BETA SOFTWARE
|
|
47
|
-
This Rails Engine is in beta and under active development. While functional and tested (
|
|
68
|
+
This Rails Engine is in beta and under active development. While functional and tested (2,226+ tests passing, including browser-based system tests), the API may change before v1.0.0. Use in production at your own discretion.
|
|
48
69
|
|
|
49
70
|
**Supports**: Rails 7.0 - 8.1 | Ruby 3.2 - 4.0
|
|
50
71
|
|
|
@@ -112,7 +133,7 @@ Modern Bootstrap 5 UI with dark/light mode, responsive design, real-time statist
|
|
|
112
133
|
Error assignment and status tracking, priority levels (critical/high/medium/low), snooze functionality, comment threads, batch operations (bulk resolve/delete), resolution tracking with references.
|
|
113
134
|
|
|
114
135
|
#### 🔒 Security & Privacy
|
|
115
|
-
HTTP Basic Auth,
|
|
136
|
+
HTTP Basic Auth or custom authentication via `config.authenticate_with` lambda (Devise, Warden, session-based — runs in controller context with access to `warden`, `session`, `request`). Environment-based settings, optional separate database for isolation. Your data stays on your server - no third-party access.
|
|
116
137
|
|
|
117
138
|
### Optional Features (Choose During Install)
|
|
118
139
|
|
|
@@ -158,7 +179,7 @@ Detect cyclical patterns (business hours, nighttime, weekend rhythms) and error
|
|
|
158
179
|
**Plus: Developer Insights Dashboard** 💡
|
|
159
180
|
Built-in analytics dashboard with severity detection, platform stability scoring, actionable recommendations, and recent error activity summaries (always available, no configuration needed).
|
|
160
181
|
|
|
161
|
-
#### 🔍 Source Code Integration
|
|
182
|
+
#### 🔍 Source Code Integration
|
|
162
183
|
|
|
163
184
|
**View actual source code directly in error backtraces** - no need to switch to your editor or GitHub.
|
|
164
185
|
|
|
@@ -168,22 +189,74 @@ Built-in analytics dashboard with severity detection, platform stability scoring
|
|
|
168
189
|
- **Smart Caching** - Fast performance with 1-hour cache (configurable)
|
|
169
190
|
- **Security Controls** - Only shows your app code by default (not gems/frameworks)
|
|
170
191
|
|
|
171
|
-
**Perfect for debugging:**
|
|
172
|
-
- Understand the code context without leaving the dashboard
|
|
173
|
-
- Identify code ownership with git blame
|
|
174
|
-
- Quick navigation to your repository
|
|
175
|
-
- See recent changes that might have caused the error
|
|
176
|
-
|
|
177
192
|
```ruby
|
|
178
|
-
# Enable in config/initializers/rails_error_dashboard.rb
|
|
179
193
|
config.enable_source_code_integration = true
|
|
180
|
-
config.source_code_context_lines = 7
|
|
181
194
|
config.enable_git_blame = true
|
|
182
|
-
config.git_repository_url = "https://github.com/user/repo"
|
|
183
195
|
```
|
|
184
196
|
|
|
185
197
|
**📖 [Complete documentation →](docs/SOURCE_CODE_INTEGRATION.md)**
|
|
186
198
|
|
|
199
|
+
#### 🥖 Breadcrumbs — Request Activity Trail (NEW!)
|
|
200
|
+
|
|
201
|
+
**See exactly what happened before the crash** — SQL queries, controller actions, cache operations, job executions, and mailer deliveries captured automatically via `ActiveSupport::Notifications`.
|
|
202
|
+
|
|
203
|
+
- **Automatic capture** — Zero config beyond the enable flag (Rails already emits the events)
|
|
204
|
+
- **Timeline display** — Color-coded event list on each error's detail page
|
|
205
|
+
- **Deprecation warnings** — `deprecation.rails` events captured with caller location
|
|
206
|
+
- **N+1 detection** — Repeated SQL patterns flagged automatically at display time
|
|
207
|
+
- **Custom breadcrumbs** — `RailsErrorDashboard.add_breadcrumb("checkout started", { cart_id: 123 })`
|
|
208
|
+
- **Safe by design** — Fixed-size ring buffer, thread-local, every subscriber wrapped in rescue
|
|
209
|
+
- **Async-compatible** — Breadcrumbs harvested before background job dispatch
|
|
210
|
+
- **Deprecation warnings page** — Aggregate view across all errors at `/errors/deprecations`
|
|
211
|
+
- **N+1 query patterns page** — Cross-error N+1 analysis at `/errors/n_plus_one_summary`
|
|
212
|
+
- **Cache health page** — App-wide cache performance sorted worst-first at `/errors/cache_health_summary`
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
config.enable_breadcrumbs = true
|
|
216
|
+
config.breadcrumb_buffer_size = 40 # Max events per request
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**📖 [Complete documentation →](docs/FEATURES.md#breadcrumbs--request-activity-trail-new)**
|
|
220
|
+
|
|
221
|
+
#### 💓 System Health Snapshot (NEW!)
|
|
222
|
+
|
|
223
|
+
**Know your app's runtime state at the moment of failure** — GC stats, process memory, thread count, connection pool utilization, and Puma thread stats captured automatically when errors occur.
|
|
224
|
+
|
|
225
|
+
- **GC stats** — Heap live/free slots, major GC count, total allocated objects
|
|
226
|
+
- **Process memory** — RSS in MB (Linux procfs only, no subprocess/fork)
|
|
227
|
+
- **Thread count** — `Thread.list.count` (O(1), safe)
|
|
228
|
+
- **Connection pool** — Size, busy, idle, dead, waiting connections
|
|
229
|
+
- **Puma stats** — Running threads, max threads, pool capacity, backlog
|
|
230
|
+
- **Sub-millisecond** — Total snapshot < 1ms, every metric individually rescue-wrapped
|
|
231
|
+
- **Safe by design** — No ObjectSpace scanning, no Thread backtraces, no subprocess calls
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
config.enable_system_health = true
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**📖 [Complete documentation →](docs/FEATURES.md#system-health-snapshot-new)**
|
|
238
|
+
|
|
239
|
+
#### 🏭 Job Health Page
|
|
240
|
+
|
|
241
|
+
**See background job queue health alongside your errors** — auto-detects Sidekiq, SolidQueue, or GoodJob stats captured at error time.
|
|
242
|
+
|
|
243
|
+
- **Per-error table** — Adapter badge, failed count (color-coded), queued/enqueued, other stats
|
|
244
|
+
- **Summary cards** — Errors with job data, total failed, adapters detected
|
|
245
|
+
- **Sorted worst-first** — Highest failed count first
|
|
246
|
+
|
|
247
|
+
**📖 [Complete documentation →](docs/FEATURES.md#job-health-page)**
|
|
248
|
+
|
|
249
|
+
#### 🗄️ Database Health Page
|
|
250
|
+
|
|
251
|
+
**PgHero-style database health built into the dashboard** — live PostgreSQL stats + historical connection pool data from error snapshots.
|
|
252
|
+
|
|
253
|
+
- **Live stats** (PostgreSQL) — Table sizes, unused indexes, dead tuples, vacuum timestamps, connection activity
|
|
254
|
+
- **Historical pool data** (all adapters) — Per-error connection pool utilization, sorted by stress score
|
|
255
|
+
- **Color-coded** — Utilization >=80% danger, >=60% warning; dead/waiting badges
|
|
256
|
+
- **Non-PG friendly** — SQLite/MySQL still see connection pool stats and historical data
|
|
257
|
+
|
|
258
|
+
**📖 [Complete documentation →](docs/FEATURES.md#database-health-page)**
|
|
259
|
+
|
|
187
260
|
#### 🆕 v0.2 Quick Wins (NEW!)
|
|
188
261
|
|
|
189
262
|
**11 features that make error tracking smarter, safer, and more actionable:**
|
|
@@ -318,6 +391,9 @@ RailsErrorDashboard.configure do |config|
|
|
|
318
391
|
config.dashboard_username = ENV.fetch('ERROR_DASHBOARD_USER', 'gandalf')
|
|
319
392
|
config.dashboard_password = ENV.fetch('ERROR_DASHBOARD_PASSWORD', 'youshallnotpass')
|
|
320
393
|
|
|
394
|
+
# Or use your existing auth (Devise, Warden, etc.) instead of Basic Auth:
|
|
395
|
+
# config.authenticate_with = -> { warden.authenticated? }
|
|
396
|
+
|
|
321
397
|
# ============================================================================
|
|
322
398
|
# OPTIONAL FEATURES (Enable as needed)
|
|
323
399
|
# ============================================================================
|
|
@@ -671,7 +747,7 @@ Clean, maintainable, testable architecture you can understand and modify.
|
|
|
671
747
|
|
|
672
748
|
## 🧪 Testing
|
|
673
749
|
|
|
674
|
-
|
|
750
|
+
2,100+ tests covering unit, integration, and browser-based system tests.
|
|
675
751
|
|
|
676
752
|
### Running Tests
|
|
677
753
|
|
|
@@ -764,7 +840,7 @@ Rails Error Dashboard is available as open source under the terms of the [MIT Li
|
|
|
764
840
|
<details>
|
|
765
841
|
<summary><strong>Is this production-ready?</strong></summary>
|
|
766
842
|
|
|
767
|
-
This is currently in **beta** but actively tested with
|
|
843
|
+
This is currently in **beta** but actively tested with 2,100+ passing tests across Rails 7.0-8.1 and Ruby 3.2-4.0. Many users are running it in production. See [production requirements](docs/FEATURES.md#production-readiness).
|
|
768
844
|
</details>
|
|
769
845
|
|
|
770
846
|
<details>
|
|
@@ -973,6 +1049,12 @@ Want to contribute? Check out our [Contributing Guide](CONTRIBUTING.md)!
|
|
|
973
1049
|
|
|
974
1050
|
---
|
|
975
1051
|
|
|
1052
|
+
## Support
|
|
1053
|
+
|
|
1054
|
+
If this gem saves you some headaches (or some money on error tracking SaaS), consider [buying me a coffee](https://buymeacoffee.com/anjanj). It keeps the project going and lets me know people are finding it useful.
|
|
1055
|
+
|
|
1056
|
+
---
|
|
1057
|
+
|
|
976
1058
|
**Made with ❤️ by [Anjan](https://www.anjan.dev) for the Rails community**
|
|
977
1059
|
|
|
978
1060
|
*One Gem to rule them all, One Gem to find them, One Gem to bring them all, and in the dashboard bind them.* 🧙♂️
|
|
@@ -236,6 +236,118 @@ module RailsErrorDashboard
|
|
|
236
236
|
@platform_specific_errors = correlation.platform_specific_errors
|
|
237
237
|
end
|
|
238
238
|
|
|
239
|
+
def deprecations
|
|
240
|
+
unless RailsErrorDashboard.configuration.enable_breadcrumbs
|
|
241
|
+
flash[:alert] = "Breadcrumbs are not enabled. Enable them in config/initializers/rails_error_dashboard.rb"
|
|
242
|
+
redirect_to errors_path
|
|
243
|
+
return
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
days = (params[:days] || 30).to_i
|
|
247
|
+
@days = days
|
|
248
|
+
result = Queries::DeprecationWarnings.call(days, application_id: @current_application_id)
|
|
249
|
+
all_deprecations = result[:deprecations]
|
|
250
|
+
|
|
251
|
+
# Summary stats (computed before pagination)
|
|
252
|
+
@unique_count = all_deprecations.size
|
|
253
|
+
@total_count = all_deprecations.sum { |d| d[:count] }
|
|
254
|
+
@affected_count = all_deprecations.flat_map { |d| d[:error_ids] }.uniq.size
|
|
255
|
+
|
|
256
|
+
@pagy, @deprecations = pagy(:offset, all_deprecations, limit: params[:per_page] || 25)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def n_plus_one_summary
|
|
260
|
+
unless RailsErrorDashboard.configuration.enable_breadcrumbs
|
|
261
|
+
flash[:alert] = "Breadcrumbs are not enabled. Enable them in config/initializers/rails_error_dashboard.rb"
|
|
262
|
+
redirect_to errors_path
|
|
263
|
+
return
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
days = (params[:days] || 30).to_i
|
|
267
|
+
@days = days
|
|
268
|
+
result = Queries::NplusOneSummary.call(days, application_id: @current_application_id)
|
|
269
|
+
all_patterns = result[:patterns]
|
|
270
|
+
|
|
271
|
+
# Summary stats (computed before pagination)
|
|
272
|
+
@unique_count = all_patterns.size
|
|
273
|
+
@total_count = all_patterns.sum { |p| p[:count] }
|
|
274
|
+
@affected_count = all_patterns.flat_map { |p| p[:error_ids] }.uniq.size
|
|
275
|
+
|
|
276
|
+
@pagy, @patterns = pagy(:offset, all_patterns, limit: params[:per_page] || 25)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def cache_health_summary
|
|
280
|
+
unless RailsErrorDashboard.configuration.enable_breadcrumbs
|
|
281
|
+
flash[:alert] = "Breadcrumbs are not enabled. Enable them in config/initializers/rails_error_dashboard.rb"
|
|
282
|
+
redirect_to errors_path
|
|
283
|
+
return
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
days = (params[:days] || 30).to_i
|
|
287
|
+
@days = days
|
|
288
|
+
result = Queries::CacheHealthSummary.call(days, application_id: @current_application_id)
|
|
289
|
+
all_entries = result[:entries]
|
|
290
|
+
|
|
291
|
+
# Summary stats (computed before pagination)
|
|
292
|
+
@errors_with_cache = all_entries.size
|
|
293
|
+
non_nil_rates = all_entries.map { |e| e[:hit_rate] }.compact
|
|
294
|
+
@avg_hit_rate = non_nil_rates.any? ? (non_nil_rates.sum / non_nil_rates.size).round(1) : nil
|
|
295
|
+
@total_cache_ops = all_entries.sum { |e| e[:reads] + e[:writes] }
|
|
296
|
+
|
|
297
|
+
@pagy, @entries = pagy(:offset, all_entries, limit: params[:per_page] || 25)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def job_health_summary
|
|
301
|
+
unless RailsErrorDashboard.configuration.enable_system_health
|
|
302
|
+
flash[:alert] = "System health is not enabled. Enable it in config/initializers/rails_error_dashboard.rb"
|
|
303
|
+
redirect_to errors_path
|
|
304
|
+
return
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
days = (params[:days] || 30).to_i
|
|
308
|
+
@days = days
|
|
309
|
+
result = Queries::JobHealthSummary.call(days, application_id: @current_application_id)
|
|
310
|
+
all_entries = result[:entries]
|
|
311
|
+
|
|
312
|
+
# Summary stats (computed before pagination)
|
|
313
|
+
@errors_with_jobs = all_entries.size
|
|
314
|
+
@total_failed = all_entries.sum { |e| e[:failed] || e[:errored] || 0 }
|
|
315
|
+
@adapters_detected = all_entries.map { |e| e[:adapter] }.uniq
|
|
316
|
+
|
|
317
|
+
@pagy, @entries = pagy(:offset, all_entries, limit: params[:per_page] || 25)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def database_health_summary
|
|
321
|
+
unless RailsErrorDashboard.configuration.enable_system_health
|
|
322
|
+
flash[:alert] = "System health is not enabled. Enable it in config/initializers/rails_error_dashboard.rb"
|
|
323
|
+
redirect_to errors_path
|
|
324
|
+
return
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
days = (params[:days] || 30).to_i
|
|
328
|
+
@days = days
|
|
329
|
+
|
|
330
|
+
# Live database health (display-time only)
|
|
331
|
+
@live_health = Services::DatabaseHealthInspector.call
|
|
332
|
+
|
|
333
|
+
# Separate host vs gem tables from live data
|
|
334
|
+
all_tables = @live_health[:tables] || []
|
|
335
|
+
@host_tables = all_tables.reject { |t| t[:gem_table] }
|
|
336
|
+
@gem_tables = all_tables.select { |t| t[:gem_table] }
|
|
337
|
+
|
|
338
|
+
# Historical connection pool stats
|
|
339
|
+
result = Queries::DatabaseHealthSummary.call(days, application_id: @current_application_id)
|
|
340
|
+
all_entries = result[:entries]
|
|
341
|
+
|
|
342
|
+
# Summary stats (computed before pagination)
|
|
343
|
+
@errors_with_pool = all_entries.size
|
|
344
|
+
@max_utilization = all_entries.map { |e| e[:utilization] }.max || 0
|
|
345
|
+
@total_dead = all_entries.sum { |e| e[:dead] }
|
|
346
|
+
@total_waiting = all_entries.sum { |e| e[:waiting] }
|
|
347
|
+
|
|
348
|
+
@pagy, @entries = pagy(:offset, all_entries, limit: params[:per_page] || 25)
|
|
349
|
+
end
|
|
350
|
+
|
|
239
351
|
def settings
|
|
240
352
|
@config = RailsErrorDashboard.configuration
|
|
241
353
|
end
|
|
@@ -272,7 +384,33 @@ module RailsErrorDashboard
|
|
|
272
384
|
end
|
|
273
385
|
|
|
274
386
|
def authenticate_dashboard_user!
|
|
275
|
-
|
|
387
|
+
auth_lambda = RailsErrorDashboard.configuration.authenticate_with
|
|
388
|
+
|
|
389
|
+
if auth_lambda
|
|
390
|
+
authenticate_with_lambda(auth_lambda)
|
|
391
|
+
else
|
|
392
|
+
authenticate_with_basic_auth
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def authenticate_with_lambda(auth_lambda)
|
|
397
|
+
authorized = begin
|
|
398
|
+
instance_exec(&auth_lambda)
|
|
399
|
+
rescue => e
|
|
400
|
+
Rails.logger.error(
|
|
401
|
+
"[RailsErrorDashboard] authenticate_with lambda raised #{e.class}: #{e.message}"
|
|
402
|
+
)
|
|
403
|
+
false
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
return if performed?
|
|
407
|
+
|
|
408
|
+
unless authorized
|
|
409
|
+
render plain: "Access Denied", status: :forbidden
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def authenticate_with_basic_auth
|
|
276
414
|
authenticate_or_request_with_http_basic do |username, password|
|
|
277
415
|
ActiveSupport::SecurityUtils.secure_compare(
|
|
278
416
|
username,
|
|
@@ -196,6 +196,31 @@ module RailsErrorDashboard
|
|
|
196
196
|
)
|
|
197
197
|
end
|
|
198
198
|
|
|
199
|
+
# Returns Bootstrap badge color class for breadcrumb category
|
|
200
|
+
# @param category [String] Breadcrumb category (sql, controller, cache, job, mailer, custom)
|
|
201
|
+
# @return [String] Bootstrap color class
|
|
202
|
+
def breadcrumb_badge_color(category)
|
|
203
|
+
case category.to_s
|
|
204
|
+
when "sql" then "primary"
|
|
205
|
+
when "controller" then "success"
|
|
206
|
+
when "cache" then "info"
|
|
207
|
+
when "job" then "warning"
|
|
208
|
+
when "mailer" then "secondary"
|
|
209
|
+
when "custom" then "dark"
|
|
210
|
+
when "deprecation" then "danger"
|
|
211
|
+
else "light"
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Extracts table name from a SQL query string
|
|
216
|
+
# @param sql [String] SQL query (e.g., 'SELECT "users".* FROM "users" WHERE ...')
|
|
217
|
+
# @return [String, nil] The table name or nil if not extractable
|
|
218
|
+
def extract_table_from_sql(sql)
|
|
219
|
+
return nil if sql.blank?
|
|
220
|
+
match = sql.match(/FROM\s+["`]?(\w+)["`]?/i)
|
|
221
|
+
match ? match[1] : nil
|
|
222
|
+
end
|
|
223
|
+
|
|
199
224
|
# Automatically converts URLs in text to clickable links that open in new window
|
|
200
225
|
# Also highlights inline code wrapped in backticks with syntax highlighting
|
|
201
226
|
# Also converts file paths to GitHub links if repository URL is configured
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module RailsErrorDashboard
|
|
4
4
|
# Background job to enforce the retention_days configuration.
|
|
5
5
|
# Deletes error logs (and their associated records) older than the configured threshold.
|
|
6
|
-
# Uses
|
|
6
|
+
# Uses batch deletion (in_batches + delete_all) for performance on large tables.
|
|
7
7
|
#
|
|
8
8
|
# Schedule this job daily via your preferred scheduler (SolidQueue, Sidekiq, cron).
|
|
9
9
|
#
|
|
@@ -20,13 +20,25 @@ module RailsErrorDashboard
|
|
|
20
20
|
return 0 if retention_days.blank?
|
|
21
21
|
|
|
22
22
|
cutoff = retention_days.days.ago
|
|
23
|
+
expired_scope = ErrorLog.where("occurred_at < ?", cutoff)
|
|
24
|
+
return 0 if expired_scope.none?
|
|
25
|
+
|
|
23
26
|
deleted_count = 0
|
|
24
27
|
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
# Delete dependents first, then errors — all in batches to prevent table locks
|
|
29
|
+
expired_ids_scope = expired_scope.select(:id)
|
|
30
|
+
|
|
31
|
+
# Batch delete dependent records (occurrences, comments, cascade patterns)
|
|
32
|
+
ErrorOccurrence.where(error_log_id: expired_ids_scope).in_batches(of: 1000).delete_all
|
|
33
|
+
ErrorComment.where(error_log_id: expired_ids_scope).in_batches(of: 1000).delete_all
|
|
34
|
+
CascadePattern.where(parent_error_id: expired_ids_scope)
|
|
35
|
+
.or(CascadePattern.where(child_error_id: expired_ids_scope))
|
|
36
|
+
.in_batches(of: 1000).delete_all
|
|
37
|
+
|
|
38
|
+
# Now batch delete the error logs themselves
|
|
39
|
+
expired_scope.in_batches(of: 1000) do |batch|
|
|
40
|
+
batch_size = batch.delete_all
|
|
41
|
+
deleted_count += batch_size
|
|
30
42
|
end
|
|
31
43
|
|
|
32
44
|
if deleted_count > 0
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<title><%= content_for?(:page_title) ? "#{content_for(:page_title)} | " : "" %><%= Rails.application.class.module_parent_name %> - Error Dashboard</title>
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<meta name="turbo-visit-control" content="reload">
|
|
6
7
|
<% if respond_to?(:csrf_meta_tags) %>
|
|
7
8
|
<%= csrf_meta_tags %>
|
|
8
9
|
<% end %>
|
|
@@ -1379,10 +1380,136 @@ body.dark-mode .skeleton {
|
|
|
1379
1380
|
vertical-align: middle;
|
|
1380
1381
|
margin-right: 0.25em;
|
|
1381
1382
|
}
|
|
1383
|
+
|
|
1384
|
+
/* Section Navigation */
|
|
1385
|
+
#section-nav-wrapper {
|
|
1386
|
+
position: sticky;
|
|
1387
|
+
top: 0;
|
|
1388
|
+
z-index: 1020;
|
|
1389
|
+
background: #f3f4f6;
|
|
1390
|
+
margin: 0 -12px;
|
|
1391
|
+
padding: 0 12px;
|
|
1392
|
+
transition: box-shadow 0.2s ease, background-color 0.3s;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
#section-nav-wrapper.is-stuck {
|
|
1396
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
1397
|
+
border-bottom: 1px solid #e5e7eb;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
.section-nav-scroll {
|
|
1401
|
+
scrollbar-width: none;
|
|
1402
|
+
-ms-overflow-style: none;
|
|
1403
|
+
mask-image: linear-gradient(to right, black 0, black calc(100% - 24px), transparent 100%);
|
|
1404
|
+
-webkit-mask-image: linear-gradient(to right, black 0, black calc(100% - 24px), transparent 100%);
|
|
1405
|
+
padding-right: 16px;
|
|
1406
|
+
}
|
|
1407
|
+
.section-nav-scroll::-webkit-scrollbar {
|
|
1408
|
+
display: none;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
.section-nav-label {
|
|
1412
|
+
font-size: 0.85rem;
|
|
1413
|
+
white-space: nowrap;
|
|
1414
|
+
color: #9ca3af !important;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
.section-nav-pill {
|
|
1418
|
+
display: inline-flex;
|
|
1419
|
+
align-items: center;
|
|
1420
|
+
gap: 4px;
|
|
1421
|
+
padding: 4px 10px;
|
|
1422
|
+
border-radius: 20px;
|
|
1423
|
+
font-size: 0.78rem;
|
|
1424
|
+
font-weight: 500;
|
|
1425
|
+
white-space: nowrap;
|
|
1426
|
+
text-decoration: none;
|
|
1427
|
+
color: #4b5563;
|
|
1428
|
+
background: white;
|
|
1429
|
+
border: 1px solid #e5e7eb;
|
|
1430
|
+
border: 1px solid transparent;
|
|
1431
|
+
transition: all 0.15s ease;
|
|
1432
|
+
cursor: pointer;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
.section-nav-pill:hover {
|
|
1436
|
+
color: #1f2937;
|
|
1437
|
+
background: #f3f4f6;
|
|
1438
|
+
text-decoration: none;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
.section-nav-pill.active {
|
|
1442
|
+
color: white;
|
|
1443
|
+
background: #3b82f6;
|
|
1444
|
+
border-color: #3b82f6;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
.section-nav-pill i {
|
|
1448
|
+
font-size: 0.75rem;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
/* Dark mode overrides for section nav */
|
|
1452
|
+
body.dark-mode #section-nav-wrapper { background: var(--ctp-base); }
|
|
1453
|
+
body.dark-mode #section-nav-wrapper.is-stuck { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); border-bottom-color: var(--ctp-surface0); }
|
|
1454
|
+
body.dark-mode .section-nav-label { color: var(--ctp-overlay2) !important; }
|
|
1455
|
+
body.dark-mode .section-nav-pill { color: var(--ctp-subtext0); background: var(--ctp-surface0); }
|
|
1456
|
+
body.dark-mode .section-nav-pill:hover { color: var(--ctp-text); background: var(--ctp-surface1); }
|
|
1457
|
+
body.dark-mode .section-nav-pill.active { color: var(--ctp-base); background: var(--ctp-blue); border-color: var(--ctp-blue); }
|
|
1458
|
+
|
|
1459
|
+
/* Sidebar sub-section panels */
|
|
1460
|
+
.sidebar-section {
|
|
1461
|
+
border-left: 3px solid #9ca3af;
|
|
1462
|
+
padding-left: 10px;
|
|
1463
|
+
margin-top: 16px;
|
|
1464
|
+
margin-bottom: 8px;
|
|
1465
|
+
}
|
|
1466
|
+
.sidebar-section-blue { border-left-color: #3b82f6; }
|
|
1467
|
+
.sidebar-section-red { border-left-color: #ef4444; }
|
|
1468
|
+
|
|
1469
|
+
.sidebar-section-title {
|
|
1470
|
+
font-size: 0.8rem;
|
|
1471
|
+
font-weight: 600;
|
|
1472
|
+
text-transform: uppercase;
|
|
1473
|
+
letter-spacing: 0.04em;
|
|
1474
|
+
color: #4b5563;
|
|
1475
|
+
margin-bottom: 2px;
|
|
1476
|
+
}
|
|
1477
|
+
.sidebar-section-title i { margin-right: 4px; }
|
|
1478
|
+
|
|
1479
|
+
.sidebar-section-hint {
|
|
1480
|
+
font-size: 0.72rem;
|
|
1481
|
+
color: #6b7280;
|
|
1482
|
+
margin-bottom: 6px;
|
|
1483
|
+
display: block;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
.sidebar-section-body {
|
|
1487
|
+
background: #f9fafb;
|
|
1488
|
+
border-radius: 6px;
|
|
1489
|
+
padding: 8px 10px;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/* Metadata field labels */
|
|
1493
|
+
.metadata-label {
|
|
1494
|
+
font-size: 0.7rem;
|
|
1495
|
+
font-weight: 600;
|
|
1496
|
+
text-transform: uppercase;
|
|
1497
|
+
letter-spacing: 0.05em;
|
|
1498
|
+
color: #4b5563;
|
|
1499
|
+
}
|
|
1500
|
+
body.dark-mode .metadata-label { color: var(--ctp-overlay2); }
|
|
1501
|
+
|
|
1502
|
+
/* Dark mode overrides */
|
|
1503
|
+
body.dark-mode .sidebar-section { border-left-color: var(--ctp-overlay2); }
|
|
1504
|
+
body.dark-mode .sidebar-section-blue { border-left-color: var(--ctp-blue); }
|
|
1505
|
+
body.dark-mode .sidebar-section-red { border-left-color: var(--ctp-red); }
|
|
1506
|
+
body.dark-mode .sidebar-section-title { color: var(--ctp-subtext1); }
|
|
1507
|
+
body.dark-mode .sidebar-section-hint { color: var(--ctp-overlay2); }
|
|
1508
|
+
body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
|
|
1382
1509
|
</style>
|
|
1383
1510
|
</head>
|
|
1384
1511
|
|
|
1385
|
-
<body>
|
|
1512
|
+
<body data-turbo="false">
|
|
1386
1513
|
<!-- Toast Container -->
|
|
1387
1514
|
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 9999;">
|
|
1388
1515
|
<!-- Toasts will be dynamically inserted here -->
|
|
@@ -1513,6 +1640,35 @@ body.dark-mode .skeleton {
|
|
|
1513
1640
|
<i class="bi bi-gear"></i> Settings
|
|
1514
1641
|
<% end %>
|
|
1515
1642
|
</li>
|
|
1643
|
+
<% if RailsErrorDashboard.configuration.enable_breadcrumbs %>
|
|
1644
|
+
<li class="nav-item">
|
|
1645
|
+
<%= link_to deprecations_errors_path(nav_params), class: "nav-link #{request.path == deprecations_errors_path ? 'active' : ''}" do %>
|
|
1646
|
+
<i class="bi bi-exclamation-triangle"></i> Deprecations
|
|
1647
|
+
<% end %>
|
|
1648
|
+
</li>
|
|
1649
|
+
<li class="nav-item">
|
|
1650
|
+
<%= link_to n_plus_one_summary_errors_path(nav_params), class: "nav-link #{request.path == n_plus_one_summary_errors_path ? 'active' : ''}" do %>
|
|
1651
|
+
<i class="bi bi-arrow-repeat"></i> N+1 Queries
|
|
1652
|
+
<% end %>
|
|
1653
|
+
</li>
|
|
1654
|
+
<li class="nav-item">
|
|
1655
|
+
<%= link_to cache_health_summary_errors_path(nav_params), class: "nav-link #{request.path == cache_health_summary_errors_path ? 'active' : ''}" do %>
|
|
1656
|
+
<i class="bi bi-lightning-charge"></i> Cache Health
|
|
1657
|
+
<% end %>
|
|
1658
|
+
</li>
|
|
1659
|
+
<% end %>
|
|
1660
|
+
<% if RailsErrorDashboard.configuration.enable_system_health %>
|
|
1661
|
+
<li class="nav-item">
|
|
1662
|
+
<%= link_to job_health_summary_errors_path(nav_params), class: "nav-link #{request.path == job_health_summary_errors_path ? 'active' : ''}" do %>
|
|
1663
|
+
<i class="bi bi-cpu"></i> Job Health
|
|
1664
|
+
<% end %>
|
|
1665
|
+
</li>
|
|
1666
|
+
<li class="nav-item">
|
|
1667
|
+
<%= link_to database_health_summary_errors_path(nav_params), class: "nav-link #{request.path == database_health_summary_errors_path ? 'active' : ''}" do %>
|
|
1668
|
+
<i class="bi bi-database"></i> DB Health
|
|
1669
|
+
<% end %>
|
|
1670
|
+
</li>
|
|
1671
|
+
<% end %>
|
|
1516
1672
|
</ul>
|
|
1517
1673
|
|
|
1518
1674
|
<h6 class="mt-4">QUICK FILTERS</h6>
|