Skip to content
Merged
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
113 changes: 77 additions & 36 deletions app/controllers/performance_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -719,27 +719,31 @@ def sql_fingerprint
end

def create_pr
project_scope = @current_project || @project
project_scope = performance_project_scope
target = params[:target]

unless project_scope
redirect_to performance_path, alert: "Select a project to open a PR."
return
end

pr_service = Github::PrService.new(project_scope)
perf_issue_attrs = performance_pr_pseudo_issue_attributes(project_scope, target)
# Build a pseudo-issue for performance with minimal attributes used by service body
issue_like = OpenStruct.new(
id: "perf-#{target.gsub(/[^a-zA-Z0-9_-]/, '-')}",
exception_class: "Performance: #{target}",
ai_summary: "Auto-detected performance regression for #{target}."
{
id: "perf-#{target.gsub(/[^a-zA-Z0-9_-]/, '-')}",
exception_class: "Performance: #{target}",
controller_action: target,
project: project_scope,
# PrService / PrContentGenerator expect an AR-like `events` relation (see #order chain).
events: project_scope.events.none
}.merge(perf_issue_attrs)
)

result = pr_service.create_pr_for_issue(issue_like)

redirect_path =
if @current_project
performance_action_detail_path(target: target)
elsif @project
project_performance_action_detail_path(@project, target: target)
else
performance_action_detail_path(target: target)
end
redirect_path = performance_action_detail_redirect_path(target)

if result[:success]
# Persist PR URL for this performance target to show a direct link next time
Expand All @@ -759,19 +763,18 @@ def create_pr
end

def reopen_pr
project_scope = @current_project || @project
project_scope = performance_project_scope
target = params[:target]

pr_url = project_scope&.settings&.dig("perf_pr_urls", target.to_s)

redirect_path = if @current_project
performance_action_detail_path(target: target)
elsif @project
project_performance_action_detail_path(@project, target: target)
else
performance_action_detail_path(target: target)
unless project_scope
redirect_to performance_path, alert: "Select a project to reopen a PR."
return
end

pr_url = project_scope.settings&.dig("perf_pr_urls", target.to_s)

redirect_path = performance_action_detail_redirect_path(target)

unless pr_url.present?
redirect_to redirect_path, alert: "No existing PR found for this action"
return
Expand All @@ -788,7 +791,12 @@ def reopen_pr
end

def create_n_plus_one_pr
project_scope = @current_project || @project
project_scope = performance_project_scope
unless project_scope
redirect_to performance_path, alert: "Select a project to create a PR."
return
end

@sql_fingerprint = project_scope.sql_fingerprints.find(params[:id])

# This is a stub for GitHub integration
Expand All @@ -798,26 +806,59 @@ def create_n_plus_one_pr
result = pr_service.create_n_plus_one_fix_pr(@sql_fingerprint)

if result[:success]
redirect_path =
if @current_project
"/#{@current_project.slug}/performance/sql_fingerprints/#{@sql_fingerprint.id}"
else
project_performance_sql_fingerprint_path(@project, @sql_fingerprint)
end
redirect_to redirect_path, notice: "PR created: #{result[:pr_url]}"
redirect_to sql_fingerprint_redirect_path(project_scope), notice: "PR created: #{result[:pr_url]}"
else
redirect_path =
if @current_project
"/#{@current_project.slug}/performance/sql_fingerprints/#{@sql_fingerprint.id}"
else
project_performance_sql_fingerprint_path(@project, @sql_fingerprint)
end
redirect_to redirect_path, alert: "Failed to create PR: #{result[:error]}"
redirect_to sql_fingerprint_redirect_path(project_scope), alert: "Failed to create PR: #{result[:error]}"
end
end

private

def performance_project_scope
@current_project || @project || selected_project_for_menu
end

def performance_action_detail_redirect_path(target)
if @current_project
project_slug_performance_action_detail_path(@current_project.slug, target: target)
elsif @project
project_performance_action_detail_path(@project, target: target)
elsif (p = selected_project_for_menu)
project_slug_performance_action_detail_path(p.slug, target: target)
else
performance_action_detail_path(target: target)
end
end

# Metrics + cached AI summary so GitHub PR bodies match what the Performance "AI" tab generated.
def performance_pr_pseudo_issue_attributes(project_scope, target)
summary_text = PerformanceSummary.find_by(project: project_scope, target: target)&.summary
window_start = 30.days.ago
pe = PerformanceEvent.where(project: project_scope, target: target).where("occurred_at > ?", window_start)
count = pe.count
avg_ms = pe.average(:duration_ms)
sample_lines = ["Performance target: #{target}"]
sample_lines << "Requests (last 30 days): #{count}" if count.positive?
sample_lines << "Avg duration: #{avg_ms.round(1)} ms" if avg_ms
{
ai_summary: summary_text.presence || "Auto-detected performance regression for #{target}.",
count: count,
first_seen_at: pe.minimum(:occurred_at),
last_seen_at: pe.maximum(:occurred_at),
sample_message: sample_lines.join("\n")
}
end

def sql_fingerprint_redirect_path(project_scope)
if @current_project
"/#{@current_project.slug}/performance/sql_fingerprints/#{@sql_fingerprint.id}"
elsif @project
project_performance_sql_fingerprint_path(@project, @sql_fingerprint)
else
"/#{project_scope.slug}/performance/sql_fingerprints/#{@sql_fingerprint.id}"
end
end

def set_project
@project = current_account.projects.find(params[:project_id])
end
Expand Down
54 changes: 41 additions & 13 deletions app/services/github/pr_content_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def initialize(anthropic_key: nil)
end

def generate(issue)
sample_event = issue.events.order(occurred_at: :desc).first
sample_event = issue.events&.order(occurred_at: :desc)&.first

