diff --git a/app/models/solid_errors/error.rb b/app/models/solid_errors/error.rb index 3c84203..0369553 100644 --- a/app/models/solid_errors/error.rb +++ b/app/models/solid_errors/error.rb @@ -53,5 +53,40 @@ def status_badge_classes def resolved? resolved_at.present? end + + def to_markdown + md = "" + md << "## Exception: #{exception_class}\n" + md << "**Severity:** #{severity} | **Source:** #{source}\n\n" + md << "### Message\n#{message}\n" + + occurrence = occurrences.order(created_at: :desc).first + return md unless occurrence + + backtrace = occurrence.parsed_backtrace + + md << "\n### Backtrace\n" + backtrace.lines.each do |line| + md << (line.filtered_method ? line.to_s : line.unparsed_line) << "\n" + end + + app_lines = backtrace.application_lines + if app_lines.any? + md << "\n### Source Context\n" + app_lines.each do |line| + md << "#### #{line}\n" + md << "```ruby\n" + line.source.each { |n, code| md << "#{n}: #{code.chomp}\n" } + md << "```\n\n" + end + end + + if occurrence.context.present? + md << "### Request Context\n" + occurrence.context.each { |k, v| md << "- #{k}: #{v}\n" } + end + + md + end end end diff --git a/app/views/layouts/solid_errors/application.html.erb b/app/views/layouts/solid_errors/application.html.erb index 1c353bc..b87e38f 100644 --- a/app/views/layouts/solid_errors/application.html.erb +++ b/app/views/layouts/solid_errors/application.html.erb @@ -51,6 +51,21 @@ document.querySelectorAll('[data-controller="fade"]').forEach(element => { fadeOut(element); }); + + var copyBtn = document.getElementById('copy-markdown'); + if (copyBtn) { + copyBtn.addEventListener('click', function() { + var text = document.getElementById('error-markdown').content.textContent; + var span = copyBtn.querySelector('span'); + var originalText = span.textContent; + navigator.clipboard.writeText(text).then(function() { + span.textContent = 'Copied!'; + setTimeout(function() { span.textContent = originalText; }, 2000); + }).catch(function(err) { + alert('Failed to copy: ' + err); + }); + }); + } diff --git a/app/views/solid_errors/errors/_actions.html.erb b/app/views/solid_errors/errors/_actions.html.erb index da60125..e3156c2 100644 --- a/app/views/solid_errors/errors/_actions.html.erb +++ b/app/views/solid_errors/errors/_actions.html.erb @@ -5,6 +5,15 @@ Back to errors <% end %> + + <% if error.resolved? %> <%= render 'solid_errors/errors/delete_button', error: error %> <% else %> diff --git a/test/models/solid_errors/error_to_markdown_test.rb b/test/models/solid_errors/error_to_markdown_test.rb new file mode 100644 index 0000000..8db5049 --- /dev/null +++ b/test/models/solid_errors/error_to_markdown_test.rb @@ -0,0 +1,61 @@ +require "test_helper" + +class SolidErrors::ErrorToMarkdownTest < ActiveSupport::TestCase + setup do + begin + raise StandardError, "Something went wrong" + rescue => e + Rails.error.report(e) + end + @error = SolidErrors::Error.last + end + + test "includes exception class and message" do + result = @error.to_markdown + + assert_includes result, "## Exception: StandardError" + assert_includes result, "### Message\nSomething went wrong" + assert_includes result, "**Severity:**" + assert_includes result, "**Source:**" + end + + test "includes backtrace when occurrence exists" do + result = @error.to_markdown + + assert_includes result, "### Backtrace" + end + + test "includes source context for application lines" do + occurrence = @error.occurrences.order(created_at: :desc).first + backtrace = occurrence.parsed_backtrace + + skip "No application lines in test backtrace" unless backtrace.application_lines.any? + + result = @error.to_markdown + assert_includes result, "### Source Context" + assert_includes result, "```ruby" + end + + test "includes request context when present" do + occurrence = @error.occurrences.order(created_at: :desc).first + occurrence.update!(context: {"controller" => "UsersController", "action" => "show"}) + + result = @error.to_markdown + + assert_includes result, "### Request Context" + assert_includes result, "- controller: UsersController" + assert_includes result, "- action: show" + end + + test "handles error with no occurrences" do + @error.occurrences.destroy_all + + result = @error.to_markdown + + assert_includes result, "## Exception: StandardError" + assert_includes result, "### Message\nSomething went wrong" + refute_includes result, "### Backtrace" + refute_includes result, "### Source Context" + refute_includes result, "### Request Context" + end +end