rails_error_dashboard 0.14.2 → 0.14.4

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -18
  3. data/app/helpers/rails_error_dashboard/application_helper.rb +8 -0
  4. data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +27 -23
  5. data/app/views/layouts/rails_error_dashboard.html.erb +13 -0
  6. data/config/locales/de.yml +4 -0
  7. data/config/locales/en.yml +5 -0
  8. data/config/locales/es.yml +4 -0
  9. data/config/locales/fr.yml +4 -0
  10. data/config/locales/it.yml +4 -0
  11. data/config/locales/ja.yml +4 -0
  12. data/config/locales/pl.yml +4 -0
  13. data/config/locales/pt-BR.yml +4 -0
  14. data/config/locales/ru.yml +4 -0
  15. data/config/locales/uk.yml +4 -0
  16. data/config/locales/zh-CN.yml +4 -0
  17. data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +9 -0
  18. data/lib/generators/rails_error_dashboard/install/install_generator.rb +45 -50
  19. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +17 -10
  20. data/lib/generators/rails_error_dashboard/solid_queue/solid_queue_generator.rb +41 -21
  21. data/lib/generators/rails_error_dashboard/uninstall/uninstall_generator.rb +79 -66
  22. data/lib/rails_error_dashboard/commands/drop_all_tables.rb +77 -0
  23. data/lib/rails_error_dashboard/commands/log_error.rb +5 -2
  24. data/lib/rails_error_dashboard/configuration.rb +67 -4
  25. data/lib/rails_error_dashboard/engine.rb +10 -4
  26. data/lib/rails_error_dashboard/logger.rb +11 -3
  27. data/lib/rails_error_dashboard/middleware/rate_limiter.rb +15 -43
  28. data/lib/rails_error_dashboard/plugin.rb +30 -0
  29. data/lib/rails_error_dashboard/queries/errors_list.rb +3 -1
  30. data/lib/rails_error_dashboard/queries/uninstall_plan.rb +63 -0
  31. data/lib/rails_error_dashboard/services/exception_filter.rb +4 -1
  32. data/lib/rails_error_dashboard/services/solid_queue_config_check.rb +261 -0
  33. data/lib/rails_error_dashboard/version.rb +1 -1
  34. data/lib/rails_error_dashboard.rb +3 -0
  35. data/lib/tasks/error_dashboard.rake +77 -5
  36. data/lib/tasks/rails_error_dashboard_tasks.rake +37 -43
  37. metadata +5 -3
  38. data/lib/generators/rails_error_dashboard/solid_queue/templates/queue.yml +0 -55
@@ -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
@@ -181,7 +186,8 @@ RailsErrorDashboard.configure do |config|
181
186
  <% if @enable_error_sampling -%>
182
187
  # Error Sampling - ENABLED
183
188
  # Samples non-critical errors to reduce storage volume.
184
- # Critical and high severity errors are ALWAYS logged at 100% regardless of this setting.
189
+ # Critical errors are always logged, regardless of this setting. So is the first
190
+ # occurrence of each error in each process.
185
191
  # 0.5 = log 50% of non-critical occurrences — halves storage while keeping
186
192
  # occurrence counts meaningful and error patterns visible.
187
193
  # Tune lower (e.g. 0.1) if one error is flooding the DB; set to 1.0 to log everything.
@@ -239,7 +245,7 @@ RailsErrorDashboard.configure do |config|
239
245
  # Automatically detect when error rates exceed normal patterns
240
246
  config.enable_baseline_alerts = true
241
247
  config.baseline_alert_threshold_std_devs = 2.0 # Alert when > 2 std devs above baseline
242
- config.baseline_alert_severities = [ :critical, :high ]
248
+ config.baseline_alert_severities = [ :critical, :high ] # Only errors of these severities alert
243
249
  config.baseline_alert_cooldown_minutes = 120 # 2 hours between alerts
244
250
  # To disable: Set config.enable_baseline_alerts = false
245
251
 
@@ -248,7 +254,7 @@ RailsErrorDashboard.configure do |config|
248
254
  # To enable: Set config.enable_baseline_alerts = true
249
255
  config.enable_baseline_alerts = false
250
256
  # config.baseline_alert_threshold_std_devs = 2.0
251
- # config.baseline_alert_severities = [ :critical, :high ]
257
+ # config.baseline_alert_severities = [ :critical, :high ] # Only errors of these severities alert
252
258
  # config.baseline_alert_cooldown_minutes = 120
253
259
 
254
260
  <% end -%>
@@ -446,7 +452,8 @@ RailsErrorDashboard.configure do |config|
446
452
  config.enable_internal_logging = false
447
453
 
448
454
  # Log level (default: :silent)
449
- # Options: :debug, :info, :warn, :error, :silent
455
+ # Options: :debug, :info, :warn, :error, :fatal, :silent
456
+ # (RED has no fatal-level messages, so :fatal logs nothing, like :silent)
450
457
  config.log_level = :silent
451
458
 
452
459
  # 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"
96
+
97
+ say "Step 7: Remove from Gemfile", :yellow
98
+ say " Remove: gem 'rails_error_dashboard'"
99
+ say " Run: bundle install"
100
+ say "\n"
86
101
 
87
- say "Step 6: Clean up environment variables (optional)", :yellow
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
 
@@ -237,13 +236,22 @@ module RailsErrorDashboard
237
236
  say "Remaining manual steps:", :cyan
238
237
  say "\n"
239
238
 
240
- say "1. Remove from Gemfile:", :yellow
239
+ unless options[:keep_data]
240
+ say "1. Regenerate the schema file:", :yellow
241
+ say " Run: bin/rails db:schema:dump (otherwise db:schema:load recreates the tables)"
242
+ say "\n"
243
+
244
+ say "2. Separate database only: remove its entry from config/database.yml", :yellow
245
+ say "\n"
246
+ end
247
+
248
+ say "#{options[:keep_data] ? 1 : 3}. Remove from Gemfile:", :yellow
241
249
  say " Open: Gemfile"
242
250
  say " Remove: gem 'rails_error_dashboard'"
243
251
  say " Run: bundle install"
244
252
  say "\n"
245
253
 
246
- say "2. Restart your application:", :yellow
254
+ say "#{options[:keep_data] ? 2 : 4}. Restart your application:", :yellow
247
255
  say " Run: rails restart"
248
256
  say " Or: kill and restart your server process"
249
257
  say "\n"
@@ -282,7 +290,7 @@ module RailsErrorDashboard
282
290
  end
283
291
 
284
292
  def migration_files
285
- Dir.glob("db/migrate/*rails_error_dashboard*.rb")
293
+ Dir.glob(MIGRATION_GLOBS)
286
294
  end
287
295
 
288
296
  def migration_count
@@ -290,22 +298,27 @@ module RailsErrorDashboard
290
298
  end
291
299
 
292
300
  def tables_exist?
293
- return false unless defined?(ActiveRecord::Base)
294
- table_names.any? { |table| ActiveRecord::Base.connection.table_exists?(table) rescue false }
301
+ table_names.any?
295
302
  end
296
303
 
304
+ # The tables Queries::UninstallPlan finds on the error database, in drop
305
+ # order. Empty (with the reason shown) when that database can't be reached.
297
306
  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
- ]
307
+ @table_names ||= begin
308
+ RailsErrorDashboard::Queries::UninstallPlan.call(drop_connection).map { |entry| entry[:table] }
309
+ rescue => e
310
+ @database_error = e.message
311
+ say " ⚠️ Could not inspect the error database: #{e.message}", :yellow
312
+ []
313
+ end
314
+ end
315
+
316
+ def drop_connection
317
+ RailsErrorDashboard::Commands::DropAllTables.connection
305
318
  end
306
319
 
307
320
  def table_count
308
- table_names.count { |table| ActiveRecord::Base.connection.table_exists?(table) rescue false }
321
+ table_names.count
309
322
  end
310
323
 
