Commit 2fa64e76 authored by Alex Buijs's avatar Alex Buijs Committed by Vitaly Slobodin

In Product Email Campaigns Self-Managed

parent 28471cf5
...@@ -35,7 +35,7 @@ module Registrations ...@@ -35,7 +35,7 @@ module Registrations
end end
def update_params def update_params
params.require(:user).permit(:role, :other_role, :setup_for_company) params.require(:user).permit(:role, :other_role, :setup_for_company, :email_opted_in)
end end
def requires_confirmation?(user) def requires_confirmation?(user)
......
...@@ -313,7 +313,8 @@ module InProductMarketingHelper ...@@ -313,7 +313,8 @@ module InProductMarketingHelper
end end
def unsubscribe_link(format) def unsubscribe_link(format)
link(s_('InProductMarketing|unsubscribe'), '%tag_unsubscribe_url%', format) unsubscribe_url = Gitlab.com? ? '%tag_unsubscribe_url%' : profile_notifications_url
link(s_('InProductMarketing|unsubscribe'), unsubscribe_url, format)
end end
def link(text, link, format) def link(text, link, format)
......
...@@ -6,6 +6,8 @@ module Emails ...@@ -6,6 +6,8 @@ module Emails
FROM_ADDRESS = 'GitLab <team@gitlab.com>' FROM_ADDRESS = 'GitLab <team@gitlab.com>'
CUSTOM_HEADERS = { CUSTOM_HEADERS = {
from: FROM_ADDRESS,
reply_to: FROM_ADDRESS,
'X-Mailgun-Track' => 'yes', 'X-Mailgun-Track' => 'yes',
'X-Mailgun-Track-Clicks' => 'yes', 'X-Mailgun-Track-Clicks' => 'yes',
'X-Mailgun-Track-Opens' => 'yes', 'X-Mailgun-Track-Opens' => 'yes',
...@@ -25,7 +27,8 @@ module Emails ...@@ -25,7 +27,8 @@ module Emails
private private
def mail_to(to:, subject:) def mail_to(to:, subject:)
mail(to: to, subject: subject, from: FROM_ADDRESS, reply_to: FROM_ADDRESS, **CUSTOM_HEADERS) do |format| custom_headers = Gitlab.com? ? CUSTOM_HEADERS : {}
mail(to: to, subject: subject, **custom_headers) do |format|
format.html { render layout: nil } format.html { render layout: nil }
format.text { render layout: nil } format.text { render layout: nil }
end end
......
- return unless Gitlab.dev_env_or_com? - is_hidden = local_assigns.fetch(:hidden, Gitlab.dev_env_or_com?)
- is_hidden = local_assigns.fetch(:hidden, false)
.js-email-opt-in{ class: is_hidden ? 'hidden' : '' } .gl-mb-3.js-email-opt-in{ class: is_hidden ? 'hidden' : '' }
.gl-font-weight-bold.gl-mb-3.gl-mt-3 .gl-font-weight-bold.gl-mb-3
= _('Email updates (optional)') = _('Email updates (optional)')
= f.check_box :email_opted_in = f.check_box :email_opted_in
= f.label :email_opted_in, _("I'd like to receive updates about GitLab via email"), class: 'gl-font-weight-normal' = f.label :email_opted_in, _("I'd like to receive updates about GitLab via email"), class: 'gl-font-weight-normal'
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
= f.label :other_role, _('What is your job title? (optional)'), class: 'form-check-label gl-mb-3' = f.label :other_role, _('What is your job title? (optional)'), class: 'form-check-label gl-mb-3'
= f.text_field :other_role, class: 'form-control' = f.text_field :other_role, class: 'form-control'
= render_if_exists "registrations/welcome/setup_for_company", f: f = render_if_exists "registrations/welcome/setup_for_company", f: f
= render 'devise/shared/email_opted_in', f: f
.row .row
.form-group.col-sm-12.gl-mb-0 .form-group.col-sm-12.gl-mb-0
- if partial_exists? "registrations/welcome/button" - if partial_exists? "registrations/welcome/button"
......
...@@ -10,7 +10,7 @@ module Namespaces ...@@ -10,7 +10,7 @@ module Namespaces
def perform def perform
return unless Gitlab::CurrentSettings.in_product_marketing_emails_enabled return unless Gitlab::CurrentSettings.in_product_marketing_emails_enabled
return unless Gitlab::Experimentation.active?(:in_product_marketing_emails) return if Gitlab.com? && !Gitlab::Experimentation.active?(:in_product_marketing_emails)
Namespaces::InProductMarketingEmailsService.send_for_all_tracks_and_intervals Namespaces::InProductMarketingEmailsService.send_for_all_tracks_and_intervals
end end
......
---
title: Send in-product marketing emails to guide users setting up their groups
merge_request: 53715
author:
type: added
...@@ -566,11 +566,11 @@ Settings.cron_jobs['user_status_cleanup_batch_worker']['job_class'] = 'UserStatu ...@@ -566,11 +566,11 @@ Settings.cron_jobs['user_status_cleanup_batch_worker']['job_class'] = 'UserStatu
Settings.cron_jobs['ssh_keys_expired_notification_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['ssh_keys_expired_notification_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['ssh_keys_expired_notification_worker']['cron'] ||= '0 2 * * *' Settings.cron_jobs['ssh_keys_expired_notification_worker']['cron'] ||= '0 2 * * *'
Settings.cron_jobs['ssh_keys_expired_notification_worker']['job_class'] = 'SshKeys::ExpiredNotificationWorker' Settings.cron_jobs['ssh_keys_expired_notification_worker']['job_class'] = 'SshKeys::ExpiredNotificationWorker'
Settings.cron_jobs['namespaces_in_product_marketing_emails_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['cron'] ||= '0 9 * * *'
Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['job_class'] = 'Namespaces::InProductMarketingEmailsWorker'
Gitlab.com do Gitlab.com do
Settings.cron_jobs['namespaces_in_product_marketing_emails_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['cron'] ||= '0 9 * * *'
Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['job_class'] = 'Namespaces::InProductMarketingEmailsWorker'
Settings.cron_jobs['batched_background_migrations_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['batched_background_migrations_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['batched_background_migrations_worker']['cron'] ||= '* * * * *' Settings.cron_jobs['batched_background_migrations_worker']['cron'] ||= '* * * * *'
Settings.cron_jobs['batched_background_migrations_worker']['job_class'] = 'Database::BatchedBackgroundMigrationWorker' Settings.cron_jobs['batched_background_migrations_worker']['job_class'] = 'Database::BatchedBackgroundMigrationWorker'
......
...@@ -27,7 +27,9 @@ module EE ...@@ -27,7 +27,9 @@ module EE
override :update_params override :update_params
def update_params def update_params
clean_params = super.merge(params.require(:user).permit(:email_opted_in)) clean_params = super
return clean_params unless ::Gitlab.dev_env_or_com?
clean_params[:email_opted_in] = '1' if clean_params[:setup_for_company] == 'true' clean_params[:email_opted_in] = '1' if clean_params[:setup_for_company] == 'true'
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
= render 'enforce_terms' = render 'enforce_terms'
= render_if_exists 'devise/shared/email_opted_in', f: f = render 'devise/shared/email_opted_in', f: f
- if current_user - if current_user
%p.text-center= _("You'll be signed out from your current account automatically.") %p.text-center= _("You'll be signed out from your current account automatically.")
......
...@@ -13,4 +13,3 @@ ...@@ -13,4 +13,3 @@
.flex-grow-1 .flex-grow-1
= f.radio_button :setup_for_company, false, class: 'js-setup-for-me' = f.radio_button :setup_for_company, false, class: 'js-setup-for-me'
= f.label :setup_for_company, _('Just me'), class: 'normal', value: 'false' = f.label :setup_for_company, _('Just me'), class: 'normal', value: 'false'
= render_if_exists 'devise/shared/email_opted_in', f: f, hidden: true
...@@ -111,44 +111,80 @@ RSpec.describe Registrations::WelcomeController do ...@@ -111,44 +111,80 @@ RSpec.describe Registrations::WelcomeController do
end end
context 'email updates' do context 'email updates' do
context 'when setup for company is false' do context 'when not on gitlab.com' do
before do
allow(::Gitlab).to receive(:com?).and_return(false)
end
context 'when the user opted in' do context 'when the user opted in' do
let(:email_opted_in) { '1' } let(:email_opted_in) { '1' }
it 'sets the email_opted_in fields' do it 'sets the email_opted_in field' do
subject subject
expect(controller.current_user.email_opted_in).to be_truthy expect(controller.current_user).to be_email_opted_in
expect(controller.current_user.email_opted_in_ip).to be_present
expect(controller.current_user.email_opted_in_source).to eq('GitLab.com')
expect(controller.current_user.email_opted_in_at).not_to be_nil
end end
end end
context 'when user opted out' do context 'when the user opted out' do
let(:email_opted_in) { '0' } it 'sets the email_opted_in field' do
it 'does not set the rest of the email_opted_in fields' do
subject subject
expect(controller.current_user.email_opted_in).to be_falsey expect(controller.current_user).not_to be_email_opted_in
expect(controller.current_user.email_opted_in_ip).to be_blank
expect(controller.current_user.email_opted_in_source).to be_blank
expect(controller.current_user.email_opted_in_at).to be_nil
end end
end end
end end
context 'when setup for company is true' do context 'when on gitlab.com' do
let(:setup_for_company) { 'true' } before do
allow(::Gitlab).to receive(:com?).and_return(true)
end
context 'when setup for company is false' do
context 'when the user opted in' do
let(:email_opted_in) { '1' }
it 'sets the email_opted_in fields' do
subject
expect(controller.current_user).to have_attributes(
email_opted_in: be_truthy,
email_opted_in_ip: be_present,
email_opted_in_source: eq('GitLab.com'),
email_opted_in_at: be_present
)
end
end
context 'when user opted out' do
let(:email_opted_in) { '0' }
it 'does not set the rest of the email_opted_in fields' do
subject
it 'sets email_opted_in fields' do expect(controller.current_user).to have_attributes(
subject email_opted_in: false,
email_opted_in_ip: nil,
email_opted_in_source: "",
email_opted_in_at: nil
)
end
end
end
expect(controller.current_user.email_opted_in).to be_truthy context 'when setup for company is true' do
expect(controller.current_user.email_opted_in_ip).to be_present let(:setup_for_company) { 'true' }
expect(controller.current_user.email_opted_in_source).to eq('GitLab.com')
expect(controller.current_user.email_opted_in_at).not_to be_nil it 'sets email_opted_in fields' do
subject
expect(controller.current_user).to have_attributes(
email_opted_in: be_truthy,
email_opted_in_ip: be_present,
email_opted_in_source: eq('GitLab.com'),
email_opted_in_at: be_present
)
end
end end
end end
end end
......
...@@ -49,6 +49,32 @@ RSpec.describe 'Welcome screen', :js do ...@@ -49,6 +49,32 @@ RSpec.describe 'Welcome screen', :js do
expect(page).not_to have_content('Your profile') expect(page).not_to have_content('Your profile')
end end
end end
context 'email opt in' do
it 'does not show the email opt in checkbox when setting up for a company' do
expect(page).not_to have_selector('input[name="user[email_opted_in]', visible: true)
choose 'user_setup_for_company_true'
expect(page).not_to have_selector('input[name="user[email_opted_in]', visible: true)
click_button 'Continue'
expect(user.reload.email_opted_in).to eq(true)
end
it 'shows the email opt checkbox in when setting up for just me' do
expect(page).not_to have_selector('input[name="user[email_opted_in]', visible: true)
choose 'user_setup_for_company_false'
expect(page).to have_selector('input[name="user[email_opted_in]', visible: true)
click_button 'Continue'
expect(user.reload.email_opted_in).to eq(false)
end
end
end end
context 'when not on GitLab.com' do context 'when not on GitLab.com' do
......
...@@ -5,53 +5,55 @@ require 'spec_helper' ...@@ -5,53 +5,55 @@ require 'spec_helper'
RSpec.describe 'registrations/welcome/show' do RSpec.describe 'registrations/welcome/show' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let_it_be(:user) { User.new } describe 'forms and progress bar' do
let_it_be(:user_other_role_details_enabled) { false } let_it_be(:user) { User.new }
let_it_be(:user_other_role_details_enabled) { false }
before do
allow(view).to receive(:current_user).and_return(user) before do
allow(view).to receive(:redirect_path).and_return(redirect_path) allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:signup_onboarding_enabled?).and_return(signup_onboarding_enabled) allow(view).to receive(:redirect_path).and_return(redirect_path)
allow(Gitlab).to receive(:com?).and_return(true) allow(view).to receive(:signup_onboarding_enabled?).and_return(signup_onboarding_enabled)
stub_feature_flags(user_other_role_details: user_other_role_details_enabled) allow(Gitlab).to receive(:com?).and_return(true)
stub_feature_flags(user_other_role_details: user_other_role_details_enabled)
render
end render
end
subject { rendered } subject { rendered }
where(:redirect_path, :signup_onboarding_enabled, :show_progress_bar, :flow, :is_continue) do where(:redirect_path, :signup_onboarding_enabled, :show_progress_bar, :flow, :is_continue) do
'/-/subscriptions/new' | false | true | :subscription | true '/-/subscriptions/new' | false | true | :subscription | true
'/-/subscriptions/new' | true | true | :subscription | true '/-/subscriptions/new' | true | true | :subscription | true
'/-/trials/new' | false | false | :trial | true '/-/trials/new' | false | false | :trial | true
'/-/trials/new' | true | false | :trial | true '/-/trials/new' | true | false | :trial | true
'/oauth/authorize/abc123' | false | false | nil | false '/oauth/authorize/abc123' | false | false | nil | false
'/oauth/authorize/abc123' | true | false | nil | false '/oauth/authorize/abc123' | true | false | nil | false
nil | false | false | nil | false nil | false | false | nil | false
nil | true | true | nil | true nil | true | true | nil | true
end end
with_them do with_them do
it 'shows the correct text for the :setup_for_company label' do it 'shows the correct text for the :setup_for_company label' do
expected_text = "Who will be using #{flow.nil? ? 'GitLab' : "this GitLab #{flow}"}?" expected_text = "Who will be using #{flow.nil? ? 'GitLab' : "this GitLab #{flow}"}?"
is_expected.to have_selector('label[for="user_setup_for_company"]', text: expected_text) is_expected.to have_selector('label[for="user_setup_for_company"]', text: expected_text)
end end
it 'shows the correct text for the submit button' do it 'shows the correct text for the submit button' do
expected_text = is_continue ? 'Continue' : 'Get started!' expected_text = is_continue ? 'Continue' : 'Get started!'
is_expected.to have_button(expected_text) is_expected.to have_button(expected_text)
end end
it { is_expected_to_have_progress_bar(status: show_progress_bar) } it { is_expected_to_have_progress_bar(status: show_progress_bar) }
context 'feature flag other_role_details is enabled' do context 'feature flag other_role_details is enabled' do
let_it_be(:user_other_role_details_enabled) { true } let_it_be(:user_other_role_details_enabled) { true }
it 'has a text field for other role' do it 'has a text field for other role' do
is_expected.not_to have_selector('input[type="hidden"][name="user[other_role]"]', visible: false) is_expected.not_to have_selector('input[type="hidden"][name="user[other_role]"]', visible: false)
is_expected.to have_selector('input[type="text"][name="user[other_role]"]') is_expected.to have_selector('input[type="text"][name="user[other_role]"]')
end
end end
end end
end end
......
...@@ -60,8 +60,10 @@ RSpec.describe Registrations::WelcomeController do ...@@ -60,8 +60,10 @@ RSpec.describe Registrations::WelcomeController do
end end
describe '#update' do describe '#update' do
let(:email_opted_in) { '0' }
subject(:update) do subject(:update) do
patch :update, params: { user: { role: 'software_developer', setup_for_company: 'false' } } patch :update, params: { user: { role: 'software_developer', setup_for_company: 'false', email_opted_in: email_opted_in } }
end end
context 'without a signed in user' do context 'without a signed in user' do
...@@ -74,6 +76,24 @@ RSpec.describe Registrations::WelcomeController do ...@@ -74,6 +76,24 @@ RSpec.describe Registrations::WelcomeController do
end end
it { is_expected.to redirect_to(dashboard_projects_path)} it { is_expected.to redirect_to(dashboard_projects_path)}
context 'when the user opted in' do
let(:email_opted_in) { '1' }
it 'sets the email_opted_in field' do
subject
expect(controller.current_user.email_opted_in).to eq(true)
end
end
context 'when the user opted out' do
it 'sets the email_opted_in field' do
subject
expect(controller.current_user.email_opted_in).to eq(false)
end
end
end end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Welcome screen' do
let(:user) { create(:user) }
before do
gitlab_sign_in(user)
visit users_sign_up_welcome_path
end
it 'shows the email opt in' do
select 'Software Developer', from: 'user_role'
check 'user_email_opted_in'
click_button 'Get started!'
expect(user.reload.email_opted_in).to eq(true)
end
end
...@@ -13,6 +13,38 @@ RSpec.describe Emails::InProductMarketing do ...@@ -13,6 +13,38 @@ RSpec.describe Emails::InProductMarketing do
describe '#in_product_marketing_email' do describe '#in_product_marketing_email' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let(:track) { :create }
let(:series) { 0 }
subject { Notify.in_product_marketing_email(user.id, group.id, track, series) }
include_context 'gitlab email notification'
it 'sends to the right user with a link to unsubscribe' do
aggregate_failures do
expect(subject).to deliver_to(user.notification_email)
expect(subject).to have_body_text(profile_notifications_url)
end
end
context 'when on gitlab.com' do
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
it 'has custom headers' do
aggregate_failures do
expect(subject).to deliver_from(described_class::FROM_ADDRESS)
expect(subject).to reply_to(described_class::FROM_ADDRESS)
expect(subject).to have_header('X-Mailgun-Track', 'yes')
expect(subject).to have_header('X-Mailgun-Track-Clicks', 'yes')
expect(subject).to have_header('X-Mailgun-Track-Opens', 'yes')
expect(subject).to have_header('X-Mailgun-Tag', 'marketing')
expect(subject).to have_body_text('%tag_unsubscribe_url%')
end
end
end
where(:track, :series) do where(:track, :series) do
:create | 0 :create | 0
:create | 1 :create | 1
...@@ -29,8 +61,6 @@ RSpec.describe Emails::InProductMarketing do ...@@ -29,8 +61,6 @@ RSpec.describe Emails::InProductMarketing do
end end
with_them do with_them do
subject { Notify.in_product_marketing_email(user.id, group.id, track, series) }
it 'has the correct subject and content' do it 'has the correct subject and content' do
aggregate_failures do aggregate_failures do
is_expected.to have_subject(subject_line(track, series)) is_expected.to have_subject(subject_line(track, series))
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'registrations/welcome/show' do RSpec.describe 'registrations/welcome/show' do
using RSpec::Parameterized::TableSyntax let(:is_gitlab_com) { false }
let_it_be(:user) { User.new } let_it_be(:user) { User.new }
...@@ -13,7 +13,7 @@ RSpec.describe 'registrations/welcome/show' do ...@@ -13,7 +13,7 @@ RSpec.describe 'registrations/welcome/show' do
allow(view).to receive(:in_trial_flow?).and_return(false) allow(view).to receive(:in_trial_flow?).and_return(false)
allow(view).to receive(:user_has_memberships?).and_return(false) allow(view).to receive(:user_has_memberships?).and_return(false)
allow(view).to receive(:in_oauth_flow?).and_return(false) allow(view).to receive(:in_oauth_flow?).and_return(false)
allow(Gitlab).to receive(:com?).and_return(false) allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
render render
end end
...@@ -22,4 +22,24 @@ RSpec.describe 'registrations/welcome/show' do ...@@ -22,4 +22,24 @@ RSpec.describe 'registrations/welcome/show' do
it { is_expected.not_to have_selector('label[for="user_setup_for_company"]') } it { is_expected.not_to have_selector('label[for="user_setup_for_company"]') }
it { is_expected.to have_button('Get started!') } it { is_expected.to have_button('Get started!') }
it { is_expected.to have_selector('input[name="user[email_opted_in]"]') }
describe 'email opt in' do
context 'when on gitlab.com' do
let(:is_gitlab_com) { true }
it 'hides the email-opt in by default' do
expect(subject).to have_css('.js-email-opt-in.hidden')
end
end
context 'when not on gitlab.com' do
let(:is_gitlab_com) { false }
it 'hides the email-opt in by default' do
expect(subject).not_to have_css('.js-email-opt-in.hidden')
expect(subject).to have_css('.js-email-opt-in')
end
end
end
end end
...@@ -3,45 +3,49 @@ ...@@ -3,45 +3,49 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Namespaces::InProductMarketingEmailsWorker, '#perform' do RSpec.describe Namespaces::InProductMarketingEmailsWorker, '#perform' do
context 'when the application setting is enabled' do using RSpec::Parameterized::TableSyntax
RSpec.shared_examples 'in-product marketing email' do
before do before do
stub_application_setting(in_product_marketing_emails_enabled: true) stub_application_setting(in_product_marketing_emails_enabled: in_product_marketing_emails_enabled)
stub_experiment(in_product_marketing_emails: experiment_active)
allow(::Gitlab).to receive(:com?).and_return(is_gitlab_com)
end end
context 'when the experiment is inactive' do it 'executes the email service service' do
before do expect(Namespaces::InProductMarketingEmailsService).to receive(:send_for_all_tracks_and_intervals).exactly(executes_service).times
stub_experiment(in_product_marketing_emails: false)
end
it 'does not execute the in product marketing emails service' do
expect(Namespaces::InProductMarketingEmailsService).not_to receive(:send_for_all_tracks_and_intervals)
subject.perform subject.perform
end
end end
end
context 'when the experiment is active' do context 'not on gitlab.com' do
before do let(:is_gitlab_com) { false }
stub_experiment(in_product_marketing_emails: true)
end
it 'calls the send_for_all_tracks_and_intervals method on the in product marketing emails service' do where(:in_product_marketing_emails_enabled, :experiment_active, :executes_service) do
expect(Namespaces::InProductMarketingEmailsService).to receive(:send_for_all_tracks_and_intervals) true | true | 1
true | false | 1
false | false | 0
false | true | 0
end
subject.perform with_them do
end include_examples 'in-product marketing email'
end end
end end
context 'when the application setting is disabled' do context 'on gitlab.com' do
before do let(:is_gitlab_com) { true }
stub_application_setting(in_product_marketing_emails_enabled: false)
end
it 'does not execute the in product marketing emails service' do where(:in_product_marketing_emails_enabled, :experiment_active, :executes_service) do
expect(Namespaces::InProductMarketingEmailsService).not_to receive(:send_for_all_tracks_and_intervals) true | true | 1
true | false | 0
false | false | 0
false | true | 0
end
subject.perform with_them do
include_examples 'in-product marketing email'
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment