Skip to content

Commit 9e860a0

Browse files
authored
Merge pull request activerabbit-ai#109 from Zoncovsky/feature/update-pr-description-by-ai
Enhance PR content generation and code fix application
2 parents 905af32 + 83665fd commit 9e860a0

23 files changed

+187
-93
lines changed

app/jobs/quota_alert_job.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ def send_appropriate_alert(account, resource_type, level, last_alert_info)
121121
# First time exceeding
122122
QuotaAlertMailer.quota_exceeded(account, resource_type).deliver_now
123123
Rails.logger.info "[QuotaAlert] Sent exceeded alert for #{account.name} - #{resource_type}"
124+
125+
# Track when first exceeded - initialize hash if needed
126+
account.last_quota_alert_sent_at[resource_type.to_s] ||= {}
127+
account.last_quota_alert_sent_at[resource_type.to_s]["first_exceeded_at"] = Time.current.iso8601
124128
elsif is_free_plan
125129
# Free plan: send upgrade reminder every 2 days
126130
QuotaAlertMailer.free_plan_upgrade_reminder(account, resource_type, days_over_quota).deliver_now

app/services/github/pr_content_generator.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ def build_enhanced_pr_body(issue, sample_event, parsed)
264264
# Header with issue link
265265
lines << "## 🐛 Bug Fix: #{issue.exception_class}"
266266
lines << ""
267-
lines << "**Issue ID:** ##{issue.id}"
267+
lines << "**Issue ID:** [##{issue.id}](#{error_url(issue)})"
268268
lines << "**Controller:** `#{issue.controller_action}`"
269269
lines << "**Occurrences:** #{issue.count} times"
270270
lines << "**First seen:** #{issue.first_seen_at&.strftime('%Y-%m-%d %H:%M')}"
@@ -354,7 +354,7 @@ def build_basic_pr_body(issue, sample_event)
354354

355355
lines << "## 🐛 Bug Fix: #{issue.exception_class}"
356356
lines << ""
357-
lines << "**Issue ID:** ##{issue.id}"
357+
lines << "**Issue ID:** [##{issue.id}](#{error_url(issue)})"
358358
lines << "**Controller:** `#{issue.controller_action}`"
359359
lines << ""
360360

@@ -512,5 +512,10 @@ def validate_method_structure(code)
512512

513513
has_def && has_end && def_count <= end_count
514514
end
515+
516+
def error_url(issue)
517+
host = Rails.env.development? ? "http://localhost:3000" : ENV.fetch("APP_HOST", "https://activerabbit.com")
518+
"#{host}/#{issue.project.slug}/errors/#{issue.id}"
519+
end
515520
end
516521
end

app/services/github/simple_code_fix_applier.rb

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -120,18 +120,18 @@ def try_apply_fix_to_file(owner, repo, file_path, before_code, after_code)
120120
# Try direct replacement if before_code is provided
121121
if before_code.present?
122122
direct_result = try_direct_replacement(current_content, before_code, after_code, 1)
123-
if direct_result && direct_result["replacements"].present?
123+
if direct_result && direct_result[:replacements].present?
124124
Rails.logger.info "[SimpleFixApplier] Direct replacement succeeded for #{normalized_path}"
125125

126126
# Apply the replacements
127127
lines = current_content.lines
128-
direct_result["replacements"].sort_by { |r| -r["line"] }.each do |replacement|
129-
line_idx = replacement["line"] - 1
128+
direct_result[:replacements].sort_by { |r| -r[:line] }.each do |replacement|
129+
line_idx = replacement[:line] - 1
130130
next if line_idx < 0 || line_idx >= lines.size
131131

132132
original_indent = lines[line_idx].match(/^(\s*)/)[1]
133133
line_ending = lines[line_idx].end_with?("\n") ? "\n" : ""
134-
new_stripped = replacement["new"].strip
134+
new_stripped = replacement[:new].strip
135135
lines[line_idx] = "#{original_indent}#{new_stripped}#{line_ending}"
136136
end
137137
new_content = lines.join
@@ -387,11 +387,15 @@ def try_direct_replacement(file_content, before_code, after_code, error_line)
387387
# Preserve original indentation from file
388388
original_indent = lines[match_start + idx]&.match(/^(\s*)/)&.[](1) || ""
389389
new_content = original_indent + new_line.lstrip
390+
old_content = lines[match_start + idx]&.chomp || old_line
391+
392+
# Only include replacement if old != new (skip unchanged lines)
393+
next if old_content.strip == new_content.strip
390394

391395
replacements << {
392-
"line" => line_num,
393-
"old" => lines[match_start + idx]&.chomp || old_line,
394-
"new" => new_content.chomp
396+
line: line_num,
397+
old: old_content,
398+
new: new_content.chomp
395399
}
396400
end
397401

@@ -402,14 +406,16 @@ def try_direct_replacement(file_content, before_code, after_code, error_line)
402406
insert_after = match_start + before_lines.size + idx
403407
original_indent = lines[match_start]&.match(/^(\s*)/)&.[](1) || ""
404408
insertions << {
405-
"after_line" => insert_after,
406-
"content" => original_indent + new_line.lstrip.chomp
409+
after_line: insert_after,
410+
content: original_indent + new_line.lstrip.chomp
407411
}
408412
end
409-
return { "replacements" => replacements, "insertions" => insertions } if insertions.any?
413+
return { replacements: replacements, insertions: insertions } if insertions.any?
410414
end
411415

412-
{ "replacements" => replacements }
416+
return nil if replacements.empty?
417+
418+
{ replacements: replacements }
413419
rescue => e
414420
Rails.logger.warn "[SimpleFixApplier] Direct replacement failed: #{e.message}"
415421
nil
@@ -609,9 +615,9 @@ def apply_line_replacements(lines, fix_instructions)
609615

610616
# First, apply insertions (in reverse order to maintain line numbers)
611617
if fix_instructions[:insertions].present?
612-
fix_instructions[:insertions].sort_by { |i| -i["after_line"] }.each do |insertion|
613-
after_idx = insertion["after_line"] # 1-indexed, insert after this line
614-
content = insertion["content"]
618+
fix_instructions[:insertions].sort_by { |i| -(i[:after_line] || i["after_line"]) }.each do |insertion|
619+
after_idx = insertion[:after_line] || insertion["after_line"] # 1-indexed, insert after this line
620+
content = insertion[:content] || insertion["content"]
615621

616622
next if after_idx < 0 || after_idx > new_lines.size
617623

@@ -643,10 +649,11 @@ def apply_line_replacements(lines, fix_instructions)
643649
end
644650

645651
# Then, apply replacements
646-
(fix_instructions[:replacements] || []).sort_by { |r| -r["line"] }.each do |replacement|
647-
line_idx = replacement["line"] - 1
648-
old_content = replacement["old"]
649-
new_content = replacement["new"]
652+
(fix_instructions[:replacements] || []).sort_by { |r| -(r[:line] || r["line"]) }.each do |replacement|
653+
line_num = replacement[:line] || replacement["line"]
654+
line_idx = line_num - 1
655+
old_content = replacement[:old] || replacement["old"]
656+
new_content = replacement[:new] || replacement["new"]
650657

651658
next if line_idx < 0 || line_idx >= new_lines.size
652659

@@ -668,9 +675,9 @@ def apply_line_replacements(lines, fix_instructions)
668675
end
669676

670677
changes_made += 1
671-
Rails.logger.info "[SimpleFixApplier] Replaced line #{replacement['line']}: #{old_content.strip[0..50]} -> #{new_stripped[0..50]}"
678+
Rails.logger.info "[SimpleFixApplier] Replaced line #{line_num}: #{old_content.strip[0..50]} -> #{new_stripped[0..50]}"
672679
else
673-
Rails.logger.warn "[SimpleFixApplier] Line #{replacement['line']} mismatch:"
680+
Rails.logger.warn "[SimpleFixApplier] Line #{line_num} mismatch:"
674681
Rails.logger.warn " Expected: #{old_content.inspect}"
675682
Rails.logger.warn " Actual: #{current_line.inspect}"
676683

@@ -681,7 +688,7 @@ def apply_line_replacements(lines, fix_instructions)
681688
line_ending = current_line.end_with?("\n") ? "\n" : ""
682689
new_lines[line_idx] = "#{original_indent}#{new_stripped}#{line_ending}"
683690
changes_made += 1
684-
Rails.logger.info "[SimpleFixApplier] Fuzzy match succeeded for line #{replacement['line']}"
691+
Rails.logger.info "[SimpleFixApplier] Fuzzy match succeeded for line #{line_num}"
685692
end
686693
end
687694
end

