rails_error_dashboard 0.14.1 → 0.14.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8dc068309c56704b429f7798ce4c3c83b628853179ebc2695c50fa684d2fae9
4
- data.tar.gz: 64e3a06e8b60ce8f4c3fe649ca750f228a16e64405b7e239b8440f53e74e536d
3
+ metadata.gz: f96e195659bca5562fcf7c4ed02c1f8f69c058daec06f6902523958f5746a8a4
4
+ data.tar.gz: 2ea940fe412652bef9e0f47eb818d4da93a6d7b7cb1e32cd5820e96cf032fafe
5
5
  SHA512:
6
- metadata.gz: b8605107393952d4e308ebb23f5c1a58ac88dafc9a9664c2429ddb83777a450cb148c1890b25495166a51238d954320bdfcba4b2ef265fe63e8ffea960fb91df
7
- data.tar.gz: 4ab62e966ce41257abefdb37e6e3f14a6ae732754cac9ef6c4a4ed37af14982f825e90dac17e8681274959d11ec74bf403458a96ee3069af5919def63b7d77e5
6
+ metadata.gz: b7b8a36c5ffde9bcd71a0649a0d2df111345625717ec67a997e4bd4929215700408efd95844b41506e3a48aa47fc7179cd1df140aa1274451a1a8760a5f32db7
7
+ data.tar.gz: d6b3722433ce12451ef44242874dfe98620441832626368d0e8ce9d852396d8e4b00b5cc5773f04a348d48f64254fc763cd4fae4a1580d5b399cb4aab21f3de3
data/README.md CHANGED
@@ -635,9 +635,9 @@ The installer guides you through optional feature selection — notifications, p
635
635
  http://localhost:3000/red
636
636
  ```
637
637
 
638
- Default credentials: `gandalf` / `youshallnotpass`
638
+ Default credentials: `gandalf` / `youshallnotpass`, for development and test only.
639
639
 
640
- **Change these before production!** Edit `config/initializers/rails_error_dashboard.rb`
640
+ **Before you deploy anywhere else**, set `ERROR_DASHBOARD_USER` and `ERROR_DASHBOARD_PASSWORD` in that environment. Outside development and test, the app refuses to boot on the defaults. See [Dashboard Credentials](docs/guides/CONFIGURATION.md#dashboard-credentials).
641
641
 
642
642
  ### 4. Test it out
643
643
 
@@ -654,11 +654,8 @@ raise "Test error from Rails Error Dashboard!"
654
654
 
655
655
  ```ruby
656
656
  RailsErrorDashboard.configure do |config|
657
- # Authentication
658
- config.dashboard_username = ENV.fetch('ERROR_DASHBOARD_USER', 'gandalf')
659
- config.dashboard_password = ENV.fetch('ERROR_DASHBOARD_PASSWORD', 'youshallnotpass')
660
-
661
- # Or use your existing auth (Devise, Warden, etc.):
657
+ # Dashboard login: set ERROR_DASHBOARD_USER and ERROR_DASHBOARD_PASSWORD outside
658
+ # development and test (the gem reads them itself). Or use your existing auth:
662
659
  # config.authenticate_with = -> { warden.authenticated? }
663
660
 
664
661
  # Optional features — enable as needed
@@ -314,6 +314,15 @@ module RailsErrorDashboard
314
314
  expected_password = RailsErrorDashboard.configuration.dashboard_password
315
315
  next false if expected_username.nil? || expected_password.nil?
316
316
 
317
+ # A blank credential must deny everyone too: "" would equal an empty login.
318
+ next false if RailsErrorDashboard::Configuration.blank_credential?(expected_username) ||
319
+ RailsErrorDashboard::Configuration.blank_credential?(expected_password)
320
+
321
+ # The boot check refuses the published credentials outside development
322
+ # and test, but it is skipped while SECRET_KEY_BASE_DUMMY is set. Refuse
323
+ # them here as well, so that variable left set at runtime reopens nothing.
324
+ next false if RailsErrorDashboard.configuration.refuse_default_credentials?
325
+
317
326
  # Non-short-circuit & on purpose: both comparisons always run, so the
318
327
  # response time does not reveal which half was wrong.
319
328
  ActiveSupport::SecurityUtils.secure_compare(username.to_s, expected_username.to_s) &
@@ -33,6 +33,32 @@ module RailsErrorDashboard
33
33
  )
34
34
  end
35
35
 
36
+ # Deletes error logs and everything that references them: dependents first,
37
+ # all in batches. Occurrences, comments and cascade patterns hold foreign
38
+ # keys to the log, so deleting the logs first fails, and one unbounded
39
+ # DELETE would lock the table. Public because error_dashboard:cleanup_resolved
40
+ # deletes the same way.
41
+ #
42
+ # @param scope [ActiveRecord::Relation] the ErrorLog rows to delete
43
+ # @return [Integer] number of error logs deleted
44
+ def self.delete_with_dependents(scope)
45
+ ids = scope.select(:id)
46
+
47
+ ErrorOccurrence.where(error_log_id: ids).in_batches(of: 1000).delete_all
48
+ # Hour buckets for storm-shed events. Listed explicitly because delete_all
49
+ # does not fire the has_many :dependent callback on ErrorLog -- without
50
+ # this line the buckets outlive the group they describe, forever.
51
+ EventCount.where(error_log_id: ids).in_batches(of: 1000).delete_all if EventCount.table_exists?
52
+ ErrorComment.where(error_log_id: ids).in_batches(of: 1000).delete_all
53
+ CascadePattern.where(parent_error_id: ids)
54
+ .or(CascadePattern.where(child_error_id: ids))
55
+ .in_batches(of: 1000).delete_all
56
+
57
+ deleted = 0
58
+ scope.in_batches(of: 1000) { |batch| deleted += batch.delete_all }
59
+ deleted
60
+ end
61
+
36
62
  # @return [Integer] number of errors deleted
37
63
  def perform
38
64
  retention_days = RailsErrorDashboard.configuration.retention_days
@@ -52,29 +78,7 @@ module RailsErrorDashboard
52
78
  expired_scope = self.class.expired_scope(cutoff)
53
79
  return 0 if expired_scope.none?
54
80
 
55
- deleted_count = 0
56
-
57
- # Delete dependents first, then errors — all in batches to prevent table locks
58
- expired_ids_scope = expired_scope.select(:id)
59
-
60
- # Batch delete dependent records (occurrences, comments, cascade patterns)
61
- ErrorOccurrence.where(error_log_id: expired_ids_scope).in_batches(of: 1000).delete_all
62
- # Hour buckets for storm-shed events. Listed explicitly because this job
63
- # deletes with delete_all, which does not fire the has_many :dependent
64
- # callback on ErrorLog -- without this line the buckets outlive the group
65
- # they describe, forever. The migration's own comment promised this
66
- # pruning before the code existed.
67
- EventCount.where(error_log_id: expired_ids_scope).in_batches(of: 1000).delete_all if EventCount.table_exists?
68
- ErrorComment.where(error_log_id: expired_ids_scope).in_batches(of: 1000).delete_all
69
- CascadePattern.where(parent_error_id: expired_ids_scope)
70
- .or(CascadePattern.where(child_error_id: expired_ids_scope))
71
- .in_batches(of: 1000).delete_all
72
-
73
- # Now batch delete the error logs themselves
74
- expired_scope.in_batches(of: 1000) do |batch|
75
- batch_size = batch.delete_all
76
- deleted_count += batch_size
77
- end
81
+ deleted_count = self.class.delete_with_dependents(expired_scope)
78
82
 
79
83
  # delete_all skips callbacks, and the stat cards are cached.
80
84
  Services::AnalyticsCacheManager.clear if deleted_count > 0
@@ -23,7 +23,7 @@ ja:
23
23
  '30': 30日間
24
24
  '90': 90日間
25
25
  frame_category:
26
- app: 自分のコード
26
+ app: アプリケーションコード
27
27
  gem: Gem
28
28
  framework: Rails フレームワーク
29
29
  ruby_core: Ruby コア
@@ -520,7 +520,7 @@ ja:
520
520
  user_agent: 'User Agent:'
521
521
  ip_address: 'IP アドレス:'
522
522
  replay: 'リクエストの再現:'
523
- raw_user_agent: User Agent の生データ
523
+ raw_user_agent: User Agent 文字列
524
524
  duration_seconds: "%{value}秒"
525
525
  duration_ms: "%{value}ミリ秒"
526
526
  copy_curl: curl としてコピー
@@ -530,7 +530,7 @@ ja:
530
530
  snapshot: 'スナップショット:'
531
531
  snapshot_captured_html: '%{time} に取得'
532
532
  snapshot_stale: より前の発生から取得 — それ以降の発生はコンテキストを伴いませんでした
533
- snapshot_fidelity_lite: 縮小された取得 (storm 保護によりコンテキストの payload が破棄されました)
533
+ snapshot_fidelity_lite: 限定的な情報取得(storm 保護によりコンテキストの payload を省略)
534
534
  snapshot_fidelity_minimal: storm 中にカウントされました — コンテキストは取得されていません
535
535
  snapshot_fidelity_partial: 混在したスナップショット — 一部のフィールドは以前の発生時のものです
536
536
  snapshot_unknown: RED がスナップショットの出所を記録する前に取得されました
@@ -667,10 +667,10 @@ ja:
667
667
  backtrace: Backtrace
668
668
  backtrace:
669
669
  label: 'Backtrace:'
670
- frame_counts: "(自分のコード%{app}件、フレームワーク/gem %{framework}件)"
670
+ frame_counts: "(アプリケーションコード%{app}件、フレームワーク/gem %{framework}件)"
671
671
  copy_full: Backtrace 全体をコピー
672
672
  copy_full_title: Backtrace 全体をコピー
673
- your_code: 自分のコード
673
+ your_code: アプリケーションコード
674
674
  framework_code: フレームワーク・gem のコード
675
675
  frames:
676
676
  other: "%{count}フレーム"
@@ -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: true, desc: "Enable async error logging (default: true, uses Rails :async adapter — no extra infrastructure needed)"
18
+ class_option :async_logging, type: :boolean, default: true, desc: "Enable async error logging (default: true; jobs run on your app's Active Job adapter, so production needs a worker)"
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')"
@@ -383,7 +383,7 @@ module RailsErrorDashboard
383
383
  return
384
384
  end
385
385
 
386
- route "mount RailsErrorDashboard::Engine => '/red' # RED (Rails Error Dashboard) — also works at /error_dashboard"
386
+ route "mount RailsErrorDashboard::Engine => '/red' # RED (Rails Error Dashboard)"
387
387
  end
388
388
 
389
389
  def show_feature_summary
@@ -400,7 +400,7 @@ module RailsErrorDashboard
400
400
  say " ✓ Dashboard UI", :green
401
401
  say " ✓ Real-time Updates", :green
402
402
  say " ✓ Analytics", :green
403
- say " ✓ Async Logging (Rails :async adapter — no extra infrastructure needed)", :green
403
+ say " ✓ Async Logging (runs on your app's Active Job adapter)", :green
404
404
 
405
405
  # Count optional features enabled
406
406
  enabled_count = 0
@@ -477,14 +477,11 @@ module RailsErrorDashboard
477
477
  say " → Set PAGERDUTY_INTEGRATION_KEY in .env", :yellow if @enable_pagerduty
478
478
  say " → Set WEBHOOK_URLS in .env", :yellow if @enable_webhooks
479
479
  if @enable_async_logging
480
- # Check which async adapter is in use — only warn about external workers when needed
481
- async_section = File.exist?(File.join(destination_root, "config/initializers/rails_error_dashboard.rb")) &&
482
- File.read(File.join(destination_root, "config/initializers/rails_error_dashboard.rb"))
483
- if async_section && (async_section.include?("async_adapter = :sidekiq") || async_section.include?("async_adapter = :solid_queue"))
484
- say " → Ensure your background worker (Sidekiq/SolidQueue) is running for async logging", :yellow
485
- else
486
- say " ✓ Async logging uses Rails :async adapter — no extra process needed", :green
487
- end
480
+ # RED's jobs run on the app's own Active Job adapter; async_adapter only
481
+ # feeds validation and the Settings page. Rails 8 production uses Solid
482
+ # Queue, which needs a worker, so don't promise that none is needed.
483
+ say " → Async logging enqueues on your app's config.active_job.queue_adapter.", :yellow
484
+ say " Run a worker for the default and error_notifications queues, or set config.async_logging = false.", :yellow
488
485
  end
