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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1826105c19a4a203cf377fca2041763849b7b2ff7bb8a9f3a18601e22150f6b
4
- data.tar.gz: fdabbc92770c81339e7499d739e941f4f2a7f3fda381a2a55ec4dcab5bb757cc
3
+ metadata.gz: 85ee33f8363a0a875c4c00bd2242a7d282dba7502cc78cf8af84a4583f376121
4
+ data.tar.gz: 51183380c9642cbabc19582daa191ee7ee4c161cc2c98fbc3dd6693459e91b40
5
5
  SHA512:
6
- metadata.gz: 6c365ae991e67cb59dd9e62deaabe694e06c7e9b64896a75bfa36473f3c735556c95482a929e8ade0328ffab05e10128e5d5e6832c94ead44cfffcf6567c05aa
7
- data.tar.gz: 6b63c5a587bf93ec1599571343c8fcaf8230364266d97564f59217d8308632817557a05fec2398f3e3557ca33d83dbc9968c3f820722e3b1395e046cd58de15a
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 or generate an RSpec test from the captured error context.
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: 500
213
- t.string :rescue_location, limit: 500
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: 500
8
- t.string :rescue_location, limit: 500
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?(/config\.use_separate_database\s*=\s*true/)
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[/config\.database\s*=\s*:(\w+)/, 1] || "error_dashboard"
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, 255),
56
- raise_location: truncate(location, 500),
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, 255),
74
- raise_location: truncate(raise_loc, 500),
75
- rescue_location: rescue_loc.present? ? truncate(rescue_loc, 500) : nil,
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
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.5.2"
2
+ VERSION = "0.5.3"
3
3
  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.2
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.2\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F195
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