From 3c0d3b58c6e1478bc9bc02e61846804cccec3a2d Mon Sep 17 00:00:00 2001 From: Patrick Bajao <ebajao@gitlab.com> Date: Mon, 24 Jul 2023 08:14:17 +0000 Subject: [PATCH] Include review summary in new review email When a review is created we send an email to notify users and we want to include the review summary generated by AI. To do that, we delay the sending of the `new_review_email` by 2 minutes as we make a request to AI to generate the review summary asynchronously. If after 2 minutes the review summary is still not available for a specific review, then the new review email will be sent without it. We are still generating review summary behind `automatically_summarize_mr_review` feature flag. --- app/mailers/emails/reviews.rb | 15 +++++----- app/mailers/previews/notify_preview.rb | 7 +++++ app/models/review.rb | 2 ++ app/services/notification_service.rb | 10 ++++++- app/views/notify/new_review_email.html.haml | 1 + app/views/notify/new_review_email.text.erb | 2 ++ ee/app/models/ee/review.rb | 11 +++++++ ee/app/services/ee/notification_service.rb | 12 ++++++++ ee/app/views/notify/_review_summary.html.haml | 7 +++++ ee/app/views/notify/_review_summary.text.erb | 8 +++++ ee/spec/mailers/notify_spec.rb | 11 +++++++ ee/spec/models/ee/review_spec.rb | 9 ++++++ .../services/ee/notification_service_spec.rb | 29 +++++++++++++++++++ locale/gitlab.pot | 3 ++ spec/mailers/previews_spec.rb | 1 + 15 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 ee/app/models/ee/review.rb create mode 100644 ee/app/views/notify/_review_summary.html.haml create mode 100644 ee/app/views/notify/_review_summary.text.erb create mode 100644 ee/spec/models/ee/review_spec.rb diff --git a/app/mailers/emails/reviews.rb b/app/mailers/emails/reviews.rb index b98fa8aa6c9fa..ed1166509a58b 100644 --- a/app/mailers/emails/reviews.rb +++ b/app/mailers/emails/reviews.rb @@ -19,17 +19,16 @@ def review_thread_options(recipient_id) end def setup_review_email(review_id, recipient_id) - review = Review.find_by_id(review_id) - - @notes = review.notes - @discussions = Discussion.build_discussions(review.discussion_ids, preload_note_diff_file: true) + @review = Review.find_by_id(review_id) + @notes = @review.notes + @discussions = Discussion.build_discussions(@review.discussion_ids, preload_note_diff_file: true) @include_diff_discussion_stylesheet = @discussions.values.any? do |discussion| discussion.diff_discussion? && discussion.on_text? end - @author = review.author - @author_name = review.author_name - @project = review.project - @merge_request = review.merge_request + @author = @review.author + @author_name = @review.author_name + @project = @review.project + @merge_request = @review.merge_request @target_url = project_merge_request_url(@project, @merge_request) @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key) end diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index 576dbdd8b52a7..93d4625c34421 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -284,6 +284,13 @@ def request_review_merge_request_email Notify.request_review_merge_request_email(user.id, merge_request.id, user.id).message end + def new_review_email + review = Review.last + mr_author = review.merge_request.author + + Notify.new_review_email(mr_author.id, review.id).message + end + def project_was_moved_email Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab").message end diff --git a/app/models/review.rb b/app/models/review.rb index c621da3b03c54..d47aaf027ce7c 100644 --- a/app/models/review.rb +++ b/app/models/review.rb @@ -32,3 +32,5 @@ def user_mentions merge_request.user_mentions.where.not(note_id: nil) end end + +Review.prepend_mod diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index b93b44ce79705..d305c8c03cfd4 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -723,9 +723,12 @@ def group_was_not_exported(group, current_user, errors) # Notify users on new review in system def new_review(review) recipients = NotificationRecipients::BuildService.build_new_review_recipients(review) + deliver_options = new_review_deliver_options(review) recipients.each do |recipient| - mailer.new_review_email(recipient.user.id, review.id).deliver_later + mailer + .new_review_email(recipient.user.id, review.id) + .deliver_later(deliver_options) end end @@ -946,6 +949,11 @@ def deliver_access_request_email(recipient, member) def warn_skipping_notifications(user, object) Gitlab::AppLogger.warn(message: "Skipping sending notifications", user: user.id, klass: object.class.to_s, object_id: object.id) end + + def new_review_deliver_options(review) + # Overridden in EE + {} + end end NotificationService.prepend_mod_with('NotificationService') diff --git a/app/views/notify/new_review_email.html.haml b/app/views/notify/new_review_email.html.haml index afc1bd682156b..8a184aa969675 100644 --- a/app/views/notify/new_review_email.html.haml +++ b/app/views/notify/new_review_email.html.haml @@ -22,3 +22,4 @@ - discussion.first_note.project = @project if discussion&.first_note - target_url = project_merge_request_url(@project, @merge_request, anchor: "note_#{note.id}") = render 'note_email', note: note, diff_limit: 3, target_url: target_url, note_style: "border-bottom:1px solid #ededed; padding-bottom: 1em;", include_stylesheet_link: false, discussion: discussion, author: @author + = render_if_exists 'notify/review_summary' diff --git a/app/views/notify/new_review_email.text.erb b/app/views/notify/new_review_email.text.erb index 69cb33b05dfdc..e974c8b6be871 100644 --- a/app/views/notify/new_review_email.text.erb +++ b/app/views/notify/new_review_email.text.erb @@ -12,3 +12,5 @@ -- <% end %> <% end %> + +<%= render_if_exists 'notify/review_summary' %> diff --git a/ee/app/models/ee/review.rb b/ee/app/models/ee/review.rb new file mode 100644 index 0000000000000..fa48aa0c9cc51 --- /dev/null +++ b/ee/app/models/ee/review.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module EE + module Review + extend ActiveSupport::Concern + + prepended do + has_one :merge_request_review_llm_summary, class_name: 'MergeRequest::ReviewLlmSummary' + end + end +end diff --git a/ee/app/services/ee/notification_service.rb b/ee/app/services/ee/notification_service.rb index 6ad77f1b66200..9b6e910e4195a 100644 --- a/ee/app/services/ee/notification_service.rb +++ b/ee/app/services/ee/notification_service.rb @@ -182,5 +182,17 @@ def send_account_validation_email(pipeline) email = user.notification_email_or_default mailer.account_validation_email(pipeline, email).deliver_later end + + override :new_review_deliver_options + def new_review_deliver_options(review) + options = super + + # We delay delivery by 2 minutes since we generate review summary asynchronously. + # We expect that in 2 minutes, we already have the summary generated so we + # can include it in the email. + options[:wait] = 2.minutes if Ability.allowed?(review.author, :summarize_submitted_review, review.merge_request) + + options + end end end diff --git a/ee/app/views/notify/_review_summary.html.haml b/ee/app/views/notify/_review_summary.html.haml new file mode 100644 index 0000000000000..ac068f1333a87 --- /dev/null +++ b/ee/app/views/notify/_review_summary.html.haml @@ -0,0 +1,7 @@ +- review_llm_summary = @review.merge_request_review_llm_summary +- if review_llm_summary.present? + .md{ style: 'border-bottom:1px solid #ededed; padding-bottom: 1em; padding-top: 1em;' } + %h4= _('Summary') + = markdown(review_llm_summary.content, pipeline: :email, author: review_llm_summary.reviewer, current_user: @recipient, issuable_reference_expansion_enabled: true) + %p{ style: 'color: #737278;' } + = _('Summary generated by AI (Experiment)') diff --git a/ee/app/views/notify/_review_summary.text.erb b/ee/app/views/notify/_review_summary.text.erb new file mode 100644 index 0000000000000..57fff56e3cb9e --- /dev/null +++ b/ee/app/views/notify/_review_summary.text.erb @@ -0,0 +1,8 @@ +<% review_llm_summary = @review.merge_request_review_llm_summary %> +<% if review_llm_summary.present? %> +-- + +<%= review_llm_summary.content %> + +<%= _('Summary generated by AI (Experiment)') %> +<% end %> diff --git a/ee/spec/mailers/notify_spec.rb b/ee/spec/mailers/notify_spec.rb index 01e42dbd30969..0d04c92a8178c 100644 --- a/ee/spec/mailers/notify_spec.rb +++ b/ee/spec/mailers/notify_spec.rb @@ -402,6 +402,17 @@ end end + describe 'merge request reviews' do + let!(:review) { create(:review, project: project, merge_request: merge_request) } + let!(:review_summary) { create(:merge_request_review_llm_summary, review: review) } + + subject { described_class.new_review_email(recipient.id, review.id) } + + it 'includes review summary' do + expect(subject.text_part.body.raw_source).to include(review_summary.content) + end + end + def expect_sender(user) sender = subject.header[:from].addrs[0] expect(sender.display_name).to eq("#{user.name} (@#{user.username})") diff --git a/ee/spec/models/ee/review_spec.rb b/ee/spec/models/ee/review_spec.rb new file mode 100644 index 0000000000000..fce8a253108ad --- /dev/null +++ b/ee/spec/models/ee/review_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Review, feature_category: :code_review_workflow do + describe 'associations' do + it { is_expected.to have_one(:merge_request_review_llm_summary).class_name('MergeRequest::ReviewLlmSummary') } + end +end diff --git a/ee/spec/services/ee/notification_service_spec.rb b/ee/spec/services/ee/notification_service_spec.rb index b34e77ab9d460..28959adc4fb0c 100644 --- a/ee/spec/services/ee/notification_service_spec.rb +++ b/ee/spec/services/ee/notification_service_spec.rb @@ -59,6 +59,35 @@ subject.new_review(review) end + context 'when review author is allowed to summarize_submitted_review' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability) + .to receive(:allowed?) + .with(review.author, :summarize_submitted_review, review.merge_request) + .and_return(true) + end + + it 'delays the sending of new_review_email by 2 minutes' do + new_review_email = double + + expect(Notify).not_to receive(:new_review_email).with(review.author.id, review.id) + expect(Notify).not_to receive(:new_review_email).with(@unsubscriber.id, review.id) + merge_request.assignee_ids.each do |assignee_id| + expect(Notify).to receive(:new_review_email).with(assignee_id, review.id).and_return(new_review_email) + end + expect(Notify).to receive(:new_review_email).with(merge_request.author.id, review.id).and_return(new_review_email) + expect(Notify).to receive(:new_review_email).with(@u_watcher.id, review.id).and_return(new_review_email) + expect(Notify).to receive(:new_review_email).with(@u_mentioned.id, review.id).and_return(new_review_email) + expect(Notify).to receive(:new_review_email).with(@subscriber.id, review.id).and_return(new_review_email) + expect(Notify).to receive(:new_review_email).with(@watcher_and_subscriber.id, review.id).and_return(new_review_email) + expect(Notify).to receive(:new_review_email).with(@subscribed_participant.id, review.id).and_return(new_review_email) + expect(new_review_email).to receive(:deliver_later).with({ wait: 2.minutes }).exactly(8).times + + subject.new_review(review) + end + end + it_behaves_like 'project emails are disabled' do let(:notification_target) { review } let(:notification_trigger) { subject.new_review(review) } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1ba69edc3bf11..d39561ec7a69c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -45170,6 +45170,9 @@ msgstr "" msgid "Summary generated by AI" msgstr "" +msgid "Summary generated by AI (Experiment)" +msgstr "" + msgid "Summary will be generated with the next push to this merge request and will appear here." msgstr "" diff --git a/spec/mailers/previews_spec.rb b/spec/mailers/previews_spec.rb index 14bd56e5d40f2..e1af0d7ef7731 100644 --- a/spec/mailers/previews_spec.rb +++ b/spec/mailers/previews_spec.rb @@ -13,6 +13,7 @@ let_it_be(:issue) { create(:issue, project: project, milestone: milestone) } let_it_be(:remote_mirror) { create(:remote_mirror, project: project) } let_it_be(:member) { create(:project_member, :maintainer, project: project, created_by: user) } + let_it_be(:review) { create(:review, project: project, merge_request: merge_request, author: user) } Gitlab.ee do let_it_be(:epic) { create(:epic, group: group) } -- GitLab