489
486
 
490
487
  # Database-specific instructions
@@ -503,21 +500,22 @@ module RailsErrorDashboard
503
500
  else
504
501
  say " 3. Run: rails db:migrate:error_dashboard"
505
502
  end
506
- say " 4. Update credentials in config/initializers/rails_error_dashboard.rb"
503
+ say " 4. Before deploying: set ERROR_DASHBOARD_USER and ERROR_DASHBOARD_PASSWORD there"
507
504
  say " 5. Restart your Rails server"
508
505
  say " 6. Visit http://localhost:3000/red"
509
506
  say " 7. Verify: rails error_dashboard:verify"
510
507
  else
511
508
  say " 1. Run: rails db:migrate"
512
- say " 2. Update credentials in config/initializers/rails_error_dashboard.rb"
509
+ say " 2. Before deploying: set ERROR_DASHBOARD_USER and ERROR_DASHBOARD_PASSWORD there"
513
510
  say " 3. Restart your Rails server"
514
511
  say " 4. Visit http://localhost:3000/red"
515
512
  end
516
513
  say "Authentication:", :cyan
517
- say " Default: HTTP Basic Auth (gandalf/youshallnotpass)", :white
514
+ say " Default: HTTP Basic Auth, gandalf/youshallnotpass in development and test only", :white
515
+ say " Elsewhere: set ERROR_DASHBOARD_USER and ERROR_DASHBOARD_PASSWORD, or the app won't boot", :white
518
516
  say " Devise/Warden: config.authenticate_with = -> { warden.authenticated? }", :white
519
517
  say " Session-based: config.authenticate_with = -> { session[:admin] == true }", :white
520
- say " See: https://github.com/AnjanJ/rails_error_dashboard/blob/main/docs/guides/CONFIGURATION.md#custom-authentication", :white
518
+ say " See: https://github.com/AnjanJ/rails_error_dashboard/blob/main/docs/guides/CONFIGURATION.md#dashboard-credentials", :white
521
519
  say "\n"
522
520
  say "Issue Tracking (optional):", :cyan
523
521
  say " Create a dedicated RED (Rails Error Dashboard) bot account on your platform:", :white
@@ -549,7 +547,7 @@ module RailsErrorDashboard
549
547
 
550
548
  def build_quick_defaults
