rails_error_dashboard 0.5.14 → 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 +4 -4
- data/app/views/layouts/rails_error_dashboard.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/index.html.erb +2 -2
- data/lib/rails_error_dashboard/commands/log_error.rb +14 -3
- data/lib/rails_error_dashboard/error_reporter.rb +11 -1
- data/lib/rails_error_dashboard/services/backtrace_parser.rb +10 -4
- data/lib/rails_error_dashboard/services/backtrace_processor.rb +44 -2
- data/lib/rails_error_dashboard/services/error_broadcaster.rb +19 -4
- data/lib/rails_error_dashboard/services/notification_helpers.rb +9 -2
- data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +21 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3569ff68dcb977b53c8565d17e3f95de2b28b9c3abc462c77c6877277f79d101
|
|
4
|
+
data.tar.gz: b27615768c8fc87bca5f55f0deee1d4f1ec5c9c7466956480180808779708fc9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 85b4d1f4017fafc58181547f3fca6c87b19d72b4b19092462dd89cdfd35cc7d84cd2203244b5273db2e7901e09c54530a8ee53baf904eccbb016f7df64b3cfbe
|
|
7
|
+
data.tar.gz: f77c96cdd3d5e322025086c889417af20668bdd8cbf582fb93b512eecaed4971596db330a81fe7912536e6bace26dab34f9d88b31f88123a887f0b0468d0a1db
|
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
|
98
|
-
file_path.include?("/app/") ||
|
|
99
|
-
|
|
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.
|
|
@@ -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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
499
|
+
RED (Rails Error Dashboard) v0.5.15
|
|
500
500
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
501
501
|
|
|
502
502
|
First install:
|