311
324
  def gemfile_includes_gem?
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Commands
5
+ # Drops every RED table, for uninstalling. DESTRUCTIVE: all error data goes.
6
+ #
7
+ # Used by the rails_error_dashboard:db:drop task and the uninstall
8
+ # generator. Drops on the connection RED's models use, so a separate or
9
+ # shared error database is covered, in the order Queries::UninstallPlan
10
+ # derives from the foreign keys. Where the adapter runs DDL in a transaction
11
+ # (PostgreSQL, SQLite) it is all-or-nothing. Errors are raised, never
12
+ # swallowed: a half-finished uninstall must not report success.
13
+ #
14
+ # @example
15
+ # RailsErrorDashboard::Commands::DropAllTables.call
16
+ # # => ["rails_error_dashboard_error_comments", ...]
17
+ class DropAllTables
18
+ class Error < StandardError; end
19
+
20
+ # The connection RED's models use.
21
+ #
22
+ # @raise [Error] when use_separate_database is on but the engine fell back
23
+ # to the primary database because database.yml has no entry for it:
24
+ # dropping there would leave the real error database untouched.
25
+ # @return [ActiveRecord::ConnectionAdapters::AbstractAdapter]
26
+ def self.connection
27
+ config = RailsErrorDashboard.configuration
28
+ record = RailsErrorDashboard::ErrorLogsRecord
29
+
30
+ if config.use_separate_database
31
+ expected = config.database.to_s
32
+ actual = record.connection_db_config&.name.to_s
33
+ if actual != expected
34
+ raise Error, "use_separate_database is on, but RED is connected to the '#{actual}' database, " \
35
+ "not '#{expected}': database.yml has no '#{expected}' entry for #{Rails.env}. " \
36
+ "Add it (see docs/guides/DATABASE_OPTIONS.md) and run this again, or RED's tables " \
37
+ "in the error database would be left behind."
38
+ end
39
+ end
40
+
41
+ record.connection
42
+ end
43
+
44
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
45
+ # @return [Array<String>] the dropped tables, in drop order
46
+ # @raise [Error] when a host table has a foreign key into a RED table
47
+ def self.call(connection: self.connection)
48
+ tables = Queries::UninstallPlan.call(connection).map { |entry| entry[:table] }
49
+ return [] if tables.empty?
50
+
51
+ blocking = host_foreign_keys(connection, tables)
52
+ if blocking.any?
53
+ raise Error, "Not dropping anything: these tables have foreign keys into RED's tables. " \
54
+ "Remove the foreign keys first: #{blocking.join('; ')}"
55
+ end
56
+
57
+ drop = -> { tables.each { |table| connection.drop_table(table, if_exists: true) } }
58
+ if connection.supports_ddl_transactions?
59
+ connection.transaction(requires_new: true) { drop.call }
60
+ else
61
+ drop.call
62
+ end
63
+
64
+ tables
65
+ end
66
+
67
+ def self.host_foreign_keys(connection, tables)
68
+ (connection.tables - tables).flat_map do |table|
69
+ connection.foreign_keys(table)
70
+ .select { |fk| tables.include?(fk.to_table) }
71
+ .map { |fk| "#{table}.#{fk.column} -> #{fk.to_table} (#{fk.name})" }
72
+ end
73
+ end
74
+ private_class_method :host_foreign_keys
75
+ end
76
+ end
77
+ end
@@ -878,8 +878,11 @@ module RailsErrorDashboard
878
878
  # Return if no anomaly detected
879
879
  return unless anomaly[:anomaly]
880
880
 
881
- # Check if severity level should trigger alert
882
- return unless config.baseline_alert_severities.include?(anomaly[:level])
881
+ # baseline_alert_severities lists ERROR severities (critical/high/
882
+ # medium/low, as validated), not anomaly levels. Comparing it with
883
+ # anomaly[:level] (elevated/high/critical) skipped every anomaly below
884
+ # threshold + 1 sigma, and :medium/:low never matched at all.
885
+ return unless config.baseline_alert_severities.include?(error_log.severity)
883
886
 
884
887
  # Enqueue alert job (which will handle throttling)
885
888
  BaselineAlertJob.perform_later(error_log.id, anomaly, ApplicationJob.enqueue_locale)
@@ -8,6 +8,14 @@ module RailsErrorDashboard
8
8
  DEFAULT_DASHBOARD_USERNAME = "gandalf"
9
9
  DEFAULT_DASHBOARD_PASSWORD = "youshallnotpass"
10
10
 
11
+ # Settings that hold credentials. #inspect masks them, so printing the
12
+ # configuration in a console (IRB shows results with pp, which uses
13
+ # #inspect) or pasting it into an issue doesn't leak them.
14
+ SECRET_ATTRIBUTES = %i[
15
+ dashboard_password slack_webhook_url discord_webhook_url pagerduty_integration_key
16
+ webhook_urls issue_tracker_token issue_webhook_secret llm_api_key
17
+ ].freeze
18
+
11
19
  # Dashboard authentication (always required)
12
20
  attr_accessor :dashboard_username
13
21
  attr_accessor :dashboard_password
@@ -351,7 +359,7 @@ module RailsErrorDashboard
351
359
 