# If we have existing AI summary, parse it for the fix section
if issue.ai_summary.present?
Expand Down Expand Up @@ -51,12 +51,13 @@ def generate(issue)
def generate_pr_title(issue, root_cause)
# Create a concise, descriptive title based on the root cause
if root_cause.present?
# Extract first sentence of root cause for title
short_cause = root_cause.split(/[.\n]/).first.to_s.strip
if short_cause.length > 60
short_cause = short_cause[0, 57] + "..."
end
"fix: #{short_cause}"
# First non-empty line reads better than split-on-period (decimals like 46.15% break titles).
short_cause = root_cause.lines.map(&:strip).find(&:present?).to_s
short_cause = short_cause.gsub(/\*\*/, "").sub(/\A(?:[-*]+\s*|\d+\.\s*)/, "").strip
short_cause = short_cause[0, 72].strip + "..." if short_cause.length > 72
"fix: #{short_cause.presence || 'performance regression'}"
elsif issue.exception_class.to_s.start_with?("Performance:") && issue.controller_action.present?
"fix: performance — #{issue.controller_action}"
else
"fix: #{issue.exception_class} in #{issue.controller_action.to_s.split('#').last}"
end
Expand All @@ -70,8 +71,17 @@ def parse_ai_summary(summary)
sections = summary.split(/^##\s+/m)

sections.each do |section|
if section.start_with?("Root Cause")
result[:root_cause] = section.sub(/^Root Cause\s*\n/, "").strip
if section.match?(/\ARoot Cause/i)
# Strip first heading line (e.g. "Root Cause Analysis (RCA)")
result[:root_cause] = section.sub(/\A[^\n]+\n+/, "").strip
elsif section.match?(/\AOptimization Steps/i)
# Performance summaries from AiPerformanceSummaryService use this section for fixes.
fix_content = section.sub(/\A[^\n]+\n+/, "").strip
result[:fix] = fix_content
Rails.logger.info "[GitHub API] Optimization Steps section (first 500 chars): #{fix_content[0..500]}"
parsed_fix = parse_single_file_fix(fix_content)
result[:fix_code] = parsed_fix[:fix_code]
result[:before_code] = parsed_fix[:before_code]
elsif section.start_with?("Suggested Fix") || section.start_with?("Fix")
fix_content = section.sub(/^(?:Suggested )?Fix\s*\n/, "").strip
result[:fix] = fix_content
Expand Down Expand Up @@ -266,9 +276,12 @@ def build_enhanced_pr_body(issue, sample_event, parsed)
lines << ""
lines << "**Issue ID:** [##{issue.id}](#{error_url(issue)})"
lines << "**Controller:** `#{issue.controller_action}`"
lines << "**Occurrences:** #{issue.count} times"
lines << "**First seen:** #{issue.first_seen_at&.strftime('%Y-%m-%d %H:%M')}"
lines << "**Last seen:** #{issue.last_seen_at&.strftime('%Y-%m-%d %H:%M')}"
occ = issue.respond_to?(:count) ? issue.count : nil
lines << "**Occurrences:** #{occ.nil? ? '—' : "#{occ} times"}"
first_seen = issue.respond_to?(:first_seen_at) ? issue.first_seen_at : nil
last_seen = issue.respond_to?(:last_seen_at) ? issue.last_seen_at : nil
lines << "**First seen:** #{first_seen ? first_seen.strftime('%Y-%m-%d %H:%M') : '—'}"
lines << "**Last seen:** #{last_seen ? last_seen.strftime('%Y-%m-%d %H:%M') : '—'}"
lines << ""

# Root Cause Analysis
Expand Down Expand Up @@ -516,7 +529,22 @@ def validate_method_structure(code)
def error_url(issue)
host = Rails.env.development? ? "http://localhost:3000" : ENV.fetch("APP_HOST", "https://activerabbit.com")
host = "https://#{host}" unless host.start_with?("http://", "https://")
"#{host}/#{issue.project.slug}/errors/#{issue.id}"
project = issue.respond_to?(:project) ? issue.project : nil
slug = project&.slug
return host if slug.blank?

# Performance "Open PR" uses a pseudo-issue (id perf-...) — link to action detail, not errors.
if issue.id.to_s.start_with?("perf-")
target = issue.respond_to?(:controller_action) ? issue.controller_action : nil
if target.present?
segment = target.to_s.gsub(" ", "%20").gsub("#", "%23")
"#{host}/#{slug}/performance/actions/#{segment}"
else
"#{host}/#{slug}/performance"
end
else
"#{host}/#{slug}/errors/#{issue.id}"
end
end
end
end
2 changes: 1 addition & 1 deletion app/services/github/pr_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ def create_fix_commit(api_client, code_fix_applier, owner, repo, branch, base_sh
commit_msg_parts = []
unapplied_fixes = []

sample_event = issue.events.order(occurred_at: :desc).first
sample_event = issue.events&.order(occurred_at: :desc)&.first
actual_fix_applied = false
files_fixed = []

Expand Down
8 changes: 6 additions & 2 deletions app/views/performance/action_detail.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@
AI summary
<% end %>
<% create_pr_path = if @current_project
performance_create_pr_path(target: @target)
project_slug_performance_action_create_pr_path(@current_project.slug, target: @target)
elsif @project
project_performance_action_create_pr_path(@project, target: @target)
elsif selected_project_for_menu
project_performance_action_create_pr_path(selected_project_for_menu, target: @target)
else
performance_create_pr_path(target: @target)
end %>
Expand All @@ -72,9 +74,11 @@
View PR
<% end %>
<% reopen_pr_path = if @current_project
performance_reopen_pr_path(target: @target)
project_slug_performance_action_reopen_pr_path(@current_project.slug, target: @target)
elsif @project
project_performance_action_reopen_pr_path(@project, target: @target)
elsif selected_project_for_menu
project_performance_action_reopen_pr_path(selected_project_for_menu, target: @target)
else
performance_reopen_pr_path(target: @target)
end %>
Expand Down
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@
get ":project_slug/performance", to: "performance#index", as: "project_slug_performance"
get ":project_slug/performance/:id", to: "performance#show", as: "project_slug_performance_issue"
get ":project_slug/performance/actions/:target", to: "performance#action_detail", as: "project_slug_performance_action_detail", constraints: { target: /[^\/]+/ }, format: false
post ":project_slug/performance/actions/:target/create_pr", to: "performance#create_pr", as: "project_slug_performance_action_create_pr", constraints: { target: /[^\/]+/ }, format: false
post ":project_slug/performance/actions/:target/reopen_pr", to: "performance#reopen_pr", as: "project_slug_performance_action_reopen_pr", constraints: { target: /[^\/]+/ }, format: false
get ":project_slug/deploys", to: "deploys#index", as: "project_slug_deploys"
get ":project_slug/uptime", to: "uptime/monitors#index", as: "project_slug_uptime"
get ":project_slug/uptime/:id", to: "uptime/monitors#show", as: "project_slug_uptime_monitor"
Expand Down
Loading