551
549
  {
552
- # Async ON (built-in Rails :async adapter, zero infrastructure)
550
+ # Async ON (runs on the app's Active Job adapter)
553
551
  async_logging: true,
554
552
  # All analytics ON — run at query/background time, zero request-path overhead
555
553
  baseline_alerts: true,
@@ -5,9 +5,12 @@ RailsErrorDashboard.configure do |config|
5
5
  # AUTHENTICATION (Always Required - Cannot Be Disabled)
6
6
  # ============================================================================
7
7
 
8
- # Dashboard authentication credentials
9
- # ⚠️ CHANGE THESE BEFORE PRODUCTION! ⚠️
10
- # Authentication is ALWAYS enforced in ALL environments (production, development, test)
8
+ # Dashboard login (HTTP Basic Auth). Always enforced, in every environment.
9
+ # gandalf / youshallnotpass work in development and test only. Everywhere
10
+ # else, set ERROR_DASHBOARD_USER and ERROR_DASHBOARD_PASSWORD in the
11
+ # environment, or the app refuses to boot. Don't replace the fallback values
12
+ # below with a real password: it would end up in source control.
13
+ # https://github.com/AnjanJ/rails_error_dashboard/blob/main/docs/guides/CONFIGURATION.md#dashboard-credentials
11
14
  config.dashboard_username = ENV.fetch("ERROR_DASHBOARD_USER", "gandalf")
12
15
  config.dashboard_password = ENV.fetch("ERROR_DASHBOARD_PASSWORD", "youshallnotpass")
13
16
 
@@ -160,9 +163,11 @@ RailsErrorDashboard.configure do |config|
160
163
 
161
164
  <% if @enable_async_logging -%>
162
165
  # Async Error Logging - ENABLED
163
- # Errors are logged in background jobs — zero impact on request response time.
164
- # Default adapter is :async (Rails built-in, no extra infrastructure needed).
165
- # Swap to :sidekiq or :solid_queue when you have a background worker running.
166
+ # Errors are logged in background jobs, off the request path. The jobs run on
167
+ # your app's Active Job adapter (config.active_job.queue_adapter), so in
168
+ # production a worker must process the default and error_notifications queues
169
+ # (Rails 8: bin/jobs for Solid Queue). async_adapter below is only validated
170
+ # and shown on the Settings page; it does not choose the backend.
166
171
  config.async_logging = true
167
172
  config.async_adapter = :async # Options: :async (built-in), :sidekiq, :solid_queue
168
173
  # To disable: Set config.async_logging = false
@@ -446,7 +451,8 @@ RailsErrorDashboard.configure do |config|
446
451
  config.enable_internal_logging = false
447
452
 
448
453
  # Log level (default: :silent)
449
- # Options: :debug, :info, :warn, :error, :silent
454
+ # Options: :debug, :info, :warn, :error, :fatal, :silent
455
+ # (RED has no fatal-level messages, so :fatal logs nothing, like :silent)
450
456
  config.log_level = :silent
451
457
 
452
458
  # Example: Enable verbose logging for debugging
@@ -2,34 +2,54 @@
2
2
 
3
3
  module RailsErrorDashboard
4
4
  module Generators
5
- # Generator for Solid Queue configuration
5
+ # Deprecated: checks the app's Solid Queue config for RED. Writes nothing.
6
6
  # Usage: rails generate rails_error_dashboard:solid_queue
7
+ #
8
+ # It used to write a config/queue.yml with workers and no dispatchers.
9
+ # Solid Queue only falls back to its default dispatcher when a section has
10
+ # neither, so that file ran no dispatcher: no delayed job ran, the host
11
+ # app's own included. A Solid Queue config with a "*" worker and a
12
+ # dispatcher already processes RED's queues, so this generator now only
13
+ # checks the file the app has. It never writes Solid Queue's files or
14
+ # points at Solid Queue's installer (decision 0001). To be removed in a
15
+ # later minor release.
7
16
  class SolidQueueGenerator < Rails::Generators::Base
8
- source_root File.expand_path("templates", __dir__)
17
+ desc "Deprecated: checks config/queue.yml for problems that stop RED's jobs. Writes nothing."
9
18
 
10
- desc "Creates Solid Queue configuration for RailsErrorDashboard"
19
+ def check_queue_config
20
+ say "\nrails_error_dashboard:solid_queue is deprecated and no longer writes config/queue.yml.", :yellow
21
+ say "A Solid Queue config with a \"*\" worker and a dispatcher already processes RED's queues.\n\n"
11
22
 
12
- def create_queue_config
13
- template "queue.yml", "config/queue.yml"
23
+ check = RailsErrorDashboard::Services::SolidQueueConfigCheck
24
+ path = check.config_path(destination_root)
25
+ relative = path.relative_path_from(Pathname(destination_root)).to_s
26
+
27
+ unless path.exist?
28
+ say "No #{relative}: Solid Queue runs its defaults (a \"*\" worker and a dispatcher), which process RED's queues.", :green
29
+ return
30
+ end
31
+
32
+ environments = check.app_environments(destination_root)
33
+ environments = [ Rails.env.to_s ] if environments.empty?
34
+ result = check.call(path, environments: environments)
35
+
36
+ if result[:skipped]
37
+ say "Could not check #{relative}: #{result[:skipped]}", :yellow
38
+ elsif result[:problems].any?
39
+ say "#{relative} has problems that stop jobs from running:", :red
40
+ result[:problems].each { |problem| say " - #{problem}", :red }
41
+ say ""
42
+ check::FIX.each { |line| say line, :yellow }
43
+ else
44
+ say "#{relative} processes RED's queues (#{check.red_queue_names.join(', ')}) and runs a dispatcher.", :green
45
+ end
14
46
  end
15
47
 
16
48
  def show_instructions
17
- say "\n" + "=" * 80, :green
18
- say "Solid Queue configuration created!", :green
19
- say "=" * 80, :green
20
- say "\nNext steps:", :yellow
21
- say " 1. Install Solid Queue gem (if not already):", :cyan
22
- say " bundle add solid_queue", :white
23
- say "\n 2. Run Solid Queue migrations:", :cyan
24
- say " bin/rails solid_queue:install", :white
25
- say "\n 3. Set ActiveJob adapter in config/application.rb:", :cyan
26
- say " config.active_job.queue_adapter = :solid_queue", :white
27
- say "\n 4. Start Solid Queue worker:", :cyan
28
- say " bin/jobs", :white
29
- say "\n 5. Enable async logging in config/initializers/rails_error_dashboard.rb:", :cyan
30
- say " config.async_logging = true", :white
31
- say " config.async_adapter = :solid_queue", :white
32
- say "\n" + "=" * 80, :green
49
+ say "\nTo run RED's jobs with Solid Queue:", :cyan
50
+ say " config.active_job.queue_adapter = :solid_queue (config/environments/production.rb)", :white
51
+ say " Run a worker: bin/jobs, or SOLID_QUEUE_IN_PUMA=1 with Puma's solid_queue plugin", :white
52
+ say " Check anytime: bin/rails error_dashboard:verify\n", :white
33
53
  end
34
54
  end
35
55
  end
@@ -5,6 +5,11 @@ module RailsErrorDashboard
5
5
  class UninstallGenerator < Rails::Generators::Base
6
6
  desc "Uninstalls Rails Error Dashboard and removes all associated files and data"
7
7
 
8
+ MIGRATION_GLOBS = [
9
+ "db/migrate/*rails_error_dashboard*.rb",
10
+ "db/error_dashboard_migrate/*rails_error_dashboard*.rb"
11
+ ].freeze
12
+
8
13
  class_option :keep_data, type: :boolean, default: false, desc: "Keep error data in database (don't drop tables)"
9
14
  class_option :skip_confirmation, type: :boolean, default: false, desc: "Skip confirmation prompts"
10
15
  class_option :manual_only, type: :boolean, default: false, desc: "Show manual instructions only, don't perform automated uninstall"
@@ -35,6 +40,15 @@ module RailsErrorDashboard
35
40
  say " #{status_icon(@components[:migrations])} Migrations (#{migration_count} files)"
36
41
  say " #{status_icon(@components[:tables])} Database tables (#{table_count} tables)"
37
42
  say "\n"
43
+
44
+ # Couldn't see the error database (no database.yml entry for it, or the
45
+ # server is down). Carrying on would remove the initializer and leave
46
+ # every table behind, with nothing left that knows where they are.
47
+ if @database_error && !options[:keep_data] && !options[:manual_only]
48
+ raise Thor::Error, "Can't reach the error database, so nothing was removed: #{@database_error}\n" \
49
+ "Fix config/database.yml (or start the database) and run this again, " \
50
+ "or pass --keep-data to remove only the files."
51
+ end
38
52
  end
39
53
 
40
54
  def show_manual_instructions
@@ -42,13 +56,16 @@ module RailsErrorDashboard
42
56
  say " 📖 Manual Uninstall Instructions", :cyan
43
57
  say "=" * 80
44
58
  say "\n"
45
-
46
- say "Step 1: Remove from Gemfile", :yellow
47
- say " Open: Gemfile"
48
- say " Remove: gem 'rails_error_dashboard'"
49
- say " Run: bundle install"
59
+ say "Do these in order: the table drop needs the gem, so the Gemfile comes last.", :yellow
50
60
  say "\n"
51
61
 
62
+ if @components[:tables]
63
+ say "Step 1: Drop database tables (⚠️ DESTRUCTIVE - will delete all error data)", :yellow
64
+ say " Run: bin/rails rails_error_dashboard:db:drop"
65
+ say " It drops every RED table in foreign-key order, on the error database if you use one."
66
+ say "\n"
67
+ end
68
+
52
69
  if @components[:initializer]
53
70
  say "Step 2: Remove initializer", :yellow
54
71
  say " Delete: config/initializers/rails_error_dashboard.rb"
@@ -58,33 +75,31 @@ module RailsErrorDashboard
58
75
  if @components[:route]
59
76
  say "Step 3: Remove route", :yellow
60
77
  say " Open: config/routes.rb"
61
- say " Remove: mount RailsErrorDashboard::Engine => '/error_dashboard'"
78
+ say " Remove the line: mount RailsErrorDashboard::Engine => ..."
62
79
  say "\n"
63
80
  end
64
81
 
65
82
  if @components[:migrations]
66
83
  say "Step 4: Remove migrations", :yellow
67
- say " Delete migration files from db/migrate/:"
68
84
  migration_files.each do |file|
69
- say " - #{File.basename(file)}", :white
85
+ say " - #{file}", :white
70
86
  end
71
87
  say "\n"
72
88
  end
73
89
 
74
- if @components[:tables]
75
- say "Step 5: Drop database tables (⚠️ DESTRUCTIVE - will delete all error data)", :yellow
76
- say " Run: rails rails_error_dashboard:db:drop"
77
- say " Or manually in rails console:"
78
- say " ActiveRecord::Base.connection.execute('DROP TABLE rails_error_dashboard_error_logs')", :white
79
- say " ActiveRecord::Base.connection.execute('DROP TABLE rails_error_dashboard_error_occurrences')", :white
80
- say " ActiveRecord::Base.connection.execute('DROP TABLE rails_error_dashboard_cascade_patterns')", :white
81
- say " ActiveRecord::Base.connection.execute('DROP TABLE rails_error_dashboard_error_baselines')", :white
82
- say " ActiveRecord::Base.connection.execute('DROP TABLE rails_error_dashboard_error_comments')", :white
83
- say " ActiveRecord::Migration.drop_table(:rails_error_dashboard_error_logs) rescue nil", :white
84
- say "\n"
85
- end
90
+ say "Step 5: Separate database only: remove its entry from config/database.yml", :yellow
91
+ say "\n"
92
+
93
+ say "Step 6: Regenerate the schema file", :yellow
94
+ say " Run: bin/rails db:schema:dump (otherwise db:schema:load recreates the tables)"
95
+ say "\n"
86
96
 
87
- say "Step 6: Clean up environment variables (optional)", :yellow
97
+ say "Step 7: Remove from Gemfile", :yellow
98
+ say " Remove: gem 'rails_error_dashboard'"
99
+ say " Run: bundle install"
100
+ say "\n"
101
+
102
+ say "Step 8: Clean up environment variables (optional)", :yellow
88
103
  say " Remove from .env or environment:"
89
104
  say " - ERROR_DASHBOARD_USER"
90
105
  say " - ERROR_DASHBOARD_PASSWORD"
@@ -96,8 +111,7 @@ module RailsErrorDashboard
96
111
  say " - DASHBOARD_BASE_URL"
97
112
  say "\n"
98
113
 
99
- say "Step 7: Restart your application", :yellow
100
- say " Run: rails restart (or restart your server)"
114
+ say "Step 9: Restart your application", :yellow
101
115
  say "\n"
102
116
 
103
117
  say "=" * 80
@@ -165,6 +179,20 @@ module RailsErrorDashboard
165
179
  say "\n"
166
180
  end
167
181
 
182
+ # Before any file is removed: if the drop fails, the app still boots with
183
+ # its initializer and can reach the error database to try again.
184
+ def drop_database_tables
185
+ return if options[:manual_only]
186
+ return if options[:keep_data]
187
+ return unless @components[:tables]
188
+
189
+ say " Dropping database tables...", :yellow
190
+ dropped = RailsErrorDashboard::Commands::DropAllTables.call(connection: drop_connection)
191
+ say " ✓ Dropped #{dropped.size} database table(s)", :green
192
+ rescue => e
193
+ raise Thor::Error, "Could not drop RED's tables, so no files were removed: #{e.message}"
194
+ end
195
+
168
196
  def remove_initializer
169
197
  return if options[:manual_only]
170
198
  return unless @components[:initializer]
@@ -182,7 +210,7 @@ module RailsErrorDashboard
182
210
  say " ✓ Removed route", :green
183
211
  rescue => e
184
212
  say " ⚠️ Could not automatically remove route: #{e.message}", :yellow
185
- say " Please manually remove: mount RailsErrorDashboard::Engine => '/error_dashboard'", :yellow
213
+ say " Please remove the mount RailsErrorDashboard::Engine line from config/routes.rb", :yellow
186
214
  end
187
215
  end
188
216
 
@@ -196,35 +224,6 @@ module RailsErrorDashboard
196
224
  say " ✓ Removed #{migration_count} migration file(s)", :green
197
225
  end
198
226
 
199
- def drop_database_tables
200
- return if options[:manual_only]
201
- return if options[:keep_data]
202
- return unless @components[:tables]
203
-
204
- say " Dropping database tables...", :yellow
205
-
206
- # Drop tables in reverse order (to respect foreign keys)
207
- tables_to_drop = [
208
- "rails_error_dashboard_error_comments",
209
- "rails_error_dashboard_error_occurrences",
210
- "rails_error_dashboard_cascade_patterns",
211
- "rails_error_dashboard_error_baselines",
212
- "rails_error_dashboard_error_logs"
213
- ]
214
-
215
- dropped_count = 0
216
- tables_to_drop.each do |table|
217
- if ActiveRecord::Base.connection.table_exists?(table)
218
- ActiveRecord::Base.connection.drop_table(table, if_exists: true)
219
- dropped_count += 1
220
- end
221
- rescue => e
222
- say " ⚠️ Could not drop table #{table}: #{e.message}", :yellow
223
- end
224
-
225
- say " ✓ Dropped #{dropped_count} database table(s)", :green
226
- end
227
-
228
227
  def show_completion_message
229
228
  return if options[:manual_only]
230
229
 
@@ -282,7 +281,7 @@ module RailsErrorDashboard
282
281
  end
283
282
 
284
283
  def migration_files
285
- Dir.glob("db/migrate/*rails_error_dashboard*.rb")
284
+ Dir.glob(MIGRATION_GLOBS)
286
285
  end
287
286
 
288
287
  def migration_count
@@ -290,22 +289,27 @@ module RailsErrorDashboard
290
289
  end
291
290
 
292
291
  def tables_exist?
293
- return false unless defined?(ActiveRecord::Base)
294
- table_names.any? { |table| ActiveRecord::Base.connection.table_exists?(table) rescue false }
292
+ table_names.any?
295
293
  end
296
294
 
295
+ # The tables Queries::UninstallPlan finds on the error database, in drop
296
+ # order. Empty (with the reason shown) when that database can't be reached.
297
297
  def table_names
298
- [
299
- "rails_error_dashboard_error_logs",
300
- "rails_error_dashboard_error_occurrences",
301
- "rails_error_dashboard_cascade_patterns",
302
- "rails_error_dashboard_error_baselines",
303
- "rails_error_dashboard_error_comments"
304
- ]
298
+ @table_names ||= begin
299
+ RailsErrorDashboard::Queries::UninstallPlan.call(drop_connection).map { |entry| entry[:table] }
300
+ rescue => e
301
+ @database_error = e.message
302
+ say " ⚠️ Could not inspect the error database: #{e.message}", :yellow
303
+ []
304
+ end
305
+ end
306
+
307
+ def drop_connection
308
+ RailsErrorDashboard::Commands::DropAllTables.connection
305
309
  end
306
310
 
307
311
  def table_count
308
- table_names.count { |table| ActiveRecord::Base.connection.table_exists?(table) rescue false }
312
+ table_names.count
309
313
  end
310
314
 
311
315
  def gemfile_includes_gem?