rails_error_dashboard 0.5.2 → 0.5.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 +4 -4
- data/README.md +2 -2
- data/app/controllers/rails_error_dashboard/errors_controller.rb +6 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +22 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +3 -0
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +3 -3
- data/db/migrate/20260306000003_create_rails_error_dashboard_swallowed_exceptions.rb +3 -3
- data/db/migrate/20260325000001_fix_swallowed_exceptions_index_for_mysql.rb +49 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +3 -3
- data/lib/rails_error_dashboard/commands/flush_swallowed_exceptions.rb +5 -5
- data/lib/rails_error_dashboard/configuration.rb +18 -0
- data/lib/rails_error_dashboard/services/markdown_error_formatter.rb +251 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85ee33f8363a0a875c4c00bd2242a7d282dba7502cc78cf8af84a4583f376121
|
|
4
|
+
data.tar.gz: 51183380c9642cbabc19582daa191ee7ee4c161cc2c98fbc3dd6693459e91b40
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 827a994438026c7b42c42927665406c165357de706014596f533d1d23b0121e33a71aa6b2a55f131e274abe2e24dde6d484a4c6b869958363a2ef4fce641c378
|
|
7
|
+
data.tar.gz: ffa7bc26d8f565f10835735e2721589c6c56a13f1ee64f72a06e1b6cd5913c6e5ad8278838368578b57ec99a9cb46ddbf304ee27db4e78c74218db39cc778286
|
data/README.md
CHANGED
|
@@ -154,9 +154,9 @@ config.enable_git_blame = true
|
|
|
154
154
|
</details>
|
|
155
155
|
|
|
156
156
|
<details>
|
|
157
|
-
<summary><strong>Error Replay — Copy as cURL / RSpec</strong></summary>
|
|
157
|
+
<summary><strong>Error Replay — Copy as cURL / RSpec / LLM Markdown</strong></summary>
|
|
158
158
|
|
|
159
|
-
Replay failing requests with one click. Copy the request as a cURL command
|
|
159
|
+
Replay failing requests with one click. Copy the request as a cURL command, generate an RSpec test, or **copy all error details as clean Markdown** for pasting into an LLM session. The LLM export includes app backtrace, cause chain, local/instance variables, breadcrumbs, environment, system health, and related errors — with framework frames filtered and sensitive data preserved as `[FILTERED]`.
|
|
160
160
|
|
|
161
161
|
[Complete documentation →](docs/FEATURES.md#error-details-page)
|
|
162
162
|
</details>
|
|
@@ -4,6 +4,7 @@ module RailsErrorDashboard
|
|
|
4
4
|
class ErrorsController < ApplicationController
|
|
5
5
|
before_action :authenticate_dashboard_user!
|
|
6
6
|
before_action :set_application_context
|
|
7
|
+
before_action :check_default_credentials
|
|
7
8
|
|
|
8
9
|
FILTERABLE_PARAMS = %i[
|
|
9
10
|
error_type
|
|
@@ -78,6 +79,7 @@ module RailsErrorDashboard
|
|
|
78
79
|
# - parent_cascade_patterns/child_cascade_patterns: Used if cascade detection is enabled
|
|
79
80
|
@error = ErrorLog.includes(:comments, :parent_cascade_patterns, :child_cascade_patterns).find(params[:id])
|
|
80
81
|
@related_errors = @error.related_errors(limit: 5, application_id: @current_application_id)
|
|
82
|
+
@error_markdown = Services::MarkdownErrorFormatter.call(@error, related_errors: @related_errors)
|
|
81
83
|
|
|
82
84
|
# Dispatch plugin event for error viewed
|
|
83
85
|
RailsErrorDashboard::PluginRegistry.dispatch(:on_error_viewed, @error)
|
|
@@ -508,6 +510,10 @@ module RailsErrorDashboard
|
|
|
508
510
|
@applications = Application.ordered_by_name.pluck(:name, :id)
|
|
509
511
|
end
|
|
510
512
|
|
|
513
|
+
def check_default_credentials
|
|
514
|
+
@default_credentials_warning = RailsErrorDashboard.configuration.default_credentials?
|
|
515
|
+
end
|
|
516
|
+
|
|
511
517
|
def authenticate_dashboard_user!
|
|
512
518
|
auth_lambda = RailsErrorDashboard.configuration.authenticate_with
|
|
513
519
|
|
|
@@ -774,6 +774,16 @@ body.dark-mode .alert-danger {
|
|
|
774
774
|
color: var(--ctp-text) !important;
|
|
775
775
|
}
|
|
776
776
|
|
|
777
|
+
body.dark-mode .alert-warning {
|
|
778
|
+
background-color: rgba(249, 226, 175, 0.15) !important;
|
|
779
|
+
border-color: var(--ctp-yellow) !important;
|
|
780
|
+
color: var(--ctp-text) !important;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
body.dark-mode .alert-warning code {
|
|
784
|
+
color: var(--ctp-peach) !important;
|
|
785
|
+
}
|
|
786
|
+
|
|
777
787
|
/* Chartkick specific - force text visibility */
|
|
778
788
|
body.dark-mode #chart-1 text,
|
|
779
789
|
body.dark-mode [id^="chart-"] text {
|
|
@@ -1747,6 +1757,18 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
|
|
|
1747
1757
|
|
|
1748
1758
|
<!-- Main content -->
|
|
1749
1759
|
<main class="col-md-10 ms-sm-auto px-md-4" id="mainContent">
|
|
1760
|
+
<% if @default_credentials_warning %>
|
|
1761
|
+
<div class="alert alert-warning d-flex align-items-center mt-3 mb-0" role="alert" style="border-left: 4px solid #ffc107;">
|
|
1762
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="me-2 flex-shrink-0" viewBox="0 0 16 16">
|
|
1763
|
+
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
|
1764
|
+
</svg>
|
|
1765
|
+
<div>
|
|
1766
|
+
<strong>Security Warning:</strong> You are using default credentials (gandalf/youshallnotpass).
|
|
1767
|
+
Set <code>ERROR_DASHBOARD_USER</code> and <code>ERROR_DASHBOARD_PASSWORD</code> environment variables,
|
|
1768
|
+
or configure <code>authenticate_with</code> in your initializer. The app will not boot in production until this is changed.
|
|
1769
|
+
</div>
|
|
1770
|
+
</div>
|
|
1771
|
+
<% end %>
|
|
1750
1772
|
<%= yield %>
|
|
1751
1773
|
</main>
|
|
1752
1774
|
</div>
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
<button type="button" class="btn btn-outline-secondary" onclick="downloadErrorJSON(event)" title="Download error details as JSON">
|
|
33
33
|
<i class="bi bi-download"></i> Export JSON
|
|
34
34
|
</button>
|
|
35
|
+
<button type="button" class="btn btn-outline-secondary" onclick="copyToClipboard(this.dataset.markdown, this)" data-markdown="<%= j @error_markdown %>" title="Copy error details as Markdown for LLM debugging">
|
|
36
|
+
<i class="bi bi-clipboard"></i> Copy for LLM
|
|
37
|
+
</button>
|
|
35
38
|
<% if @error.respond_to?(:muted?) && @error.muted? %>
|
|
36
39
|
<button type="button" class="btn btn-secondary" disabled>
|
|
37
40
|
<i class="bi bi-bell-slash"></i> Muted
|
|
@@ -208,9 +208,9 @@ class CreateRailsErrorDashboardCompleteSchema < ActiveRecord::Migration[7.0]
|
|
|
208
208
|
|
|
209
209
|
# Create swallowed_exceptions table (from 20260306000003)
|
|
210
210
|
create_table :rails_error_dashboard_swallowed_exceptions do |t|
|
|
211
|
-
t.string :exception_class, null: false
|
|
212
|
-
t.string :raise_location, null: false, limit:
|
|
213
|
-
t.string :rescue_location, limit:
|
|
211
|
+
t.string :exception_class, null: false, limit: 250
|
|
212
|
+
t.string :raise_location, null: false, limit: 250
|
|
213
|
+
t.string :rescue_location, limit: 250
|
|
214
214
|
t.datetime :period_hour, null: false
|
|
215
215
|
t.integer :raise_count, null: false, default: 0
|
|
216
216
|
t.integer :rescue_count, null: false, default: 0
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
class CreateRailsErrorDashboardSwallowedExceptions < ActiveRecord::Migration[7.0]
|
|
4
4
|
def change
|
|
5
5
|
create_table :rails_error_dashboard_swallowed_exceptions do |t|
|
|
6
|
-
t.string :exception_class, null: false
|
|
7
|
-
t.string :raise_location, null: false, limit:
|
|
8
|
-
t.string :rescue_location, limit:
|
|
6
|
+
t.string :exception_class, null: false, limit: 250
|
|
7
|
+
t.string :raise_location, null: false, limit: 250
|
|
8
|
+
t.string :rescue_location, limit: 250
|
|
9
9
|
t.datetime :period_hour, null: false
|
|
10
10
|
t.integer :raise_count, null: false, default: 0
|
|
11
11
|
t.integer :rescue_count, null: false, default: 0
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Fix MySQL "Specified key was too long" error on the swallowed_exceptions
|
|
4
|
+
# composite unique index. The original columns (varchar 255 + 500 + 500)
|
|
5
|
+
# total 5042 bytes under utf8mb4, exceeding MySQL's 3072-byte InnoDB limit.
|
|
6
|
+
#
|
|
7
|
+
# Reduces all three string columns to limit: 250, bringing the total to
|
|
8
|
+
# 3022 bytes (250 * 4 * 3 + 6 length prefixes + 16 datetime/bigint).
|
|
9
|
+
#
|
|
10
|
+
# See: https://github.com/AnjanJ/rails_error_dashboard/issues/96
|
|
11
|
+
class FixSwallowedExceptionsIndexForMysql < ActiveRecord::Migration[7.0]
|
|
12
|
+
def up
|
|
13
|
+
return unless table_exists?(:rails_error_dashboard_swallowed_exceptions)
|
|
14
|
+
|
|
15
|
+
# Remove the oversized index if it exists (it may not exist on MySQL
|
|
16
|
+
# since the original migration would have failed at this point)
|
|
17
|
+
if index_exists?(:rails_error_dashboard_swallowed_exceptions, name: "index_swallowed_exceptions_upsert_key")
|
|
18
|
+
remove_index :rails_error_dashboard_swallowed_exceptions, name: "index_swallowed_exceptions_upsert_key"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Shrink columns to fit within MySQL's 3072-byte index key limit
|
|
22
|
+
change_column :rails_error_dashboard_swallowed_exceptions, :exception_class, :string, null: false, limit: 250
|
|
23
|
+
change_column :rails_error_dashboard_swallowed_exceptions, :raise_location, :string, null: false, limit: 250
|
|
24
|
+
change_column :rails_error_dashboard_swallowed_exceptions, :rescue_location, :string, limit: 250
|
|
25
|
+
|
|
26
|
+
# Re-add the index with the smaller columns
|
|
27
|
+
add_index :rails_error_dashboard_swallowed_exceptions,
|
|
28
|
+
[ :exception_class, :raise_location, :rescue_location, :period_hour, :application_id ],
|
|
29
|
+
unique: true,
|
|
30
|
+
name: "index_swallowed_exceptions_upsert_key"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def down
|
|
34
|
+
return unless table_exists?(:rails_error_dashboard_swallowed_exceptions)
|
|
35
|
+
|
|
36
|
+
if index_exists?(:rails_error_dashboard_swallowed_exceptions, name: "index_swallowed_exceptions_upsert_key")
|
|
37
|
+
remove_index :rails_error_dashboard_swallowed_exceptions, name: "index_swallowed_exceptions_upsert_key"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
change_column :rails_error_dashboard_swallowed_exceptions, :exception_class, :string, null: false
|
|
41
|
+
change_column :rails_error_dashboard_swallowed_exceptions, :raise_location, :string, null: false, limit: 500
|
|
42
|
+
change_column :rails_error_dashboard_swallowed_exceptions, :rescue_location, :string, limit: 500
|
|
43
|
+
|
|
44
|
+
add_index :rails_error_dashboard_swallowed_exceptions,
|
|
45
|
+
[ :exception_class, :raise_location, :rescue_location, :period_hour, :application_id ],
|
|
46
|
+
unique: true,
|
|
47
|
+
name: "index_swallowed_exceptions_upsert_key"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -233,10 +233,10 @@ module RailsErrorDashboard
|
|
|
233
233
|
content = File.read(initializer_path)
|
|
234
234
|
@existing_install_detected = true
|
|
235
235
|
|
|
236
|
-
# Detect separate database from existing config
|
|
237
|
-
if content.match?(
|
|
236
|
+
# Detect separate database from existing config (skip comments)
|
|
237
|
+
if content.match?(/^\s*config\.use_separate_database\s*=\s*true/)
|
|
238
238
|
@database_mode = :separate
|
|
239
|
-
@database_name = content[
|
|
239
|
+
@database_name = content[/^\s*config\.database\s*=\s*:(\w+)/, 1] || "error_dashboard"
|
|
240
240
|
@enable_separate_database = true
|
|
241
241
|
@application_name = detect_application_name
|
|
242
242
|
say_status "detected", "existing separate database configuration", :green
|
|
@@ -52,8 +52,8 @@ module RailsErrorDashboard
|
|
|
52
52
|
|
|
53
53
|
def upsert_raise(class_name, location, period, app_id, count)
|
|
54
54
|
record = SwallowedException.find_or_initialize_by(
|
|
55
|
-
exception_class: truncate(class_name,
|
|
56
|
-
raise_location: truncate(location,
|
|
55
|
+
exception_class: truncate(class_name, 250),
|
|
56
|
+
raise_location: truncate(location, 250),
|
|
57
57
|
rescue_location: nil,
|
|
58
58
|
period_hour: period,
|
|
59
59
|
application_id: app_id
|
|
@@ -70,9 +70,9 @@ module RailsErrorDashboard
|
|
|
70
70
|
|
|
71
71
|
def upsert_rescue(class_name, raise_loc, rescue_loc, period, app_id, count)
|
|
72
72
|
record = SwallowedException.find_or_initialize_by(
|
|
73
|
-
exception_class: truncate(class_name,
|
|
74
|
-
raise_location: truncate(raise_loc,
|
|
75
|
-
rescue_location: rescue_loc.present? ? truncate(rescue_loc,
|
|
73
|
+
exception_class: truncate(class_name, 250),
|
|
74
|
+
raise_location: truncate(raise_loc, 250),
|
|
75
|
+
rescue_location: rescue_loc.present? ? truncate(rescue_loc, 250) : nil,
|
|
76
76
|
period_hour: period,
|
|
77
77
|
application_id: app_id
|
|
78
78
|
)
|
|
@@ -332,6 +332,12 @@ module RailsErrorDashboard
|
|
|
332
332
|
errors = []
|
|
333
333
|
warnings = []
|
|
334
334
|
|
|
335
|
+
# Block boot with default or blank credentials in production
|
|
336
|
+
if default_credentials? &&
|
|
337
|
+
defined?(Rails) && Rails.respond_to?(:env) && Rails.env.production?
|
|
338
|
+
errors << "Default or blank credentials cannot be used in production. Set ERROR_DASHBOARD_USER and ERROR_DASHBOARD_PASSWORD environment variables, or use authenticate_with for custom auth."
|
|
339
|
+
end
|
|
340
|
+
|
|
335
341
|
# Validate sampling_rate (must be between 0.0 and 1.0)
|
|
336
342
|
if sampling_rate && (sampling_rate < 0.0 || sampling_rate > 1.0)
|
|
337
343
|
errors << "sampling_rate must be between 0.0 and 1.0 (got: #{sampling_rate})"
|
|
@@ -529,6 +535,18 @@ module RailsErrorDashboard
|
|
|
529
535
|
true
|
|
530
536
|
end
|
|
531
537
|
|
|
538
|
+
# Check if using default or blank demo credentials with basic auth
|
|
539
|
+
#
|
|
540
|
+
# @return [Boolean] true if basic auth is active with default gandalf/youshallnotpass or blank credentials
|
|
541
|
+
def default_credentials?
|
|
542
|
+
return false unless authenticate_with.nil?
|
|
543
|
+
|
|
544
|
+
default = dashboard_username == "gandalf" && dashboard_password == "youshallnotpass"
|
|
545
|
+
blank = dashboard_username.to_s.strip.empty? || dashboard_password.to_s.strip.empty?
|
|
546
|
+
|
|
547
|
+
default || blank
|
|
548
|
+
end
|
|
549
|
+
|
|
532
550
|
# Get the effective user model (auto-detected if not configured)
|
|
533
551
|
#
|
|
534
552
|
# @return [String, nil] User model class name
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Services
|
|
5
|
+
# Pure algorithm: Format error details as clean Markdown for LLM debugging
|
|
6
|
+
#
|
|
7
|
+
# Reads data already stored in ErrorLog — zero runtime cost.
|
|
8
|
+
# Called at display time only. Sections are conditional — only included
|
|
9
|
+
# when data is present.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# RailsErrorDashboard::Services::MarkdownErrorFormatter.call(error, related_errors: related)
|
|
13
|
+
# # => "# NoMethodError\n\nundefined method 'foo' for nil\n\n## Backtrace\n\n..."
|
|
14
|
+
class MarkdownErrorFormatter
|
|
15
|
+
MAX_BACKTRACE_LINES = 15
|
|
16
|
+
MAX_BREADCRUMBS = 10
|
|
17
|
+
MAX_VARIABLES = 10
|
|
18
|
+
|
|
19
|
+
# @param error [ErrorLog] An error log record
|
|
20
|
+
# @param related_errors [Array] Related error results with :error and :similarity
|
|
21
|
+
# @return [String] Markdown-formatted error details, or "" on failure
|
|
22
|
+
def self.call(error, related_errors: [])
|
|
23
|
+
new(error, related_errors).generate
|
|
24
|
+
rescue => e
|
|
25
|
+
""
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(error, related_errors)
|
|
29
|
+
@error = error
|
|
30
|
+
@related_errors = related_errors
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [String]
|
|
34
|
+
def generate
|
|
35
|
+
sections = []
|
|
36
|
+
|
|
37
|
+
sections << heading_section
|
|
38
|
+
sections << backtrace_section
|
|
39
|
+
sections << cause_chain_section
|
|
40
|
+
sections << local_variables_section
|
|
41
|
+
sections << instance_variables_section
|
|
42
|
+
sections << request_context_section
|
|
43
|
+
sections << breadcrumbs_section
|
|
44
|
+
sections << environment_section
|
|
45
|
+
sections << system_health_section
|
|
46
|
+
sections << related_errors_section
|
|
47
|
+
sections << metadata_section
|
|
48
|
+
|
|
49
|
+
sections.compact.join("\n\n")
|
|
50
|
+
rescue => e
|
|
51
|
+
""
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def heading_section
|
|
57
|
+
"# #{@error.error_type}\n\n#{@error.message}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def backtrace_section
|
|
61
|
+
raw = @error.backtrace
|
|
62
|
+
return nil if raw.blank?
|
|
63
|
+
|
|
64
|
+
lines = raw.split("\n")
|
|
65
|
+
app_lines = lines.reject { |l| l.include?("/gems/") || l.include?("/ruby/") || l.include?("/vendor/") }
|
|
66
|
+
app_lines = lines.first(MAX_BACKTRACE_LINES) if app_lines.empty?
|
|
67
|
+
app_lines = app_lines.first(MAX_BACKTRACE_LINES)
|
|
68
|
+
|
|
69
|
+
"## Backtrace\n\n```\n#{app_lines.join("\n")}\n```"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def cause_chain_section
|
|
73
|
+
raw = @error.exception_cause
|
|
74
|
+
return nil if raw.blank?
|
|
75
|
+
|
|
76
|
+
causes = parse_json(raw)
|
|
77
|
+
return nil unless causes.is_a?(Array) && causes.any?
|
|
78
|
+
|
|
79
|
+
items = causes.each_with_index.map { |cause, i|
|
|
80
|
+
"#{i + 1}. **#{cause["class_name"]}** — #{cause["message"]}"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
"## Exception Cause Chain\n\n#{items.join("\n")}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def local_variables_section
|
|
87
|
+
raw = @error.local_variables
|
|
88
|
+
return nil if raw.blank?
|
|
89
|
+
|
|
90
|
+
vars = parse_json(raw)
|
|
91
|
+
return nil unless vars.is_a?(Hash) && vars.any?
|
|
92
|
+
|
|
93
|
+
rows = vars.first(MAX_VARIABLES).map { |name, info|
|
|
94
|
+
if info.is_a?(Hash)
|
|
95
|
+
"| #{name} | #{info["type"]} | #{truncate_value(info["value"])} |"
|
|
96
|
+
else
|
|
97
|
+
"| #{name} | — | #{truncate_value(info)} |"
|
|
98
|
+
end
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
"## Local Variables\n\n| Variable | Type | Value |\n|----------|------|-------|\n#{rows.join("\n")}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def instance_variables_section
|
|
105
|
+
raw = @error.instance_variables
|
|
106
|
+
return nil if raw.blank?
|
|
107
|
+
|
|
108
|
+
vars = parse_json(raw)
|
|
109
|
+
return nil unless vars.is_a?(Hash) && vars.any?
|
|
110
|
+
|
|
111
|
+
self_class = vars.delete("_self_class")
|
|
112
|
+
return nil if vars.empty? && self_class.nil?
|
|
113
|
+
|
|
114
|
+
lines = []
|
|
115
|
+
lines << "**Class:** #{self_class}" if self_class
|
|
116
|
+
|
|
117
|
+
if vars.any?
|
|
118
|
+
rows = vars.first(MAX_VARIABLES).map { |name, info|
|
|
119
|
+
if info.is_a?(Hash)
|
|
120
|
+
"| #{name} | #{info["type"]} | #{truncate_value(info["value"])} |"
|
|
121
|
+
else
|
|
122
|
+
"| #{name} | — | #{truncate_value(info)} |"
|
|
123
|
+
end
|
|
124
|
+
}
|
|
125
|
+
lines << "| Variable | Type | Value |\n|----------|------|-------|\n#{rows.join("\n")}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
"## Instance Variables\n\n#{lines.join("\n\n")}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def request_context_section
|
|
132
|
+
return nil if @error.request_url.blank?
|
|
133
|
+
|
|
134
|
+
items = []
|
|
135
|
+
items << "- **Method:** #{@error.http_method}" if @error.http_method.present?
|
|
136
|
+
items << "- **URL:** #{@error.request_url}"
|
|
137
|
+
items << "- **Hostname:** #{@error.hostname}" if @error.hostname.present?
|
|
138
|
+
items << "- **Content-Type:** #{@error.content_type}" if @error.content_type.present?
|
|
139
|
+
items << "- **Duration:** #{@error.request_duration_ms}ms" if @error.request_duration_ms.present?
|
|
140
|
+
items << "- **IP:** #{@error.ip_address}" if @error.ip_address.present?
|
|
141
|
+
|
|
142
|
+
"## Request Context\n\n#{items.join("\n")}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def breadcrumbs_section
|
|
146
|
+
raw = @error.breadcrumbs
|
|
147
|
+
return nil if raw.blank?
|
|
148
|
+
|
|
149
|
+
crumbs = parse_json(raw)
|
|
150
|
+
return nil unless crumbs.is_a?(Array) && crumbs.any?
|
|
151
|
+
|
|
152
|
+
# Take last N breadcrumbs (most recent, closest to error)
|
|
153
|
+
crumbs = crumbs.last(MAX_BREADCRUMBS)
|
|
154
|
+
|
|
155
|
+
rows = crumbs.map { |c|
|
|
156
|
+
time = c["t"] ? Time.at(c["t"] / 1000.0).utc.strftime("%H:%M:%S.%L") : "—"
|
|
157
|
+
duration = c["d"] ? "#{c["d"]}ms" : "—"
|
|
158
|
+
"| #{time} | #{c["c"]} | #{truncate_value(c["m"], 80)} | #{duration} |"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
"## Breadcrumbs (last #{crumbs.size})\n\n| Time | Category | Message | Duration |\n|------|----------|---------|----------|\n#{rows.join("\n")}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def environment_section
|
|
165
|
+
raw = @error.environment_info
|
|
166
|
+
return nil if raw.blank?
|
|
167
|
+
|
|
168
|
+
env = parse_json(raw)
|
|
169
|
+
return nil unless env.is_a?(Hash) && env.any?
|
|
170
|
+
|
|
171
|
+
items = []
|
|
172
|
+
items << "- **Ruby:** #{env["ruby_version"]}" if env["ruby_version"]
|
|
173
|
+
items << "- **Rails:** #{env["rails_version"]}" if env["rails_version"]
|
|
174
|
+
items << "- **Env:** #{env["rails_env"]}" if env["rails_env"]
|
|
175
|
+
items << "- **Server:** #{env["server"]}" if env["server"]
|
|
176
|
+
items << "- **DB:** #{env["database_adapter"]}" if env["database_adapter"]
|
|
177
|
+
|
|
178
|
+
version_line = []
|
|
179
|
+
version_line << "- **App Version:** #{@error.app_version}" if @error.app_version.present?
|
|
180
|
+
version_line << "- **Git:** #{@error.git_sha}" if @error.git_sha.present?
|
|
181
|
+
items.concat(version_line)
|
|
182
|
+
|
|
183
|
+
return nil if items.empty?
|
|
184
|
+
|
|
185
|
+
"## Environment\n\n#{items.join("\n")}"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def system_health_section
|
|
189
|
+
raw = @error.system_health
|
|
190
|
+
return nil if raw.blank?
|
|
191
|
+
|
|
192
|
+
health = parse_json(raw)
|
|
193
|
+
return nil unless health.is_a?(Hash) && health.any?
|
|
194
|
+
|
|
195
|
+
items = []
|
|
196
|
+
items << "- **Memory:** #{health["process_memory_mb"]} MB RSS" if health["process_memory_mb"]
|
|
197
|
+
items << "- **Threads:** #{health["thread_count"]}" if health["thread_count"]
|
|
198
|
+
|
|
199
|
+
pool = health["connection_pool"]
|
|
200
|
+
if pool.is_a?(Hash)
|
|
201
|
+
items << "- **DB Pool:** #{pool["busy"]}/#{pool["size"]} busy" if pool["size"]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
gc = health["gc_stats"]
|
|
205
|
+
if gc.is_a?(Hash)
|
|
206
|
+
items << "- **GC:** #{gc["major_gc_count"]} major cycles" if gc["major_gc_count"]
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
return nil if items.empty?
|
|
210
|
+
|
|
211
|
+
"## System Health at Error Time\n\n#{items.join("\n")}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def related_errors_section
|
|
215
|
+
return nil if @related_errors.nil? || @related_errors.empty?
|
|
216
|
+
|
|
217
|
+
items = @related_errors.map { |r|
|
|
218
|
+
pct = (r.similarity * 100).round(1)
|
|
219
|
+
"- `#{r.error.error_type}` — #{r.error.message} (#{pct}% similar, #{r.error.occurrence_count} occurrences)"
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
"## Related Errors\n\n#{items.join("\n")}"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def metadata_section
|
|
226
|
+
items = []
|
|
227
|
+
items << "- **Severity:** #{@error.severity}" if @error.severity.present?
|
|
228
|
+
items << "- **Status:** #{@error.status}" if @error.status.present?
|
|
229
|
+
items << "- **Priority:** P#{3 - @error.priority_level}" if @error.priority_level.present?
|
|
230
|
+
items << "- **Platform:** #{@error.platform}" if @error.platform.present?
|
|
231
|
+
items << "- **First seen:** #{@error.first_seen_at&.utc&.strftime("%Y-%m-%d %H:%M:%S UTC")}" if @error.first_seen_at
|
|
232
|
+
items << "- **Occurrences:** #{@error.occurrence_count}" if @error.occurrence_count
|
|
233
|
+
items << "- **Assigned to:** #{@error.assigned_to}" if @error.assigned_to.present?
|
|
234
|
+
|
|
235
|
+
"## Metadata\n\n#{items.join("\n")}"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def parse_json(raw)
|
|
239
|
+
return nil if raw.blank?
|
|
240
|
+
JSON.parse(raw)
|
|
241
|
+
rescue JSON::ParserError
|
|
242
|
+
nil
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def truncate_value(value, max_length = 200)
|
|
246
|
+
str = value.to_s
|
|
247
|
+
str.length > max_length ? "#{str[0...max_length]}..." : str
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -55,6 +55,7 @@ require "rails_error_dashboard/services/breadcrumb_collector"
|
|
|
55
55
|
require "rails_error_dashboard/services/n_plus_one_detector"
|
|
56
56
|
require "rails_error_dashboard/services/curl_generator"
|
|
57
57
|
require "rails_error_dashboard/services/rspec_generator"
|
|
58
|
+
require "rails_error_dashboard/services/markdown_error_formatter"
|
|
58
59
|
require "rails_error_dashboard/services/database_health_inspector"
|
|
59
60
|
require "rails_error_dashboard/services/cache_analyzer"
|
|
60
61
|
require "rails_error_dashboard/services/variable_serializer"
|
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.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anjan Jagirdar
|
|
@@ -340,6 +340,7 @@ files:
|
|
|
340
340
|
- db/migrate/20260306000003_create_rails_error_dashboard_swallowed_exceptions.rb
|
|
341
341
|
- db/migrate/20260307000001_create_rails_error_dashboard_diagnostic_dumps.rb
|
|
342
342
|
- db/migrate/20260323000001_add_muted_to_error_logs.rb
|
|
343
|
+
- db/migrate/20260325000001_fix_swallowed_exceptions_index_for_mysql.rb
|
|
343
344
|
- lib/generators/rails_error_dashboard/install/install_generator.rb
|
|
344
345
|
- lib/generators/rails_error_dashboard/install/templates/README
|
|
345
346
|
- lib/generators/rails_error_dashboard/install/templates/initializer.rb
|
|
@@ -428,6 +429,7 @@ files:
|
|
|
428
429
|
- lib/rails_error_dashboard/services/git_blame_reader.rb
|
|
429
430
|
- lib/rails_error_dashboard/services/github_link_generator.rb
|
|
430
431
|
- lib/rails_error_dashboard/services/local_variable_capturer.rb
|
|
432
|
+
- lib/rails_error_dashboard/services/markdown_error_formatter.rb
|
|
431
433
|
- lib/rails_error_dashboard/services/n_plus_one_detector.rb
|
|
432
434
|
- lib/rails_error_dashboard/services/notification_helpers.rb
|
|
433
435
|
- lib/rails_error_dashboard/services/notification_throttler.rb
|
|
@@ -465,7 +467,7 @@ metadata:
|
|
|
465
467
|
bug_tracker_uri: https://github.com/AnjanJ/rails_error_dashboard/issues
|
|
466
468
|
funding_uri: https://buymeacoffee.com/anjanj
|
|
467
469
|
post_install_message: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n
|
|
468
|
-
\ Rails Error Dashboard v0.5.
|
|
470
|
+
\ Rails Error Dashboard v0.5.3\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F195
|
|
469
471
|
First time? Quick start:\n rails generate rails_error_dashboard:install\n rails
|
|
470
472
|
db:migrate\n # Add to config/routes.rb:\n mount RailsErrorDashboard::Engine
|
|
471
473
|
=> '/error_dashboard'\n\n\U0001F504 Upgrading from v0.1.x?\n rails db:migrate\n
|