rails_error_dashboard 0.1.38 → 0.2.0

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -4
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +1 -0
  4. data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +10 -0
  5. data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +19 -15
  6. data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +19 -9
  7. data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +37 -11
  8. data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +44 -0
  9. data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +38 -16
  10. data/app/models/rails_error_dashboard/error_log.rb +10 -0
  11. data/app/models/rails_error_dashboard/error_logs_record.rb +11 -6
  12. data/app/views/layouts/rails_error_dashboard.html.erb +16 -0
  13. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +3 -0
  14. data/app/views/rails_error_dashboard/errors/_stats.html.erb +12 -4
  15. data/app/views/rails_error_dashboard/errors/index.html.erb +7 -5
  16. data/app/views/rails_error_dashboard/errors/show.html.erb +138 -7
  17. data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +36 -0
  18. data/db/migrate/20251224081522_add_better_tracking_to_error_logs.rb +1 -1
  19. data/db/migrate/20251224101217_add_controller_action_to_error_logs.rb +1 -1
  20. data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +1 -1
  21. data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +1 -1
  22. data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +1 -1
  23. data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +1 -1
  24. data/db/migrate/20251225100236_create_error_occurrences.rb +1 -1
  25. data/db/migrate/20251225101920_create_cascade_patterns.rb +1 -1
  26. data/db/migrate/20251225102500_create_error_baselines.rb +1 -1
  27. data/db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb +1 -1
  28. data/db/migrate/20251226020100_create_error_comments.rb +1 -1
  29. data/db/migrate/20251230075315_cleanup_orphaned_migrations.rb +1 -1
  30. data/db/migrate/20260220000001_add_exception_cause_to_error_logs.rb +9 -0
  31. data/db/migrate/20260220000002_add_enriched_context_to_error_logs.rb +12 -0
  32. data/db/migrate/20260220000003_add_time_series_indexes_to_error_logs.rb +67 -0
  33. data/db/migrate/20260221000001_add_environment_info_to_error_logs.rb +9 -0
  34. data/db/migrate/20260221000002_add_reopened_at_to_error_logs.rb +9 -0
  35. data/lib/generators/rails_error_dashboard/install/install_generator.rb +145 -24
  36. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +12 -8
  37. data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +58 -10
  38. data/lib/rails_error_dashboard/commands/log_error.rb +109 -10
  39. data/lib/rails_error_dashboard/configuration.rb +52 -0
  40. data/lib/rails_error_dashboard/manual_error_reporter.rb +12 -0
  41. data/lib/rails_error_dashboard/middleware/error_catcher.rb +3 -0
  42. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +8 -0
  43. data/lib/rails_error_dashboard/queries/errors_list.rb +8 -0
  44. data/lib/rails_error_dashboard/services/backtrace_parser.rb +31 -0
  45. data/lib/rails_error_dashboard/services/backtrace_processor.rb +31 -1
  46. data/lib/rails_error_dashboard/services/cause_chain_extractor.rb +62 -0
  47. data/lib/rails_error_dashboard/services/environment_snapshot.rb +85 -0
  48. data/lib/rails_error_dashboard/services/error_hash_generator.rb +50 -2
  49. data/lib/rails_error_dashboard/services/notification_throttler.rb +109 -0
  50. data/lib/rails_error_dashboard/services/platform_detector.rb +36 -11
  51. data/lib/rails_error_dashboard/services/sensitive_data_filter.rb +176 -0
  52. data/lib/rails_error_dashboard/value_objects/error_context.rb +81 -4
  53. data/lib/rails_error_dashboard/version.rb +1 -1
  54. data/lib/rails_error_dashboard.rb +11 -5
  55. data/lib/tasks/error_dashboard.rake +158 -2
  56. metadata +12 -58
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e6cc2792ac8e61248b57b513ac64f02426ee651bb9cf07c3d4557760f8d7d31
4
- data.tar.gz: 9450f4745f01d22c8eef4e5988c745d89f7951d318a7f625a8839b1eebd371f5
3
+ metadata.gz: 7ded53b39ba42ddedec2dea52df11dc8e69c77376bb8204891e6c4f9d3fc165b
4
+ data.tar.gz: c0d80a90c1a2f9dff08f82d4bd40dcc2e1f0336ffe443cc5c96420d059b42a64
5
5
  SHA512:
6
- metadata.gz: 49a0c0f2988e1dfe03a40599693411d4a55690be71b7f7921d3dc6e305bc9f2bb54e2792bea7ad8a956127a7d87abf565b81e935536e8b37adbfe844fd017025
7
- data.tar.gz: c040c6f3ea54b21d6c23addd72c9a787075e17cc4413c5a88c911c7e06c6376e3d5969bea85f106c53bb8b226a155b5ad01646626372488496120f580475cec7
6
+ metadata.gz: fcd8b1b91f2d6a9563ff137b8ceeb50fd82cbda3738792680a3d08b418a204102be98b3527dce1da6e1495a6a0c0647cc85e0d3a073de53e0657fdb8a4d4b490
7
+ data.tar.gz: 0f374fc4577b29cc3165ed5ad756243298d1d5c691a9f0e865c974411a9eec3430b544f8abf176b47d8a1e816d7f26b36eff819a9fe483bbf13798aef801f9ae
data/README.md CHANGED
@@ -24,12 +24,12 @@ gem 'rails_error_dashboard'
24
24
 
25
25
  Username: `gandalf` · Password: `youshallnotpass`
26
26
 
27
- Experience the full dashboard with 250+ realistic Rails errors, LOTR-themed demo data, and all features enabled.
27
+ Experience the full dashboard with 480+ realistic Rails errors, LOTR-themed demo data, cause chains, enriched context, auto-reopened errors, and all features enabled.
28
28
 
29
29
  ---
30
30
 
31
31
  ### ⚠️ BETA SOFTWARE
32
- This Rails Engine is in beta and under active development. While functional and tested (1,300+ tests passing, including browser-based system tests), the API may change before v1.0.0. Use in production at your own discretion.
32
+ This Rails Engine is in beta and under active development. While functional and tested (1,800+ tests passing, including browser-based system tests), the API may change before v1.0.0. Use in production at your own discretion.
33
33
 
34
34
  **Supports**: Rails 7.0 - 8.1 | Ruby 3.2 - 4.0
35
35
 
@@ -169,6 +169,22 @@ config.git_repository_url = "https://github.com/user/repo"
169
169
 
170
170
  **📖 [Complete documentation →](docs/SOURCE_CODE_INTEGRATION.md)**
171
171
 
172
+ #### 🆕 v0.2 Quick Wins (NEW!)
173
+
174
+ **11 features that make error tracking smarter, safer, and more actionable:**
175
+
176
+ - **Exception Cause Chains** — Automatically captures the full `cause` chain (e.g., `SocketError` → `RuntimeError`) so you see root causes, not just wrappers
177
+ - **Enriched Context** — Every HTTP error captures `http_method`, `hostname`, `content_type`, and `request_duration_ms` automatically
178
+ - **Custom Fingerprint** — Override error grouping with a lambda: group `RecordNotFound` by controller, or any custom logic
179
+ - **CurrentAttributes Integration** — Zero-config capture of `Current.user`, `Current.account`, `Current.request_id`
180
+ - **Environment Info** — Ruby version, Rails version, gem versions, server, and database adapter captured at error time
181
+ - **Sensitive Data Filtering** — Passwords, tokens, secrets, and API keys auto-filtered from error context before storage
182
+ - **Auto-Reopen** — Resolved errors automatically reopen when they recur, with a "Reopened" badge in the UI
183
+ - **Notification Throttling** — Severity filters, per-error cooldown, and milestone threshold alerts prevent alert fatigue
184
+ - **BRIN Indexes** — PostgreSQL BRIN index on `occurred_at` for dramatically faster time-range queries (72KB vs 676MB)
185
+ - **Structured Backtrace** — Uses `backtrace_locations` for richer backtrace data with proper path/line/method fields
186
+ - **Reduced Dependencies** — Core gem now requires only `rails` + `pagy`; `browser`, `chartkick`, `httparty`, `turbo-rails` are optional
187
+
172
188
  #### 🔌 Plugin System
173
189
  Extensible architecture with event hooks (`on_error_logged`, `on_error_resolved`, `on_threshold_exceeded`). Built-in examples for Jira integration, metrics tracking, audit logging. Easy to create custom plugins - just drop a file in `config/initializers/error_dashboard_plugins/`.
