rails_error_dashboard 0.5.13 → 0.5.15

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: f0771a79dec323d80d0182e82dd4f5e9b9af6374a036baab00aa8b219c49ab38
4
- data.tar.gz: 79facf6902dca535dae648469452b95f9fc093fe79b5f318790542384e6de4a1
3
+ metadata.gz: 3569ff68dcb977b53c8565d17e3f95de2b28b9c3abc462c77c6877277f79d101
4
+ data.tar.gz: b27615768c8fc87bca5f55f0deee1d4f1ec5c9c7466956480180808779708fc9
5
5
  SHA512:
6
- metadata.gz: d36de8595bd53ad2ec533d6d98fc7ba149b6dc218af477271daaa1f764456a72bdb64ac905090dfcc66749b6abfe833f983579890a7fbdaf254426549a61cc4a
7
- data.tar.gz: 56729d084a67b300f231bb74a4108962a05c02ce73e97638dd9461464f14cc92e3085465e6645d69070e3b47f5281a3e081cd8cd8aa562ac3dfb373260a6b3a4
6
+ metadata.gz: 85b4d1f4017fafc58181547f3fca6c87b19d72b4b19092462dd89cdfd35cc7d84cd2203244b5273db2e7901e09c54530a8ee53baf904eccbb016f7df64b3cfbe
7
+ data.tar.gz: f77c96cdd3d5e322025086c889417af20668bdd8cbf582fb93b512eecaed4971596db330a81fe7912536e6bace26dab34f9d88b31f88123a887f0b0468d0a1db
data/README.md CHANGED
@@ -527,7 +527,7 @@ Built with [Rails](https://rubyonrails.org/) · UI by [Bootstrap 5](https://getb
527
527
 
528
528
  [![Contributors](https://contrib.rocks/image?repo=AnjanJ/rails_error_dashboard)](https://github.com/AnjanJ/rails_error_dashboard/graphs/contributors)
529
529
 
530
- Special thanks to [@bonniesimon](https://github.com/bonniesimon), [@gundestrup](https://github.com/gundestrup), [@midwire](https://github.com/midwire), [@RafaelTurtle](https://github.com/RafaelTurtle), and [@j4rs](https://github.com/j4rs). See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the full list.
530
+ Special thanks to [@bonniesimon](https://github.com/bonniesimon), [@gundestrup](https://github.com/gundestrup), [@midwire](https://github.com/midwire), [@RafaelTurtle](https://github.com/RafaelTurtle), [@j4rs](https://github.com/j4rs), and [@gmarziou](https://github.com/gmarziou). See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the full list.
531
531
 
532
532
  ---
533
533
 
@@ -1855,7 +1855,7 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1855
1855
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
1856
1856
 
1857
1857
  <!-- Stimulus (for loading state management) -->
1858
- <script src="https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.min.js"></script>
1858
+ <script src="https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.umd.js"></script>
1859
1859
 
1860
1860
  <!-- Loading State Controller -->
1861
1861
  <script>
@@ -1,7 +1,7 @@
1
1
  <% content_for :page_title, "Errors" %>
2
2
 
3
- <!-- Subscribe to Turbo Stream updates (only if Turbo Streams is available) -->
4
- <% if defined?(Turbo::StreamsHelper) && defined?(ActionCable) && respond_to?(:turbo_stream_from) %>
3
+ <!-- Subscribe to Turbo Stream updates (only if Turbo Streams + ActionCable pubsub are available) -->
4
+ <% if defined?(Turbo::StreamsHelper) && defined?(ActionCable) && respond_to?(:turbo_stream_from) && RailsErrorDashboard::Services::ErrorBroadcaster.available? %>
5
5
  <%= turbo_stream_from "error_list" %>
6
6
  <% end %>
7
7
 
@@ -67,7 +67,18 @@ module RailsErrorDashboard
67
67
 
68
68
  # Enqueue the async job using ActiveJob
69
69
  # The queue adapter (:sidekiq, :solid_queue, :async) is configured separately
70
- AsyncErrorLoggingJob.perform_later(exception_data, context)
70
+ begin
71
+ AsyncErrorLoggingJob.perform_later(exception_data, context)
72
+ rescue => e
73
+ # Queue adapter failed (e.g., Redis down for Sidekiq). Fall back to
74
+ # sync logging so the error is still captured. Without this rescue,
75
+ # the exception propagates back to ErrorReporter, which re-reports it
76
+ # via Rails.error.report → infinite recursion (issue #114).
77
+ RailsErrorDashboard::Logger.error(
78
+ "[RailsErrorDashboard] Async enqueue failed (#{e.class}: #{e.message}), falling back to sync logging"
79
+ )
80
+ new(exception, context).call
81
+ end
71
82
  end
72
83
 
73
84
  # Serialize cause chain for async job serialization
@@ -87,7 +98,7 @@ module RailsErrorDashboard
87
98
  chain << {
88
99
  class_name: current.class.name,
89
100
  message: current.message&.to_s,
90
- backtrace: current.backtrace&.first(20)
101
+ backtrace: current.backtrace&.first(20)&.map { |line| Services::BacktraceProcessor.shorten_gem_path(line) }
91
102
  }
92
103
 
93
104
  current = current.respond_to?(:cause) ? current.cause : nil
@@ -292,7 +303,7 @@ module RailsErrorDashboard
292
303
  # CRITICAL: Log but never propagate exception
293
304
  RailsErrorDashboard::Logger.error("[RailsErrorDashboard] LogError command failed: #{e.class} - #{e.message}")
294
305
  RailsErrorDashboard::Logger.error("Original exception: #{@exception.class} - #{@exception.message}") if @exception
295
- RailsErrorDashboard::Logger.error("Context: #{@context.inspect}") if @context
306
+ RailsErrorDashboard::Logger.error("Context: #{@context.inspect.truncate(500)}") if @context
296
307
  RailsErrorDashboard::Logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
297
308
  nil # Explicitly return nil, never raise
298
309
  end
@@ -22,8 +22,16 @@ module RailsErrorDashboard
22
22
  # Skip low-severity warnings
23
23
  return if handled && severity == :warning
24
24
 
25
+ # Prevent recursive error capture (issue #114).
26
+ # If LogError.call itself triggers a new error (e.g., Redis down causes
27
+ # perform_later to fail), Rails.error.report fires again for that failure.
28
+ # Without this guard, each cycle double-escapes JSON → exponential payload growth.
29
+ return if Thread.current[:rails_error_dashboard_logging]
30
+
25
31
  # CRITICAL: Wrap entire process in rescue to ensure failures don't break the app
26
32
  begin
33
+ Thread.current[:rails_error_dashboard_logging] = true
34
+
27
35
  # Enrich context with request data from Thread.current when available.
28
36
  # Rails internals (ActionDispatch::Executor) report errors with
29
37
  # source: "application.action_dispatch" but pass NO request object,
@@ -60,9 +68,11 @@ module RailsErrorDashboard
60
68
  # Log failure for debugging but NEVER propagate exception
61
69
  RailsErrorDashboard::Logger.error("[RailsErrorDashboard] ErrorReporter failed: #{e.class} - #{e.message}")
62
70
  RailsErrorDashboard::Logger.error("Original error: #{error.class} - #{error.message}") if error
63
- RailsErrorDashboard::Logger.error("Context: #{context.inspect}") if context
71
+ RailsErrorDashboard::Logger.error("Context: #{context.inspect.truncate(500)}") if context
64
72
  RailsErrorDashboard::Logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
65
73
  nil # Explicitly return nil, never raise
74
+ ensure
75
+ Thread.current[:rails_error_dashboard_logging] = nil
66
76
  end
67
77
  end
68
78
  end
@@ -94,13 +94,19 @@ module RailsErrorDashboard
94
94
  end
95
95
 
96
96
  def app_code?(file_path)
97
- # Match /app/, /lib/ directories in the application
98
- file_path.include?("/app/") ||
99
- (file_path.include?("/lib/") && !file_path.include?("/gems/") && !file_path.include?("/ruby/"))
97
+ # Match /app/ or app/ (shortened paths start without leading /)
98
+ return true if file_path.include?("/app/") || file_path.start_with?("app/")
99
+
100
+ # Match /lib/ or lib/ but exclude gems and ruby stdlib
101
+ lib_path = file_path.include?("/lib/") || file_path.start_with?("lib/")
102
+ return false unless lib_path
103
+
104
+ !file_path.include?("/gems/") && !file_path.include?("/ruby/") &&
105
+ !file_path.start_with?("gems/") && !file_path.start_with?("ruby/")
100
106
  end
101
107
 
102
108
  def gem_code?(file_path)
103
- file_path.include?("/gems/") ||
109
+ file_path.include?("/gems/") || file_path.start_with?("gems/") ||
104
110
  file_path.include?("/bundler/gems/") ||
105
111
  file_path.include?("/vendor/bundle/")
106
112
  end
@@ -5,7 +5,15 @@ module RailsErrorDashboard
5
5
  # Pure algorithm service for backtrace processing
6
6
  # Handles truncation and signature generation with no database access.
7
7
  class BacktraceProcessor
8
- # Truncate backtrace to a maximum number of lines
8
+ # Truncate backtrace to a maximum number of lines and shorten gem paths.
9
+ #
10
+ # Gem paths like:
11
+ # /home/user/.local/share/mise/installs/ruby/4.0.2/lib/ruby/gems/4.0.0/gems/actionpack-8.1.3/lib/...
12
+ # are shortened to:
13
+ # gems/actionpack-8.1.3/lib/...
14
+ #
15
+ # This saves significant disk space without losing debugging value (issue #115).
16
+ #
9
17
  # @param backtrace [Array<String>, nil] The backtrace lines
10
18
  # @param max_lines [Integer] Maximum lines to keep
11
19
  # @return [String, nil] Truncated backtrace as a single string
@@ -14,7 +22,7 @@ module RailsErrorDashboard
14
22
 
15
23
  max_lines ||= RailsErrorDashboard.configuration.max_backtrace_lines
16
24
 
17
- limited_backtrace = backtrace.first(max_lines)
25
+ limited_backtrace = backtrace.first(max_lines).map { |line| shorten_gem_path(line) }
18
26
  result = limited_backtrace.join("\n")
19
27
 
20
28
  if backtrace.length > max_lines
@@ -25,6 +33,40 @@ module RailsErrorDashboard
25
33
  result
26
34
  end
27
35
 
36
+ # Shorten gem/ruby paths to remove user-specific prefixes.
37
+ # Preserves gem name + version for debugging.
38
+ #
39
+ # Examples:
40
+ # /home/user/.gem/ruby/3.4.0/gems/rack-3.2.6/lib/rack/head.rb:15
41
+ # → gems/rack-3.2.6/lib/rack/head.rb:15
42
+ #
43
+ # /home/user/.local/share/mise/installs/ruby/4.0.2/lib/ruby/4.0.0/net/http.rb:1234
44
+ # → ruby/4.0.0/net/http.rb:1234
45
+ #
46
+ # /home/user/myapp/app/controllers/users_controller.rb:10
47
+ # → app/controllers/users_controller.rb:10
48
+ #
49
+ # @param line [String] A single backtrace line
50
+ # @return [String] The line with shortened path
51
+ def self.shorten_gem_path(line)
52
+ # Strip everything before /gems/ (gem code)
53
+ if line.include?("/gems/")
54
+ line.sub(%r{^.*/gems/}, "gems/")
55
+ # Strip everything before /lib/ruby/ (Ruby stdlib)
56
+ elsif line.include?("/lib/ruby/")
57
+ line.sub(%r{^.*/lib/ruby/}, "ruby/")
58
+ # Strip everything before /app/ (application code)
59
+ elsif line.include?("/app/")
60
+ line.sub(%r{^.*/app/}, "app/")
61
+ # Strip everything before /lib/ for app lib code (but not gem/ruby paths already handled)
62
+ elsif line.include?("/lib/")
63
+ line.sub(%r{^.*/lib/}, "lib/")
64
+ else
65
+ line
66
+ end
67
+ end
68
+ # Keep public — used by LogError for cause chain backtrace shortening too.
69
+
28
70
  # Calculate a signature hash from backtrace for fuzzy similarity matching
29
71
  # Extracts file paths and method names, ignoring line numbers,
30
72
  # then produces an order-independent SHA256 digest.
@@ -66,7 +66,7 @@ module RailsErrorDashboard
66
66
  hostname = @error.respond_to?(:hostname) && @error.hostname.presence
67
67
  return nil unless hostname
68
68
 
69
- scheme = hostname.include?("localhost") ? "http" : "https"
69
+ scheme = local_host?(hostname) ? "http" : "https"
70
70
  "#{scheme}://#{hostname}#{request_url}"
71
71
  end
72
72
 
@@ -75,6 +75,10 @@ module RailsErrorDashboard
75
75
  escaped = str.to_s.gsub("'") { "'\\''" }
76
76
  "'#{escaped}'"
77
77
  end
78
+
79
+ def local_host?(hostname)
80
+ hostname.match?(/\A(localhost|127\.\d+\.\d+\.\d+|::1|0\.0\.0\.0)(:\d+)?\z/)
81
+ end
78
82
  end
79
83
  end
80
84
  end
@@ -90,17 +90,32 @@ module RailsErrorDashboard
90
90
  )
91
91
  end
92
92
 
93
- # Check if broadcasting infrastructure is available
93
+ # Check if broadcasting infrastructure is available.
94
+ # Returns false when Turbo/ActionCable isn't loaded, or when the
95
+ # ActionCable pubsub adapter can't be reached (e.g., Redis down).
96
+ # Uses a 60-second cooldown after failure to avoid hammering a
97
+ # dead Redis on every error (issue #114).
94
98
  # @return [Boolean]
95
99
  def self.available?
96
100
  return false unless defined?(Turbo)
97
101
  return false unless defined?(ActionCable)
98
102
 
99
- Rails.cache.write("rails_error_dashboard_broadcast_test", true, expires_in: 1.second)
100
- Rails.cache.delete("rails_error_dashboard_broadcast_test")
103
+ # Circuit breaker: skip broadcast attempts for 60s after a failure
104
+ if @broadcast_unavailable_until && Time.current < @broadcast_unavailable_until
105
+ return false
106
+ end
107
+
108
+ # Verify the pubsub adapter is reachable — without this,
109
+ # broadcast_* calls attempt Redis and fail loudly when it's down
110
+ server = ActionCable.server
111
+ return false unless server.respond_to?(:pubsub)
112
+
113
+ server.pubsub
114
+ @broadcast_unavailable_until = nil
101
115
  true
102
116
  rescue => e
103
- Rails.logger.debug("[RailsErrorDashboard] Broadcast not available: #{e.message}")
117
+ @broadcast_unavailable_until = Time.current + 60
118
+ RailsErrorDashboard::Logger.debug("[RailsErrorDashboard] Broadcast not available (pausing 60s): #{e.message}")
104
119
  false
105
120
  end
106
121
  end
@@ -13,9 +13,16 @@ module RailsErrorDashboard
13
13
  # @param error_log [ErrorLog] The error
14
14
  # @return [String] Full URL to the error detail page
15
15
  def dashboard_url(error_log)
16
- base_url = RailsErrorDashboard.configuration.dashboard_base_url || "http://localhost:3000"
16
+ base_url = (RailsErrorDashboard.configuration.dashboard_base_url || "http://localhost:3000").chomp("/")
17
17
  mount_path = RailsErrorDashboard.configuration.engine_mount_path
18
- "#{base_url}#{mount_path}/errors/#{error_log.id}"
18
+
19
+ # Avoid doubling the mount path when base_url already includes it.
20
+ # e.g., base_url="https://app.com/red" + mount_path="/red" → don't produce "/red/red"
21
+ if mount_path.present? && base_url.end_with?(mount_path.chomp("/"))
22
+ "#{base_url}/errors/#{error_log.id}"
23
+ else
24
+ "#{base_url}#{mount_path}/errors/#{error_log.id}"
25
+ end
19
26
  end
20
27
 
21
28
  # Truncate a message to a maximum length
@@ -53,6 +53,27 @@ module RailsErrorDashboard
53
53
  return false unless config.enable_issue_tracking
54
54
  return false if error_log.external_issue_url.present?
55
55
 
56
+ # Check if another error record with the same hash already has a linked
57
+ # issue. The 24-hour dedup window in FindOrIncrementError can create new
58
+ # ErrorLog records for the same logical error — we must not create
59
+ # duplicate GitHub/GitLab issues for them (issue #114 screenshot).
60
+ existing = ErrorLog
61
+ .where(error_hash: error_log.error_hash, application_id: error_log.application_id)
62
+ .where.not(external_issue_url: [ nil, "" ])
63
+ .where.not(id: error_log.id)
64
+ .order(created_at: :desc)
65
+ .first
66
+
67
+ if existing
68
+ # Link this record to the existing issue instead of creating a new one
69
+ error_log.update_columns(
70
+ external_issue_url: existing.external_issue_url,
71
+ external_issue_number: existing.external_issue_number,
72
+ external_issue_provider: existing.external_issue_provider
73
+ )
74
+ return false
75
+ end
76
+
56
77
  # First occurrence — always auto-create
57
78
  return true if error_log.occurrence_count == 1
58
79
 
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.5.13"
2
+ VERSION = "0.5.15"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_error_dashboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.13
4
+ version: 0.5.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anjan Jagirdar
@@ -496,7 +496,7 @@ metadata:
496
496
  funding_uri: https://github.com/sponsors/AnjanJ
497
497
  post_install_message: |
498
498
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
499
- RED (Rails Error Dashboard) v0.5.13
499
+ RED (Rails Error Dashboard) v0.5.15
500
500
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
501
501
 
502
502
  First install: