Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions app/views/layouts/solid_errors/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,39 @@
document.querySelectorAll('[data-controller="fade"]').forEach(element => {
fadeOut(element);
});

function copyClaudeAnalysis(event) {
const button = event.currentTarget;
const text = button.getAttribute('data-claude-text');

// Create a temporary textarea to copy the text
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);

// Select and copy the text
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);

// Update button text to show success
const buttonText = document.getElementById('copy-button-text');
const originalText = buttonText.textContent;
buttonText.textContent = 'Copied!';

// Update button colors for visual feedback
button.classList.remove('text-purple-600', 'border-purple-600', 'hover:ring-purple-200');
button.classList.add('text-green-600', 'border-green-600', 'hover:ring-green-200');

// Reset button text after 2 seconds
setTimeout(() => {
buttonText.textContent = originalText;
button.classList.remove('text-green-600', 'border-green-600', 'hover:ring-green-200');
button.classList.add('text-purple-600', 'border-purple-600', 'hover:ring-purple-200');
}, 2000);
}
</script>
</body>
</html>
16 changes: 16 additions & 0 deletions app/views/solid_errors/errors/_actions.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@
</svg>
<span>Back to errors</span>
<% end %>

<% llm_text = capture do %>
<%= render 'solid_errors/errors/llm_text', error: error, occurrences: occurrences %>
<% end %>
<% # Strip HTML comments that Rails adds in development mode %>
<% clean_llm_text = llm_text.gsub(/<!--.*?-->/m, '').strip %>
<button
onclick="copyClaudeAnalysis(event)"
class="inline-flex items-center justify-center gap-2 font-medium cursor-pointer border rounded-lg py-3 px-5 bg-transparent text-purple-600 border-purple-600 hover:ring-purple-200 hover:ring-8"
data-claude-text="<%= clean_llm_text %>">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2Zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H6ZM2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1H2Z"/>
</svg>
<span id="copy-button-text">Copy for LLM</span>
</button>

<% if error.resolved? %>
<%= render 'solid_errors/errors/delete_button', error: error %>
<% else %>
Expand Down
24 changes: 15 additions & 9 deletions app/views/solid_errors/errors/_error.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,13 @@
</dt>
<dd class="inline-flex items-center gap-1">
<% first_seen_at = error.occurrences.minimum(:created_at) %>

<abbr title="<%= first_seen_at.iso8601 %>" class="cursor-help">
<%= time_tag first_seen_at, "#{time_ago_in_words(first_seen_at)} ago" %>
</abbr>
<% if first_seen_at %>
<abbr title="<%= first_seen_at.iso8601 %>" class="cursor-help">
<%= time_tag first_seen_at, "#{time_ago_in_words(first_seen_at)} ago" %>
</abbr>
<% else %>
<span>N/A</span>
<% end %>
</dd>
</div>
<div class="flex items-center justify-between flex-wrap gap-x-2">
Expand All @@ -57,10 +60,13 @@
</dt>
<dd class="inline-flex items-center gap-1">
<% last_seen_at = error.occurrences.maximum(:created_at) %>

<abbr title="<%= last_seen_at.iso8601 %>" class="cursor-help">
<%= time_tag last_seen_at, "#{time_ago_in_words(last_seen_at)} ago" %>
</abbr>
<% if last_seen_at %>
<abbr title="<%= last_seen_at.iso8601 %>" class="cursor-help">
<%= time_tag last_seen_at, "#{time_ago_in_words(last_seen_at)} ago" %>
</abbr>
<% else %>
<span>N/A</span>
<% end %>
</dd>
</div>
<div class="flex items-center justify-between flex-wrap gap-x-2">
Expand Down Expand Up @@ -101,5 +107,5 @@
</div>
</dl>

<%= render "solid_errors/errors/actions", error: error if show_actions %>
<%= render "solid_errors/errors/actions", error: error, occurrences: @occurrences if show_actions %>
<% end %>
77 changes: 77 additions & 0 deletions app/views/solid_errors/errors/_llm_text.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
Error Analysis Request:

EXCEPTION: <%= error.exception_class %>
MESSAGE: <%= error.message %>
SEVERITY: <%= error.severity %>
SOURCE: <%= error.source %>
STATUS: <%= error.resolved? ? 'Resolved' : 'Unresolved' %>