174
190
 
@@ -640,7 +656,7 @@ Clean, maintainable, testable architecture you can understand and modify.
640
656
 
641
657
  ## 🧪 Testing
642
658
 
643
- 1,300+ tests covering unit, integration, and browser-based system tests.
659
+ 1,800+ tests covering unit, integration, and browser-based system tests.
644
660
 
645
661
  ### Running Tests
646
662
 
@@ -733,7 +749,7 @@ Rails Error Dashboard is available as open source under the terms of the [MIT Li
733
749
  <details>
734
750
  <summary><strong>Is this production-ready?</strong></summary>
735
751
 
736
- This is currently in **beta** but actively tested with 935+ passing tests across Rails 7.0-8.0 and Ruby 3.2-3.4. Many users are running it in production. See [production requirements](docs/FEATURES.md#production-readiness).
752
+ 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).
737
753
  </details>
738
754
 
739
755
  <details>
@@ -19,6 +19,7 @@ module RailsErrorDashboard
19
19
  assignee_name
20
20
  priority_level
21
21
  hide_snoozed
22
+ reopened
22
23
  sort_by
23
24
  sort_direction
24
25
  ].freeze
@@ -10,9 +10,19 @@ module RailsErrorDashboard
10
10
  # @param exception_data [Hash] Serialized exception data
11
11
  # @param context [Hash] Error context (request, user, etc.)
12
12
  def perform(exception_data, context)
13
+ # Normalize string keys (ActiveJob may deserialize with string keys)
14
+ exception_data = exception_data.symbolize_keys if exception_data.respond_to?(:symbolize_keys)
15
+ context = context.symbolize_keys if context.respond_to?(:symbolize_keys)
16
+
13
17
  # Reconstruct the exception from serialized data
14
18
  exception = reconstruct_exception(exception_data)
15
19
 
20
+ # Pass pre-extracted cause chain via context so LogError can use it
21
+ # (reconstructed exceptions don't have Ruby's built-in cause set)
22
+ if exception_data[:cause_chain]
23
+ context[:_serialized_cause_chain] = exception_data[:cause_chain]
24
+ end
25
+
16
26
  # Log the error synchronously in the background job
17
27
  # Call .new().call to bypass async check (we're already async)
18
28
  Commands::LogError.new(exception, context).call
@@ -73,11 +73,7 @@ module RailsErrorDashboard
73
73
  def send_slack_notification(error_log, anomaly_data, config)
74
74
  payload = Services::BaselineAlertPayloadBuilder.slack_payload(error_log, anomaly_data)
75
75
 
76
- HTTParty.post(
77
- config.slack_webhook_url,
78
- body: payload.to_json,
79
- headers: { "Content-Type" => "application/json" }
80
- )
76
+ post_json(config.slack_webhook_url, payload)
81
77
  rescue => e
82
78
  Rails.logger.error("Failed to send baseline alert to Slack: #{e.message}")
83
79
  end
@@ -93,11 +89,7 @@ module RailsErrorDashboard
93
89
  def send_discord_notification(error_log, anomaly_data, config)
94
90
  payload = Services::BaselineAlertPayloadBuilder.discord_payload(error_log, anomaly_data)
95
91
 
96
- HTTParty.post(
97
- config.discord_webhook_url,
98
- body: payload.to_json,
99
- headers: { "Content-Type" => "application/json" }
100
- )
92
+ post_json(config.discord_webhook_url, payload)
101
93
  rescue => e
102
94
  Rails.logger.error("Failed to send baseline alert to Discord: #{e.message}")
103
95
  end
@@ -106,16 +98,28 @@ module RailsErrorDashboard
106
98
  payload = Services::BaselineAlertPayloadBuilder.webhook_payload(error_log, anomaly_data)
107
99
 
108
100
  config.webhook_urls.each do |url|
109
- HTTParty.post(
110
- url,
111
- body: payload.to_json,
112
- headers: { "Content-Type" => "application/json" }
113
- )
101
+ post_json(url, payload)
114
102
  end
115
103
  rescue => e
