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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +96 -14
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +139 -1
  4. data/app/helpers/rails_error_dashboard/application_helper.rb +25 -0
  5. data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +18 -6
  6. data/app/views/layouts/rails_error_dashboard.html.erb +157 -1
  7. data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +236 -0
  8. data/app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb +70 -0
  9. data/app/views/rails_error_dashboard/errors/_discussion.html.erb +107 -0
  10. data/app/views/rails_error_dashboard/errors/_error_cascades.html.erb +138 -0
  11. data/app/views/rails_error_dashboard/errors/_error_info.html.erb +190 -0
  12. data/app/views/rails_error_dashboard/errors/_modals.html.erb +139 -0
  13. data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +1 -1
  14. data/app/views/rails_error_dashboard/errors/_request_context.html.erb +108 -0
  15. data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +156 -0
  16. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +352 -0
  17. data/app/views/rails_error_dashboard/errors/_similar_errors.html.erb +75 -0
  18. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +1 -1
  19. data/app/views/rails_error_dashboard/errors/cache_health_summary.html.erb +143 -0
  20. data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +450 -0
  21. data/app/views/rails_error_dashboard/errors/deprecations.html.erb +129 -0
  22. data/app/views/rails_error_dashboard/errors/job_health_summary.html.erb +152 -0
  23. data/app/views/rails_error_dashboard/errors/n_plus_one_summary.html.erb +134 -0
  24. data/app/views/rails_error_dashboard/errors/settings.html.erb +17 -0
  25. data/app/views/rails_error_dashboard/errors/show.html.erb +20 -1132
  26. data/config/routes.rb +5 -0
  27. data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +6 -0
  28. data/db/migrate/20260303000001_add_breadcrumbs_to_error_logs.rb +9 -0
  29. data/db/migrate/20260304000001_add_system_health_to_error_logs.rb +12 -0
  30. data/lib/generators/rails_error_dashboard/install/install_generator.rb +31 -3
  31. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +67 -5
  32. data/lib/rails_error_dashboard/commands/log_error.rb +33 -0
  33. data/lib/rails_error_dashboard/configuration.rb +45 -3
  34. data/lib/rails_error_dashboard/engine.rb +6 -1
  35. data/lib/rails_error_dashboard/middleware/error_catcher.rb +8 -0
  36. data/lib/rails_error_dashboard/queries/cache_health_summary.rb +72 -0
  37. data/lib/rails_error_dashboard/queries/database_health_summary.rb +82 -0
  38. data/lib/rails_error_dashboard/queries/deprecation_warnings.rb +80 -0
  39. data/lib/rails_error_dashboard/queries/job_health_summary.rb +101 -0
  40. data/lib/rails_error_dashboard/queries/n_plus_one_summary.rb +83 -0
  41. data/lib/rails_error_dashboard/services/breadcrumb_collector.rb +182 -0
  42. data/lib/rails_error_dashboard/services/cache_analyzer.rb +76 -0
  43. data/lib/rails_error_dashboard/services/curl_generator.rb +80 -0
  44. data/lib/rails_error_dashboard/services/database_health_inspector.rb +168 -0
  45. data/lib/rails_error_dashboard/services/n_plus_one_detector.rb +74 -0
  46. data/lib/rails_error_dashboard/services/rspec_generator.rb +145 -0
  47. data/lib/rails_error_dashboard/services/system_health_snapshot.rb +145 -0
  48. data/lib/rails_error_dashboard/subscribers/breadcrumb_subscriber.rb +210 -0
  49. data/lib/rails_error_dashboard/version.rb +1 -1
  50. data/lib/rails_error_dashboard.rb +24 -0
  51. data/lib/tasks/error_dashboard.rake +68 -2
  52. metadata +33 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68039a8977f567c9175c9a1493c8162ff9d05cef23abe3f2cba629e815608bb2
4
- data.tar.gz: e448d91e7038288916b705c048f1e9f7e2debc654e4bf136c243190a6a9a2a0b
3
+ metadata.gz: f95521c0c769dd28fdd0934540282c64f32a8d23a8c6be9a63805326384b1c01
4
+ data.tar.gz: 3c66324e1e40e8fa0b474a6566babe67eb831927269f37a0108e77db6c928749
5
5
  SHA512:
6
- metadata.gz: '034283ec93474130ec2786c3c0de00fcba1d15e2b358b167e0856e95b2fc62398cd71f0cef767ab7a4ea538d60cf7fc9dbfb56c0951efa9bf19eded0ba5a2c9e'
7
- data.tar.gz: 40496fc117f9695d0da1cbb0ef9b5d4a1ceea6dc13c2fdd73c8ff262e60424bc142646af7ff442d880858e8c2eb10d3fbb9cb00c7d064117a24733f774cfe062
6
+ metadata.gz: 9ca327e64bea48cc347e96969db0d2da76c4ffe347ba5aad41c8576b6f18b5696fdd64d6df8f910c2b8df4a64dafc2f75f925f9aa9610b20346535d5e05ffbfc
7
+ data.tar.gz: 5188358be68c4831caff5808db350058f334bc9d9e3310acd4bccab8ebef667785035b7481fd2f46ed5bc12e793f4b481f44ae15765babf410ea9292dedfb3d4
data/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
  [![Downloads](https://img.shields.io/gem/dt/rails_error_dashboard)](https://rubygems.org/gems/rails_error_dashboard)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Tests](https://github.com/AnjanJ/rails_error_dashboard/workflows/Tests/badge.svg)](https://github.com/AnjanJ/rails_error_dashboard/actions)
7
+ [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow?logo=buymeacoffee)](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
  ![Analytics](docs/images/analytics.png)
43
44
 
45
+ **Deprecation Warnings** — Aggregate deprecation warnings across all errors with occurrence counts and affected error links.
46
+
47
+ ![Deprecation Warnings](docs/images/deprecations.png)
48
+
49
+ **N+1 Query Patterns** — Cross-error N+1 detection grouped by SQL fingerprint.
50
+
51
+ ![N+1 Query Patterns](docs/images/n-plus-one-queries.png)
52
+
53
+ **Cache Health** — Per-error cache performance sorted worst-first.
54
+
55
+ ![Cache Health](docs/images/cache-health.png)
56
+
57
+ **Job Health** — Background job queue stats across errors, sorted by failed count.
58
+
59
+ ![Job Health](docs/images/job-health.png)
60
+
61
+ **Database Health** — Connection pool utilization, PostgreSQL table stats, and per-error stress scores.
62
+
63
+ ![Database Health](docs/images/database-health.png)
64
+
44
65
  ---
45
66
 
46
67
  ### ⚠️ BETA SOFTWARE
47
- This Rails Engine is in beta and under active development. While functional and tested (1,895+ tests passing, including browser-based system tests), the API may change before v1.0.0. Use in production at your own discretion.
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, environment-based settings, optional separate database for isolation. Your data stays on your server - no third-party access.
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 (NEW!)
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
- 1,800+ tests covering unit, integration, and browser-based system tests.
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 1,800+ 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).
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
- # Authentication is ALWAYS required - no bypass allowed in any environment
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 find_each + destroy to respect dependent: :destroy on associations.
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
- # Use find_each to process in batches (default 1000)
26
- # destroy triggers dependent: :destroy on associations (occurrences, comments, cascades)
27
- ErrorLog.where("occurred_at < ?", cutoff).find_each do |error_log|
28
- error_log.destroy
29
- deleted_count += 1
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>