OCCURRENCES: <%= error.occurrences.count %> total
First seen: <%= error.occurrences.minimum(:created_at)&.iso8601 || 'N/A' %>
Last seen: <%= error.occurrences.maximum(:created_at)&.iso8601 || 'N/A' %>

<% if occurrences&.any? %>
MOST RECENT OCCURRENCE:
<% most_recent = occurrences.first %>
Timestamp: <%= most_recent.created_at.iso8601 %>
<% if most_recent.context.present? %>
Context:
<% most_recent.context.each do |key, value| %>
<%= key %>: <%= value %>
<% end %>
<% end %>

BACKTRACE:
<%
lines = most_recent.parsed_backtrace.lines
filtered_lines = []

lines.each_with_index do |line, i|
if line.application?
# Add the previous line if it exists and is from GEM_ROOT
if i > 0 && !lines[i-1].application?
filtered_lines << lines[i-1] unless filtered_lines.last == lines[i-1]
end

# Add the current application line
filtered_lines << line

# Add the next line if it exists and is from GEM_ROOT
if i < lines.length - 1 && !lines[i+1].application?
filtered_lines << lines[i+1]
end
elsif i == 0
# Always include the first line if it's not an application line
filtered_lines << line
end
end

# If no application lines, show first few lines
filtered_lines = lines.take(5) if filtered_lines.empty?

prev_was_gem = false
collapsed_count = 0

filtered_lines.each_with_index do |line, i|
# Check if we should show a collapsed indicator
if i > 0 && !line.application? && !filtered_lines[i-1].application?
# Count how many GEM_ROOT lines were skipped
original_index = lines.index(line)
prev_original_index = lines.index(filtered_lines[i-1])
if original_index && prev_original_index
skipped = original_index - prev_original_index - 1
if skipped > 0
%> ... (<%= skipped %> GEM_ROOT frames omitted) ...
<%
end
end
end
%><%= lines.index(line) + 1 %>. <%= line.filtered_file || line.unparsed_line %><% if line.filtered_number %>:<%= line.filtered_number %><% end %><% if line.filtered_method %> in `<%= line.filtered_method %>`<% end %>
<% end %>
<% end %>

Please analyze this error and suggest:
1. Root cause of the error
2. Potential fixes
3. Any patterns or anti-patterns you notice
4. Recommendations for preventing similar errors
163 changes: 163 additions & 0 deletions test/controllers/solid_errors/errors_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# frozen_string_literal: true

require "test_helper"

module SolidErrors
class ErrorsControllerTest < ActionDispatch::IntegrationTest
setup do
# Create a test error with occurrences
@error = SolidErrors::Error.create!(
exception_class: "StandardError",
message: "Test error message for Claude analysis",
severity: "error",
source: "application",
fingerprint: Digest::SHA256.hexdigest("test-error")
)

# Create a more realistic backtrace with mix of GEM_ROOT and PROJECT_ROOT
rails_root = Rails.root.to_s
@occurrence = SolidErrors::Occurrence.create!(
error: @error,
backtrace: [
"#{Gem.path.first}/gems/actionpack-7.0.0/lib/action_dispatch.rb:10:in `call'",
"#{Gem.path.first}/gems/rack-2.0.0/lib/rack.rb:20:in `call'",
"#{Gem.path.first}/gems/rails-7.0.0/lib/rails.rb:30:in `call'",
"#{rails_root}/app/controllers/test_controller.rb:10:in `index'",
"#{rails_root}/app/models/test_model.rb:25:in `process'",
"#{Gem.path.first}/gems/activerecord-7.0.0/lib/active_record.rb:100:in `execute'",
"#{Gem.path.first}/gems/activerecord-7.0.0/lib/active_record.rb:110:in `query'",
"#{Gem.path.first}/gems/activerecord-7.0.0/lib/active_record.rb:120:in `select_all'",
"#{rails_root}/app/services/data_processor.rb:45:in `fetch_data'",
"#{Gem.path.first}/gems/sidekiq-6.0.0/lib/sidekiq.rb:200:in `perform'"
].join("\n"),
context: {
request_url: "http://example.com/test",
user_id: 123,
params: { id: 1 }.to_json
}
)
end

teardown do
SolidErrors::Occurrence.destroy_all
SolidErrors::Error.destroy_all
end

test "should show error with Claude analysis section" do
get "/solid_errors/#{@error.id}"
assert_response :success