116
104
  Rails.logger.error("Failed to send baseline alert to webhook: #{e.message}")
117
105
  end
118
106
 
107
+ def post_json(url, payload)
108
+ if defined?(HTTParty)
109
+ HTTParty.post(url, body: payload.to_json,
110
+ headers: { "Content-Type" => "application/json" }, timeout: 10)
111
+ else
112
+ uri = URI(url)
113
+ http = Net::HTTP.new(uri.host, uri.port)
114
+ http.use_ssl = uri.scheme == "https"
115
+ http.open_timeout = 5
116
+ http.read_timeout = 10
117
+ request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/json" })
118
+ request.body = payload.to_json
119
+ http.request(request)
120
+ end
121
+ end
122
+
119
123
  def send_pagerduty_notification(error_log, _anomaly_data, _config)
120
124
  Rails.logger.info(
121
125
  "Baseline alert PagerDuty notification for #{error_log.error_type}"
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "httparty"
4
-
5
3
  module RailsErrorDashboard
6
4
  # Job to send error notifications to Discord via webhook
7
5
  class DiscordErrorNotificationJob < ApplicationJob
@@ -14,16 +12,28 @@ module RailsErrorDashboard
14
12
  return unless webhook_url.present?
15
13
 
16
14
  payload = Services::DiscordPayloadBuilder.call(error_log)
17
-
18
- HTTParty.post(
19
- webhook_url,
20
- body: payload.to_json,
21
- headers: { "Content-Type" => "application/json" },
22
- timeout: 10 # CRITICAL: 10 second timeout to prevent hanging
23
- )
15
+ post_json(webhook_url, payload)
24
16
  rescue StandardError => e
25
17
  Rails.logger.error("[RailsErrorDashboard] Failed to send Discord notification: #{e.message}")
26
18
  Rails.logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
27
19
  end
20
+
21
+ private
22
+
23
+ def post_json(url, payload)
24
+ if defined?(HTTParty)
25
+ HTTParty.post(url, body: payload.to_json,
26
+ headers: { "Content-Type" => "application/json" }, timeout: 10)
27
+ else
28
+ uri = URI(url)
29
+ http = Net::HTTP.new(uri.host, uri.port)
30
+ http.use_ssl = uri.scheme == "https"
31
+ http.open_timeout = 5
32
+ http.read_timeout = 10
33
+ request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/json" })
34
+ request.body = payload.to_json
35
+ http.request(request)
36
+ end
37
+ end
28
38
  end
29
39
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "httparty"
4
-
5
3
  module RailsErrorDashboard
6
4
  # Job to send critical error notifications to PagerDuty
7
5
  # Only triggers for critical severity errors
@@ -20,20 +18,48 @@ module RailsErrorDashboard
20
18
  return unless routing_key.present?
21
19
 
22
20
  payload = Services::PagerdutyPayloadBuilder.call(error_log, routing_key: routing_key)
21
+ response = post_json(PAGERDUTY_EVENTS_API, payload)
23
22
 
24
- response = HTTParty.post(
25
- PAGERDUTY_EVENTS_API,
26
- body: payload.to_json,
27
- headers: { "Content-Type" => "application/json" },
28
- timeout: 10 # CRITICAL: 10 second timeout to prevent hanging
29
- )
30
-
31
- unless response.success?
32
- Rails.logger.error("[RailsErrorDashboard] PagerDuty API error: #{response.code} - #{response.body}")
23
+ unless response_success?(response)
24
+ Rails.logger.error("[RailsErrorDashboard] PagerDuty API error: #{response_code(response)} - #{response_body(response)}")
33
25
  end
34
26
  rescue StandardError => e
35
27
  Rails.logger.error("[RailsErrorDashboard] Failed to send PagerDuty notification: #{e.message}")
36
28
  Rails.logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
37
29
  end
30
+
31
+ private
32
+
33
+ def post_json(url, payload)
34
+ if defined?(HTTParty)
35
+ HTTParty.post(url, body: payload.to_json,
36
+ headers: { "Content-Type" => "application/json" }, timeout: 10)
37
+ else
38
+ uri = URI(url)
39
+ http = Net::HTTP.new(uri.host, uri.port)
40
+ http.use_ssl = uri.scheme == "https"
41
+ http.open_timeout = 5
42
+ http.read_timeout = 10
43
+ request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/json" })
44
+ request.body = payload.to_json
45
+ http.request(request)
46
+ end
47
+ end
48
+
49
+ def response_success?(response)
50
+ if response.respond_to?(:success?)
51
+ response.success?
52
+ else
53
+ response.is_a?(Net::HTTPSuccess)
54
+ end
55
+ end
56
+
57
+ def response_code(response)
58
+ response.respond_to?(:code) ? response.code : response&.code
59
+ end
60
+
61
+ def response_body(response)
62
+ response.respond_to?(:body) ? response.body : response&.body
63
+ end
38
64
  end
39
65
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ # Background job to enforce the retention_days configuration.
5
+ # Deletes error logs (and their associated records) older than the configured threshold.
6
+ # Uses find_each + destroy to respect dependent: :destroy on associations.
7
+ #
8
+ # Schedule this job daily via your preferred scheduler (SolidQueue, Sidekiq, cron).
9
+ #
10
+ # @example Schedule in initializer
11
+ # RailsErrorDashboard.configure do |config|
12
+ # config.retention_days = 90
13
+ # end
14
+ class RetentionCleanupJob < ApplicationJob
15
+ queue_as :default
16
+
17
+ # @return [Integer] number of errors deleted
18
+ def perform
19
+ retention_days = RailsErrorDashboard.configuration.retention_days
20
+ return 0 if retention_days.blank?
21
+
22
+ cutoff = retention_days.days.ago
23
+ deleted_count = 0
24
+
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
30
+ end
31
+
32
+ if deleted_count > 0
33
+ RailsErrorDashboard::Logger.info(
34
+ "[RailsErrorDashboard] Retention cleanup: deleted #{deleted_count} errors older than #{retention_days} days"
35
+ )
36
+ end
37
+
38
+ deleted_count
39
+ rescue => e
40
+ RailsErrorDashboard::Logger.error("[RailsErrorDashboard] Retention cleanup failed: #{e.class} - #{e.message}")
41
+ 0
42
+ end
43
+ end
44
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "httparty"
4
-
5
3
  module RailsErrorDashboard
6
4
  # Job to send error notifications to custom webhook URLs
7
5
  # Supports multiple webhooks for different integrations
@@ -30,23 +28,47 @@ module RailsErrorDashboard
30
28
  private
31
29
 
32
30
  def send_webhook(url, payload, error_log)
33
- response = HTTParty.post(
34
- url,
35
- body: payload.to_json,
36
- headers: {
37
- "Content-Type" => "application/json",
38
- "User-Agent" => "RailsErrorDashboard/1.0",
39
- "X-Error-Dashboard-Event" => "error.created",
40
- "X-Error-Dashboard-ID" => error_log.id.to_s
41
- },
42
- timeout: 10 # CRITICAL: 10 second timeout to prevent hanging
43
- )
44
-
45
- unless response.success?
46
- Rails.logger.warn("[RailsErrorDashboard] Webhook failed for #{url}: #{response.code}")
31
+ headers = {
32
+ "Content-Type" => "application/json",
33
+ "User-Agent" => "RailsErrorDashboard/1.0",
34
+ "X-Error-Dashboard-Event" => "error.created",
35
+ "X-Error-Dashboard-ID" => error_log.id.to_s
36
+ }
37
+
38
+ response = post_json(url, payload, headers)
39
+
40
+ unless response_success?(response)
41
+ Rails.logger.warn("[RailsErrorDashboard] Webhook failed for #{url}: #{response_code(response)}")
47
42
  end
48
43
  rescue StandardError => e
49
44
  Rails.logger.error("[RailsErrorDashboard] Webhook error for #{url}: #{e.message}")
50
45
  end
46
+
47
+ def post_json(url, payload, headers)
48
+ if defined?(HTTParty)
49
+ HTTParty.post(url, body: payload.to_json, headers: headers, timeout: 10)
50
+ else
51
+ uri = URI(url)
52
+ http = Net::HTTP.new(uri.host, uri.port)
53
+ http.use_ssl = uri.scheme == "https"
54
+ http.open_timeout = 5
55
+ http.read_timeout = 10
56
+ request = Net::HTTP::Post.new(uri.path, headers)
57
+ request.body = payload.to_json
58
+ http.request(request)
59
+ end
60
+ end
61
+
62
+ def response_success?(response)
63
+ if response.respond_to?(:success?)
64
+ response.success?
65
+ else
66
+ response.is_a?(Net::HTTPSuccess)
67
+ end
68
+ end
69
+
70
+ def response_code(response)
71
+ response.respond_to?(:code) ? response.code : response&.code
72
+ end
51
73
  end
52
74
  end
@@ -4,6 +4,16 @@ module RailsErrorDashboard
4
4
  class ErrorLog < ErrorLogsRecord
5
5
  self.table_name = "rails_error_dashboard_error_logs"
6
6
 
7
+ # Transient flag: set to true when a resolved/wont_fix error is reopened by FindOrIncrementError.
8
+ # Not persisted — used by LogError to decide notification behavior.
9
+ attr_accessor :just_reopened
10
+
11
+ # Was this error previously resolved and then reopened due to recurrence?
12
+ # Uses the persisted `reopened_at` column (set by FindOrIncrementError).
13
+ def reopened?
14
+ respond_to?(:reopened_at) && reopened_at.present?
15
+ end
16
+
7
17
  # Priority level constants
8
18
  # Using industry standard: P0 = Critical (highest), P3 = Low (lowest)
9
19
  PRIORITY_LEVELS = {
@@ -5,17 +5,22 @@
5
5
  # By default, this connects to the same database as the main application.
6
6
  #
7
7
  # To enable a separate error dashboard database:
8
- # 1. Set use_separate_database: true in the gem configuration
9
- # 2. Set database: :error_dashboard (or your custom name) in the gem configuration
10
- # 3. Configure error_dashboard settings in config/database.yml
11
- # 4. Run: rails db:create
12
- # 5. Run: rails db:migrate
8
+ # 1. Set config.use_separate_database = true in the gem configuration
9
+ # 2. Set config.database = :error_dashboard in the gem configuration
10
+ # 3. Add an "error_dashboard:" entry in config/database.yml
11
+ # 4. Run: rails db:create:error_dashboard
12
+ # 5. Run: rails db:migrate:error_dashboard
13
+ #
14
+ # For multi-app setups, point all apps' database.yml "error_dashboard:" entry
15
+ # to the same physical database. Each app is identified by config.application_name.
16
+ #
17
+ # Run "rails error_dashboard:verify" to check your setup.
13
18
  #
14
19
  # Benefits of separate database:
15
20
  # - Performance isolation (error logging doesn't slow down user requests)
16
21
  # - Independent scaling (can put error DB on separate server)
22
+ # - Multi-app support (centralized error tracking across multiple Rails apps)
17
23
  # - Different retention policies (archive old errors without affecting main data)
18
- # - Security isolation (different access controls for error logs)
19
24
  #
20
25
  # Trade-offs:
21
26
  # - No foreign keys between error_logs and users tables
@@ -1148,6 +1148,22 @@ body.dark-mode .source-file-path {
1148
1148
  color: var(--ctp-text) !important; /* Bright text for dark mode */
1149
1149
  }
1150
1150
 
1151
+ /* Backtrace frame number styling */
1152
+ .backtrace-frame-number {
1153
+ display: inline-block;
1154
+ min-width: 2em;
1155
+ text-align: right;
1156
+ margin-right: 0.5em;
1157
+ color: #9ca3af;
1158
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
1159
+ font-size: 0.85em;
1160
+ user-select: none;
1161
+ }
1162
+
1163
+ body.dark-mode .backtrace-frame-number {
1164
+ color: var(--ctp-overlay1);
1165
+ }
1166
+
1151
1167
  /* Backtrace method name styling - override Bootstrap text-info */
1152
1168
  span.backtrace-method-name {
1153
1169
  color: #0066cc !important; /* Darker blue for better readability in light mode */
@@ -87,6 +87,9 @@
87
87
  <% else %>
88
88
  <i class="bi bi-exclamation-circle-fill text-danger" data-bs-toggle="tooltip" title="Unresolved"></i>
89
89
  <% end %>
90
+ <% if error.reopened? %>
91
+ <i class="bi bi-arrow-counterclockwise text-warning ms-1" data-bs-toggle="tooltip" title="Reopened"></i>
92
+ <% end %>
90
93
  </td>
91
94
  <td onclick="event.stopPropagation();">
92
95
  <%= link_to error_path(error), class: "btn btn-sm btn-outline-primary" do %>
@@ -1,5 +1,5 @@
1
1
  <div class="row g-4">
2
- <div class="col-md-3">
2
+ <div class="col">
3
3
  <div class="card stat-card">
4
4
  <div class="card-body">
5
5
  <div class="stat-label mb-2">Today</div>
@@ -7,7 +7,7 @@
7
7
  </div>
8
8
  </div>
9
9
  </div>
10
- <div class="col-md-3">
10
+ <div class="col">
11
11
  <div class="card stat-card">
12
12
  <div class="card-body">
13
13
  <div class="stat-label mb-2">This Week</div>
@@ -15,7 +15,7 @@
15
15
  </div>
16
16
  </div>
17
17
  </div>
18
- <div class="col-md-3">
18
+ <div class="col">
19
19
  <div class="card stat-card">
20
20
  <div class="card-body">
21
21
  <div class="stat-label mb-2">Unresolved</div>
@@ -23,7 +23,7 @@
23
23
  </div>
24
24
  </div>
25
25
  </div>
26
- <div class="col-md-3">
26
+ <div class="col">
27
27
  <div class="card stat-card">
28
28
  <div class="card-body">
29
29
  <div class="stat-label mb-2">Resolved</div>
@@ -31,4 +31,12 @@
31
31
  </div>
32
32
  </div>
33
33
  </div>
34
+ <div class="col">
35
+ <div class="card stat-card">
36
+ <div class="card-body">
37
+ <div class="stat-label mb-2">Reopened</div>
38
+ <div class="stat-value text-warning"><%= stats[:reopened] %></div>
39
+ </div>
40
+ </div>
41
+ </div>
34
42
  </div>
@@ -1,7 +1,7 @@
1
1
  <% content_for :page_title, "Errors" %>
2
2
 
3
- <!-- Subscribe to Turbo Stream updates (only if ActionCable is available) -->
4
- <% if defined?(ActionCable) %>
3
+ <!-- Subscribe to Turbo Stream updates (only if Turbo Streams is available) -->
4
+ <% if defined?(Turbo::StreamsHelper) && defined?(ActionCable) && respond_to?(:turbo_stream_from) %>
5
5
  <%= turbo_stream_from "error_list" %>
6
6
  <% end %>
7
7
 
@@ -74,8 +74,8 @@
74
74
  </div>
75
75
  <% end %>
76
76
 
77
- <!-- 7-Day Error Trend -->
78
- <% if @stats[:errors_trend_7d]&.any? %>
77
+ <!-- 7-Day Error Trend (requires chartkick gem) -->
78
+ <% if @stats[:errors_trend_7d]&.any? && respond_to?(:line_chart) %>
79
79
  <div class="row g-4 mb-4">
80
80
  <div class="col-md-8">
81
81
  <div class="card">
@@ -186,9 +186,11 @@
186
186
  <!-- Quick Filter Buttons -->
187
187
  <div class="d-flex gap-2 mb-3">
188
188
  <%= link_to "All Errors", errors_path,
189
- class: "btn btn-sm #{params[:assigned_to].blank? ? 'btn-primary' : 'btn-outline-primary'}" %>
189
+ class: "btn btn-sm #{params[:assigned_to].blank? && params[:reopened].blank? ? 'btn-primary' : 'btn-outline-primary'}" %>
190
190
  <%= link_to "Unassigned", errors_path(assigned_to: '__unassigned__'),
191
191
  class: "btn btn-sm #{params[:assigned_to] == '__unassigned__' ? 'btn-primary' : 'btn-outline-primary'}" %>
192
+ <%= link_to "Reopened", errors_path(reopened: 'true'),
193
+ class: "btn btn-sm #{params[:reopened] == 'true' ? 'btn-primary' : 'btn-outline-primary'}" %>
192
194
  </div>
193
195
 
194
196
  <%= form_with url: errors_path, method: :get, class: "row g-3", data: { turbo: false } do %>