app/services/github/token_manager.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ def initialize(project_pat:, installation_id:, env_pat:, project_app_id:, projec
1414
end
1515

1616
def get_token
17-
@project_pat.presence || generate_installation_token || @env_pat
17+
# Prefer installation token (GitHub App) for PR authorship, fallback to PAT
18+
generate_installation_token || @project_pat.presence || @env_pat
1819
end
1920

2021
def configured?

app/services/issues/fingerprint_recomputer.rb

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ class FingerprintRecomputer
1515
def initialize(dry_run: true)
1616
@dry_run = dry_run
1717
@stats = { processed: 0, merged: 0, updated: 0, unchanged: 0, errors: 0 }
18+
# Track virtual fingerprint changes for dry run mode
19+
# Key: [project_id, new_fingerprint] => issue_id that "owns" this fingerprint
20+
@virtual_fingerprints = {}
1821
end
1922

2023
def call
@@ -53,9 +56,8 @@ def process_issue(issue)
5356
end
5457

5558
# Check if there's already an issue with the new fingerprint in the same project
56-
existing = Issue.where(project_id: issue.project_id, fingerprint: new_fingerprint)
57-
.where.not(id: issue.id)
58-
.first
59+
# In dry run mode, also check virtual fingerprints
60+
existing = find_existing_issue(issue.project_id, new_fingerprint, issue.id)
5961

6062
if existing
6163
merge_issues(issue, existing)
@@ -64,6 +66,25 @@ def process_issue(issue)
6466
end
6567
end
6668

69+
def find_existing_issue(project_id, new_fingerprint, current_issue_id)
70+
# First check actual database
71+
existing = Issue.where(project_id: project_id, fingerprint: new_fingerprint)
72+
.where.not(id: current_issue_id)
73+
.first
74+
return existing if existing
75+
76+
# In dry run mode, also check virtual fingerprints
77+
if dry_run
78+
virtual_key = [project_id, new_fingerprint]
79+
virtual_owner_id = @virtual_fingerprints[virtual_key]
80+
if virtual_owner_id && virtual_owner_id != current_issue_id
81+
return Issue.find_by(id: virtual_owner_id)
82+
end
83+
end
84+
85+
nil
86+
end
87+
6788
def merge_issues(source_issue, target_issue)
6889
action = dry_run ? "[DRY RUN] Would merge" : "Merging"
6990
puts " #{action} issue ##{source_issue.id} into ##{target_issue.id}"
@@ -98,7 +119,10 @@ def update_fingerprint(issue, new_fingerprint)
98119
puts " Old fingerprint: #{issue.fingerprint[0..16]}..."
99120
puts " New fingerprint: #{new_fingerprint[0..16]}..."
100121

101-
unless dry_run
122+
if dry_run
123+
# Track virtual fingerprint for detecting merges in dry run mode
124+
@virtual_fingerprints[[issue.project_id, new_fingerprint]] = issue.id
125+
else
102126
issue.update_column(:fingerprint, new_fingerprint)
103127
end
104128

config/environments/development.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,5 @@
100100
config.hosts << "127.0.0.1"
101101
config.hosts << "host.docker.internal"
102102
config.hosts << "host.docker.internal:3000"
103+
config.hosts << "160ec4227c78.ngrok-free.app"
103104
end

spec/factories/users.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
provider { "github" }
3333
uid { SecureRandom.hex(10) }
3434
confirmed_at { nil } # OAuth users don't need confirmed_at
35+
36+
after(:build) do |user|
37+
user.skip_confirmation_notification!
38+
end
3539
end
3640
end
3741
end

spec/integration/email_delivery_spec.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
# These tests verify that emails are actually queued for delivery,
77
# not just that the mailer methods return mail objects.
88

9+
before do
10+
# Ensure we use the :test delivery method so emails are captured in deliveries array
11+
ActionMailer::Base.delivery_method = :test
12+
ActionMailer::Base.deliveries.clear
13+
end
14+
915
describe "QuotaAlertJob email delivery" do
1016
let(:account) do
1117
create(:account, :free_plan,

spec/jobs/alert_job_spec.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
"channels" => { "email" => true }
3636
}
3737
})
38+
39+
# Stub Resend API for email delivery
40+
stub_request(:post, "https://api.resend.com/emails")
41+
.to_return(status: 200, body: '{"id": "test-email-id"}', headers: { 'Content-Type' => 'application/json' })
3842
end
3943

4044
describe "#perform" do

spec/jobs/quota_alert_job_spec.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
let(:account) { create(:account, :with_stats) }
1111
let!(:user) { create(:user, :confirmed, account: account) }
1212

13+
before do
14+
# Stub Resend API for email delivery
15+
stub_request(:post, "https://api.resend.com/emails")
16+
.to_return(status: 200, body: '{"id": "test-email-id"}', headers: { 'Content-Type' => 'application/json' })
17+
end
18+
1319
describe "#perform" do
1420
it "checks quotas for all accounts" do
1521
create(:account, :with_stats)

0 commit comments

Comments
 (0)