# Check that the LLM copy button is present with icon
assert_select "button[onclick='copyClaudeAnalysis(event)']" do
assert_select "svg" # Check for copy icon
assert_select "span", text: "Copy for LLM"
end
end

test "Claude analysis section contains error details" do
get "/solid_errors/#{@error.id}"
assert_response :success

# Check that the button with error details in data attribute exists
assert_select "button[data-claude-text]" do |elements|
button_content = elements.first['data-claude-text']

# Check for key error information
assert_match(/EXCEPTION: StandardError/, button_content)
assert_match(/MESSAGE: Test error message for Claude analysis/, button_content)
assert_match(/SEVERITY: error/, button_content)
assert_match(/SOURCE: application/, button_content)
assert_match(/STATUS: Unresolved/, button_content)

# Check for occurrence information
assert_match(/OCCURRENCES: 1 total/, button_content)
assert_match(/MOST RECENT OCCURRENCE:/, button_content)

# Check for context
assert_match(/Context:/, button_content)
assert_match(/request_url:/, button_content)
assert_match(/user_id:/, button_content)

# Check for backtrace
assert_match(/BACKTRACE:/, button_content)
assert_match(/test_controller\.rb/, button_content)
assert_match(/test_model\.rb/, button_content)
assert_match(/data_processor\.rb/, button_content)

# Check for analysis prompt
assert_match(/Please analyze this error and suggest:/, button_content)
assert_match(/1\. Root cause of the error/, button_content)
assert_match(/2\. Potential fixes/, button_content)
assert_match(/3\. Any patterns or anti-patterns you notice/, button_content)
assert_match(/4\. Recommendations for preventing similar errors/, button_content)
end
end

test "Claude analysis section handles errors without occurrences" do
# Create an error without occurrences
error_without_occurrences = SolidErrors::Error.create!(
exception_class: "NoMethodError",
message: "undefined method `foo' for nil:NilClass",
severity: "error",
source: "application",
fingerprint: Digest::SHA256.hexdigest("test-error-no-occurrences")
)

get "/solid_errors/#{error_without_occurrences.id}"
assert_response :success

# Should still have the Claude analysis button
assert_select "button[onclick='copyClaudeAnalysis(event)']"

# Check that it handles missing occurrence data gracefully
assert_select "button[data-claude-text]" do |elements|
button_content = elements.first['data-claude-text']

assert_match(/EXCEPTION: NoMethodError/, button_content)
assert_match(/OCCURRENCES: 0 total/, button_content)
# Should not have MOST RECENT OCCURRENCE section when there are no occurrences
assert_no_match(/MOST RECENT OCCURRENCE:/, button_content)
end
end

test "Claude analysis button has onclick handler" do
get "/solid_errors/#{@error.id}"
assert_response :success

# Check that the button has the onclick handler for copying
assert_select "button[onclick='copyClaudeAnalysis(event)']"

# Check that the JavaScript function is defined
assert_match(/function copyClaudeAnalysis/, response.body)
end

test "Claude analysis section filters backtrace intelligently" do
get "/solid_errors/#{@error.id}"
assert_response :success

# Check that the button contains filtered backtrace
assert_select "button[data-claude-text]" do |elements|
button_content = elements.first['data-claude-text']

# Should include PROJECT_ROOT frames
assert_match(/\[PROJECT_ROOT\]\/app\/controllers\/test_controller\.rb/, button_content)
assert_match(/\[PROJECT_ROOT\]\/app\/models\/test_model\.rb/, button_content)
assert_match(/\[PROJECT_ROOT\]\/app\/services\/data_processor\.rb/, button_content)

# Should include some GEM_ROOT frames (context around PROJECT_ROOT)
assert_match(/\[GEM_ROOT\]/, button_content)

# Should show omission indicator when multiple GEM_ROOT frames are collapsed
assert_match(/\.\.\. \(\d+ GEM_ROOT frames omitted\) \.\.\./, button_content) ||
assert_match(/BACKTRACE:/, button_content) # Allow for cases where no omission occurs

# Should NOT include all the intermediate GEM_ROOT frames
backtrace_section = button_content.split("BACKTRACE:").last.split("Please analyze").first
gem_root_count = backtrace_section.scan(/\[GEM_ROOT\]/).count

# We expect fewer GEM_ROOT entries than the original (which had 7)
assert gem_root_count < 7, "Expected filtered backtrace to have fewer GEM_ROOT entries, but found #{gem_root_count}"
end
end
end
end
Loading