352
360
  # Rate limiting defaults
353
361
  @enable_rate_limiting = false # OFF by default (opt-in)
354
- @rate_limit_per_minute = 100 # Requests per minute per IP for API endpoints
362
+ @rate_limit_per_minute = 300 # Requests per minute per IP, per dashboard path
355
363
 
356
364
  # Enhanced metrics defaults
357
365
  @app_version = ENV["APP_VERSION"]
@@ -380,7 +388,7 @@ module RailsErrorDashboard
380
388
  # Baseline alert defaults
381
389
  @enable_baseline_alerts = false # OFF by default (opt-in)
382
390
  @baseline_alert_threshold_std_devs = ENV.fetch("BASELINE_ALERT_THRESHOLD", "2.0").to_f
383
- @baseline_alert_severities = [ :critical, :high ] # Alert on critical and high severity anomalies
391
+ @baseline_alert_severities = [ :critical, :high ] # Alert only for errors of these severities
384
392
  @baseline_alert_cooldown_minutes = ENV.fetch("BASELINE_ALERT_COOLDOWN", "120").to_i
385
393
 
386
394
  # Source code integration defaults - OFF by default (opt-in)
@@ -495,7 +503,7 @@ module RailsErrorDashboard
495
503
 
496
504
  # Internal logging defaults - SILENT by default
497
505
  @enable_internal_logging = false # Opt-in for debugging
498
- @log_level = :silent # Silent by default, use :debug, :info, :warn, :error, or :silent
506
+ @log_level = :silent # Silent by default; see Logger::LOG_LEVELS for the levels
499
507
 
500
508
  # Dashboard UI
501
509
  @accent_color = :crimson # :crimson, :ruby, :ember, :violet
@@ -522,6 +530,30 @@ module RailsErrorDashboard
522
530
  initialize
523
531
  end
524
532
 
533
+ # Whether config/database.yml has the entry config.database names for this
534
+ # environment. Without it the engine skips connects_to and RED uses the
535
+ # main database, where its tables usually don't exist, so nothing is
536
+ # recorded. The engine and error_dashboard:verify both ask this.
537
+ #
538
+ # @param env_name [String] the Rails environment
539
+ # @return [Boolean]
540
+ def separate_database_entry?(env_name = Rails.env)
541
+ name = (database || :error_dashboard).to_s
542
+ ActiveRecord::Base.configurations.configs_for(env_name: env_name.to_s).any? { |config| config.name == name }
543
+ end
544
+
545
+ # Like Object#inspect, with every credential in SECRET_ATTRIBUTES replaced
546
+ # by [FILTERED] when set. Unset ones still show nil or [], which is useful
547
+ # when checking why a channel stays off.
548
+ def inspect
549
+ attributes = instance_variables.map do |ivar|
550
+ value = instance_variable_get(ivar)
551
+ secret = SECRET_ATTRIBUTES.include?(ivar.to_s.delete_prefix("@").to_sym) && value.present?
552
+ "#{ivar}=#{secret ? '[FILTERED]' : value.inspect}"
553
+ end
554
+ "#<#{self.class.name} #{attributes.join(', ')}>"
555
+ end
556
+
525
557
  # Validate configuration values
526
558
  # Raises ConfigurationError if any validation fails
527
559
  # Logs warnings for non-fatal issues (e.g., Ruby version incompatibilities)
@@ -554,6 +586,14 @@ module RailsErrorDashboard
554
586
  errors << "Default or blank credentials cannot be used in #{Rails.env}: #{reason}. Only development and test may run on the built-in credentials. Set ERROR_DASHBOARD_USER and ERROR_DASHBOARD_PASSWORD environment variables and make sure the initializer does not overwrite them, or use authenticate_with for custom auth."
555
587
  end
556
588
 
589
+ # In development the app boots on a blank credential, and the login then
590
+ # denies everyone: say so, or the developer is locked out with no clue
591
+ # why. Development only, because a test run boots on every CI job.
592
+ if default_credentials? && credentials_problem == :blank &&
593
+ defined?(Rails) && Rails.respond_to?(:env) && Rails.env.development?
594
+ warnings.concat(blank_credential_warnings)
595
+ end
596
+
557
597
  # Validate sampling_rate (must be between 0.0 and 1.0)
558
598
  if sampling_rate && (sampling_rate < 0.0 || sampling_rate > 1.0)
559
599
  errors << "sampling_rate must be between 0.0 and 1.0 (got: #{sampling_rate})"
@@ -786,7 +826,9 @@ module RailsErrorDashboard
786
826
 
787
827
  # Validate log level (must be valid symbol)
788
828
  if log_level
789
- valid_log_levels = %i[debug info warn error fatal silent]
829
+ # Referenced here, not as a class-body constant: this file loads before
830
+ # logger.rb, and a bare Logger in the class body would be ::Logger.
831
+ valid_log_levels = RailsErrorDashboard::Logger::LOG_LEVELS.keys
790
832
  unless valid_log_levels.include?(log_level)
791
833
  errors << "log_level must be one of #{valid_log_levels.inspect} (got: #{log_level.inspect})"
792
834
  end
@@ -996,6 +1038,27 @@ module RailsErrorDashboard
996
1038
  !Rails.env.development? && !Rails.env.test?
997
1039
  end
998
1040
 
1041
+ # One warning per blank credential, naming where the blank came from: an
1042
+ # environment variable that is set but empty, or the initializer.
1043
+ #
1044
+ # @return [Array<String>]
1045
+ private def blank_credential_warnings
1046
+ {
1047
+ "ERROR_DASHBOARD_USER" => [ :dashboard_username, dashboard_username ],
1048
+ "ERROR_DASHBOARD_PASSWORD" => [ :dashboard_password, dashboard_password ]
1049
+ }.filter_map do |variable, (setting, value)|
1050
+ next unless self.class.blank_credential?(value)
1051
+
1052
+ if ENV.key?(variable) && ENV[variable].to_s == value.to_s
1053
+ "#{variable} is set but empty, so every dashboard login will be denied. " \
1054
+ "Unset it to use the development default, or give it a value."
1055
+ else
1056
+ "config.#{setting} is blank, so every dashboard login will be denied. " \
1057
+ "Remove that line from the initializer to use the development default, or give it a value."
1058
+ end
1059
+ end
1060
+ end
1061
+
999
1062
  # Resolve the effective issue tracker provider (auto-detect from git_repository_url).
1000
1063
  # Linear is never auto-detected (it is not a git forge) — set issue_tracker_provider
1001
1064
  # explicitly.
@@ -11,10 +11,11 @@ module RailsErrorDashboard
11
11
 
12
12
  # Guard: skip connects_to if the database config doesn't exist yet in database.yml.
13
13
  # This happens during `rails generate` when the initializer was just created but
14
- # the user hasn't added the database.yml entry yet.
15
- db_configs = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env)
16
- unless db_configs.any? { |c| c.name == database_name.to_s }
17
- Rails.logger.warn "[Rails Error Dashboard] Separate database '#{database_name}' is not configured in database.yml for the '#{Rails.env}' environment. Skipping connects_to. See https://github.com/AnjanJ/rails_error_dashboard/blob/main/docs/guides/DATABASE_OPTIONS.md"
14
+ # the user hasn't added the database.yml entry yet. Logged at error level:
15
+ # RED then uses the main database, and unless that has RED's tables no
16
+ # error is recorded. error_dashboard:verify reports it as FAILED.
17
+ unless RailsErrorDashboard.configuration.separate_database_entry?
18
+ Rails.logger.error "[Rails Error Dashboard] Separate database '#{database_name}' is not configured in database.yml for the '#{Rails.env}' environment. Skipping connects_to: errors are not recorded until it is added. See https://github.com/AnjanJ/rails_error_dashboard/blob/main/docs/guides/DATABASE_OPTIONS.md"
18
19
  next
19
20
  end
20
21
 
@@ -76,6 +77,11 @@ module RailsErrorDashboard
76
77
  # configured: then it is never consulted.
77
78
  RailsErrorDashboard.detected_git_sha if RailsErrorDashboard.configuration.git_sha.blank?
78
79
 
80
+ # One error-level line if Solid Queue runs RED's jobs with a
81
+ # config/queue.yml that never would (no dispatcher, no worker for RED's
82
+ # queues). Reads the file once; never raises. The dashboard shows a banner.
83
+ RailsErrorDashboard::Services::SolidQueueConfigCheck.log_current_problems
84
+
79
85
  if RailsErrorDashboard.configuration.enable_error_subscriber
80
86
  Rails.error.subscribe(RailsErrorDashboard::ErrorReporter.new)
81
87
  end