From cce68d18820f9feed1927d8f5f3ee91202d10c98 Mon Sep 17 00:00:00 2001 From: Doug Stull <dstull@gitlab.com> Date: Wed, 25 Aug 2021 08:57:06 +0000 Subject: [PATCH] Experiment: change invite email to be from inviter --- app/controllers/invites_controller.rb | 7 +++- app/controllers/registrations_controller.rb | 1 + app/helpers/notify_helper.rb | 9 ++++- app/mailers/emails/members.rb | 29 ++++++++++++++-- .../experiment/invite_email_from.yml | 8 +++++ locale/gitlab.pot | 3 ++ spec/controllers/invites_controller_spec.rb | 26 +++++++++++++- .../registrations_controller_spec.rb | 34 +++++++++++++++++++ spec/features/invites_spec.rb | 14 ++++++++ spec/helpers/notify_helper_spec.rb | 22 ++++++++++++ spec/mailers/notify_spec.rb | 29 ++++++++++++++++ 11 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 config/feature_flags/experiment/invite_email_from.yml diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 7f5750d20114..4242f918ea0b 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -77,7 +77,12 @@ def ensure_member_exists def track_invite_join_click return unless member && initial_invite_email? - experiment(:invite_email_preview_text, actor: member).track(:join_clicked) if params[:experiment_name] == 'invite_email_preview_text' + if params[:experiment_name] == 'invite_email_preview_text' + experiment(:invite_email_preview_text, actor: member).track(:join_clicked) + elsif params[:experiment_name] == 'invite_email_from' + experiment(:invite_email_from, actor: member).track(:join_clicked) + end + Gitlab::Tracking.event(self.class.name, 'join_clicked', label: 'invite_email', property: member.id.to_s) end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index cc985e845426..3dffbbf3108c 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -201,6 +201,7 @@ def after_pending_invitations_hook experiment_name = session.delete(:invite_email_experiment_name) experiment(:invite_email_preview_text, actor: member).track(:accepted) if experiment_name == 'invite_email_preview_text' + experiment(:invite_email_from, actor: member).track(:accepted) if experiment_name == 'invite_email_from' Gitlab::Tracking.event(self.class.name, 'accepted', label: 'invite_email', property: member.id.to_s) end diff --git a/app/helpers/notify_helper.rb b/app/helpers/notify_helper.rb index da42740f9939..ed96f3cef4fc 100644 --- a/app/helpers/notify_helper.rb +++ b/app/helpers/notify_helper.rb @@ -24,7 +24,14 @@ def invited_to_description(source) def invited_join_url(token, member) additional_params = { invite_type: Emails::Members::INITIAL_INVITE } - if experiment(:invite_email_preview_text, actor: member).enabled? + # order important below to our scheduled testing of these + # `from` experiment will be after the `text` on, but we may not cleanup + # from the `text` one by the time we run the `from` experiment, + # therefore we want to support `text` being fully enabled + # but if `from` is also enabled, then we only care about `from` + if experiment(:invite_email_from, actor: member).enabled? + additional_params[:experiment_name] = 'invite_email_from' + elsif experiment(:invite_email_preview_text, actor: member).enabled? additional_params[:experiment_name] = 'invite_email_preview_text' end diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index fe2d28915477..53b38cc59725 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -57,7 +57,7 @@ def member_invited_email(member_source_type, member_id, token) Gitlab::Tracking.event(self.class.name, 'invite_email_sent', label: 'invite_email', property: member_id.to_s) - mail(to: member.invite_email, subject: invite_email_subject, **invite_email_headers) do |format| + mail(to: member.invite_email, subject: invite_email_subject, **invite_email_headers.merge(additional_invite_settings)) do |format| format.html { render layout: 'unknown_user_mailer' } format.text { render layout: 'unknown_user_mailer' } end @@ -147,7 +147,17 @@ def notification_group def invite_email_subject if member.created_by - subject(s_("MemberInviteEmail|%{member_name} invited you to join GitLab") % { member_name: member.created_by.name }) + experiment(:invite_email_from, actor: member) do |experiment_instance| + experiment_instance.use do + subject(s_("MemberInviteEmail|%{member_name} invited you to join GitLab") % { member_name: member.created_by.name }) + end + + experiment_instance.candidate do + subject(s_("MemberInviteEmail|I've invited you to join me in GitLab")) + end + + experiment_instance.run + end else subject(s_("MemberInviteEmail|Invitation to join the %{project_or_group} %{project_or_group_name}") % { project_or_group: member_source.human_name, project_or_group_name: member_source.model_name.singular }) end @@ -164,6 +174,21 @@ def invite_email_headers end end + def additional_invite_settings + return {} unless member.created_by + + experiment(:invite_email_from, actor: member) do |experiment_instance| + experiment_instance.use { {} } + experiment_instance.candidate do + { + from: "#{member.created_by.name} <#{member.created_by.email}>" + } + end + + experiment_instance.run + end + end + def member_exists? Gitlab::AppLogger.info("Tried to send an email invitation for a deleted group. Member id: #{@member_id}") if member.blank? member.present? diff --git a/config/feature_flags/experiment/invite_email_from.yml b/config/feature_flags/experiment/invite_email_from.yml new file mode 100644 index 000000000000..59baf2493411 --- /dev/null +++ b/config/feature_flags/experiment/invite_email_from.yml @@ -0,0 +1,8 @@ +--- +name: invite_email_from +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68376 +rollout_issue_url: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/429 +milestone: '14.3' +type: experiment +group: group::expansion +default_enabled: false diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 33ee854e7a92..16369188a581 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -20784,6 +20784,9 @@ msgstr "" msgid "MemberInviteEmail|%{member_name} invited you to join GitLab" msgstr "" +msgid "MemberInviteEmail|I've invited you to join me in GitLab" +msgstr "" + msgid "MemberInviteEmail|Invitation to join the %{project_or_group} %{project_or_group_name}" msgstr "" diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb index dc1fb0454dfc..d4091461062c 100644 --- a/spec/controllers/invites_controller_spec.rb +++ b/spec/controllers/invites_controller_spec.rb @@ -120,6 +120,29 @@ end end + context 'when it is part of the invite_email_from experiment' do + let(:extra_params) { { invite_type: 'initial_email', experiment_name: 'invite_email_from' } } + + it 'tracks the initial join click from email' do + experiment = double(track: true) + allow(controller).to receive(:experiment).with(:invite_email_from, actor: member).and_return(experiment) + + request + + expect(experiment).to have_received(:track).with(:join_clicked) + end + + context 'when member does not exist' do + let(:raw_invite_token) { '_bogus_token_' } + + it 'does not track the experiment' do + expect(controller).not_to receive(:experiment).with(:invite_email_from, actor: member) + + request + end + end + end + context 'when member does not exist' do let(:raw_invite_token) { '_bogus_token_' } @@ -147,8 +170,9 @@ end context 'when it is not part of our invite email experiment' do - it 'does not track via experiment' do + it 'does not track via experiment', :aggregate_failures do expect(controller).not_to receive(:experiment).with(:invite_email_preview_text, actor: member) + expect(controller).not_to receive(:experiment).with(:invite_email_from, actor: member) request end diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 301c60e89c8a..a5a0f16f2b17 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -227,6 +227,40 @@ end end end + + context 'with the invite_email_preview_text experiment', :experiment do + let(:extra_session_params) { { invite_email_experiment_name: 'invite_email_from' } } + + context 'when member and invite_email_experiment_name exists from the session key value' do + it 'tracks the invite acceptance' do + expect(experiment(:invite_email_from)).to track(:accepted) + .with_context(actor: member) + .on_next_instance + + subject + end + end + + context 'when member does not exist from the session key value' do + let(:originating_member_id) { -1 } + + it 'does not track invite acceptance' do + expect(experiment(:invite_email_from)).not_to track(:accepted) + + subject + end + end + + context 'when invite_email_experiment_name does not exist from the session key value' do + let(:extra_session_params) { {} } + + it 'does not track invite acceptance' do + expect(experiment(:invite_email_from)).not_to track(:accepted) + + subject + end + end + end end context 'when invite email matches email used on registration' do diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb index d56bedd4852b..583daba37f15 100644 --- a/spec/features/invites_spec.rb +++ b/spec/features/invites_spec.rb @@ -216,6 +216,20 @@ def fill_in_welcome_form end end + context 'with invite email acceptance for the invite_email_from experiment', :experiment do + let(:extra_params) do + { invite_type: Emails::Members::INITIAL_INVITE, experiment_name: 'invite_email_from' } + end + + it 'tracks the accepted invite' do + expect(experiment(:invite_email_from)).to track(:accepted) + .with_context(actor: group_invite) + .on_next_instance + + fill_in_sign_up_form(new_user) + end + end + it 'signs up and redirects to the group activity page with all the project/groups invitation automatically accepted' do fill_in_sign_up_form(new_user) fill_in_welcome_form diff --git a/spec/helpers/notify_helper_spec.rb b/spec/helpers/notify_helper_spec.rb index 633a4b65139f..a4193444528f 100644 --- a/spec/helpers/notify_helper_spec.rb +++ b/spec/helpers/notify_helper_spec.rb @@ -70,6 +70,28 @@ def reference_link(entity, url) expect(helper.invited_join_url(token, member)) .to eq("http://test.host/-/invites/#{token}?experiment_name=invite_email_preview_text&invite_type=initial_email") end + + context 'when invite_email_from is enabled' do + before do + stub_experiments(invite_email_from: :control) + end + + it 'has correct params' do + expect(helper.invited_join_url(token, member)) + .to eq("http://test.host/-/invites/#{token}?experiment_name=invite_email_from&invite_type=initial_email") + end + end + end + + context 'when invite_email_from is enabled' do + before do + stub_experiments(invite_email_from: :control) + end + + it 'has correct params' do + expect(helper.invited_join_url(token, member)) + .to eq("http://test.host/-/invites/#{token}?experiment_name=invite_email_from&invite_type=initial_email") + end end context 'when invite_email_preview_text is disabled' do diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 5d2b136043ec..ecff5c15816b 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -834,6 +834,35 @@ def invite_to_project(project, inviter:, user: nil) invite_type: Emails::Members::INITIAL_INVITE, experiment_name: 'invite_email_preview_text')) end + + it 'tracks the sent invite' do + expect(experiment(:invite_email_preview_text)).to track(:assignment) + .with_context(actor: project_member) + .on_next_instance + + invite_email.deliver_now + end + end + + context 'with invite_email_from enabled', :experiment do + before do + stub_experiments(invite_email_from: :control) + end + + it 'has the correct invite_url with params' do + is_expected.to have_link('Join now', + href: invite_url(project_member.invite_token, + invite_type: Emails::Members::INITIAL_INVITE, + experiment_name: 'invite_email_from')) + end + + it 'tracks the sent invite' do + expect(experiment(:invite_email_from)).to track(:assignment) + .with_context(actor: project_member) + .on_next_instance + + invite_email.deliver_now + end end context 'when invite email sent is tracked', :snowplow do -- GitLab