Commit 21b9c2fc authored by Alex Buijs's avatar Alex Buijs

Add in product marketing emails service

For sending in product marketing emails
parent 449d67ac
# frozen_string_literal: true
module Namespaces
class InProductMarketingEmailsService
include Gitlab::Experimentation::GroupTypes
INTERVAL_DAYS = [1, 5, 10].freeze
TRACKS = {
create: :git_write,
verify: :pipeline_created,
trial: :trial_started,
team: :user_added
}.freeze
def self.send_for_all_tracks_and_intervals
TRACKS.each_key do |track|
INTERVAL_DAYS.each do |interval|
new(track, interval).execute
end
end
end
def initialize(track, interval)
@track = track
@interval = interval
@sent_email_user_ids = []
end
def execute
groups_for_track.each_batch do |groups|
groups.each do |group|
send_email_for_group(group)
end
end
end
private
attr_reader :track, :interval, :sent_email_user_ids
def send_email_for_group(group)
experiment_enabled_for_group = experiment_enabled_for_group?(group)
experiment_add_group(group, experiment_enabled_for_group)
return unless experiment_enabled_for_group
users_for_group(group).each do |user|
send_email(user, group) if can_perform_action?(user, group)
end
end
def experiment_enabled_for_group?(group)
Gitlab::Experimentation.in_experiment_group?(:in_product_marketing_emails, subject: group)
end
def experiment_add_group(group, experiment_enabled_for_group)
variant = experiment_enabled_for_group ? GROUP_EXPERIMENTAL : GROUP_CONTROL
Experiment.add_group(:in_product_marketing_emails, variant: variant, group: group)
end
# rubocop: disable CodeReuse/ActiveRecord
def groups_for_track
onboarding_progress_scope = OnboardingProgress
.completed_actions_with_latest_in_range(completed_actions, range)
.incomplete_actions(incomplete_action)
Group.joins(:onboarding_progress).merge(onboarding_progress_scope)
end
def users_for_group(group)
group.users.where(email_opted_in: true)
.where.not(id: sent_email_user_ids)
end
# rubocop: enable CodeReuse/ActiveRecord
def can_perform_action?(user, group)
case track
when :create
user.can?(:create_projects, group)
when :verify
user.can?(:create_projects, group)
when :trial
user.can?(:start_trial, group)
when :team
user.can?(:admin_group_member, group)
else
raise NotImplementedError, "No ability defined for track #{track}"
end
end
def send_email(user, group)
NotificationService.new.in_product_marketing(user.id, group.id, track, series)
sent_email_user_ids << user.id
end
def completed_actions
index = TRACKS.keys.index(track)
index == 0 ? [:created] : TRACKS.values[0..index - 1]
end
def range
(interval + 1).days.ago.beginning_of_day..(interval + 1).days.ago.end_of_day
end
def incomplete_action
TRACKS[track]
end
def series
INTERVAL_DAYS.index(interval)
end
end
end
......@@ -664,6 +664,10 @@ class NotificationService
end
end
def in_product_marketing(user_id, group_id, track, series)
mailer.in_product_marketing_email(user_id, group_id, track, series).deliver_later
end
protected
def new_resource_email(target, method)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Namespaces::InProductMarketingEmailsService, '#execute' do
subject(:execute_service) { described_class.new(track, interval).execute }
let(:track) { :create }
let(:interval) { 1 }
let(:previous_action_completed_at) { 2.days.ago.middle_of_day }
let(:current_action_completed_at) { nil }
let(:experiment_enabled) { true }
let(:user_can_perform_current_track_action) { true }
let(:actions_completed) { { created_at: previous_action_completed_at, git_write_at: current_action_completed_at } }
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user, email_opted_in: true) }
before do
create(:onboarding_progress, namespace: group, **actions_completed)
group.add_developer(user)
stub_experiment_for_subject(in_product_marketing_emails: experiment_enabled)
allow(Ability).to receive(:allowed?).with(user, anything, anything).and_return(user_can_perform_current_track_action)
allow(Notify).to receive(:in_product_marketing_email).and_return(double(deliver_later: nil))
end
RSpec::Matchers.define :send_in_product_marketing_email do |*args|
match do
expect(Notify).to have_received(:in_product_marketing_email).with(*args).once
end
match_when_negated do
expect(Notify).not_to have_received(:in_product_marketing_email)
end
end
context 'for each track and series with the right conditions' do
using RSpec::Parameterized::TableSyntax
where(:track, :interval, :actions_completed) do
:create | 1 | { created_at: 2.days.ago.middle_of_day }
:create | 5 | { created_at: 6.days.ago.middle_of_day }
:create | 10 | { created_at: 11.days.ago.middle_of_day }
:verify | 1 | { created_at: 2.days.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day }
:verify | 5 | { created_at: 6.days.ago.middle_of_day, git_write_at: 6.days.ago.middle_of_day }
:verify | 10 | { created_at: 11.days.ago.middle_of_day, git_write_at: 11.days.ago.middle_of_day }
:trial | 1 | { created_at: 2.days.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day, pipeline_created_at: 2.days.ago.middle_of_day }
:trial | 5 | { created_at: 6.days.ago.middle_of_day, git_write_at: 6.days.ago.middle_of_day, pipeline_created_at: 6.days.ago.middle_of_day }
:trial | 10 | { created_at: 11.days.ago.middle_of_day, git_write_at: 11.days.ago.middle_of_day, pipeline_created_at: 11.days.ago.middle_of_day }
:team | 1 | { created_at: 2.days.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day, pipeline_created_at: 2.days.ago.middle_of_day, trial_started_at: 2.days.ago.middle_of_day }
:team | 5 | { created_at: 6.days.ago.middle_of_day, git_write_at: 6.days.ago.middle_of_day, pipeline_created_at: 6.days.ago.middle_of_day, trial_started_at: 6.days.ago.middle_of_day }
:team | 10 | { created_at: 11.days.ago.middle_of_day, git_write_at: 11.days.ago.middle_of_day, pipeline_created_at: 11.days.ago.middle_of_day, trial_started_at: 11.days.ago.middle_of_day }
end
with_them do
it { is_expected.to send_in_product_marketing_email(user.id, group.id, track, described_class::INTERVAL_DAYS.index(interval)) }
end
end
context 'when initialized with a different track' do
let(:track) { :verify }
it { is_expected.not_to send_in_product_marketing_email }
context 'when the previous track actions have been completed' do
let(:current_action_completed_at) { 2.days.ago.middle_of_day }
it { is_expected.to send_in_product_marketing_email(user.id, group.id, :verify, 0) }
end
end
context 'when initialized with a different interval' do
let(:interval) { 5 }
it { is_expected.not_to send_in_product_marketing_email }
context 'when the previous track action was completed within the intervals range' do
let(:previous_action_completed_at) { 6.days.ago.middle_of_day }
it { is_expected.to send_in_product_marketing_email(user.id, group.id, :create, 1) }
end
end
describe 'experimentation' do
context 'when the experiment is enabled' do
it 'adds the group as an experiment subject in the experimental group' do
expect(Experiment).to receive(:add_group)
.with(:in_product_marketing_emails, variant: :experimental, group: group)
execute_service
end
end
context 'when the experiment is disabled' do
let(:experiment_enabled) { false }
it 'adds the group as an experiment subject in the control group' do
expect(Experiment).to receive(:add_group)
.with(:in_product_marketing_emails, variant: :control, group: group)
execute_service
end
it { is_expected.not_to send_in_product_marketing_email }
end
end
context 'when the previous track action is not yet completed' do
let(:previous_action_completed_at) { nil }
it { is_expected.not_to send_in_product_marketing_email }
end
context 'when the previous track action is completed outside the intervals range' do
let(:previous_action_completed_at) { 3.days.ago }
it { is_expected.not_to send_in_product_marketing_email }
end
context 'when the current track action is completed' do
let(:current_action_completed_at) { Time.current }
it { is_expected.not_to send_in_product_marketing_email }
end
context "when the user cannot perform the current track's action" do
let(:user_can_perform_current_track_action) { false }
it { is_expected.not_to send_in_product_marketing_email }
end
context 'when the user has not opted into marketing emails' do
let(:user) { create(:user, email_opted_in: false) }
it { is_expected.not_to send_in_product_marketing_email }
end
context 'when the user has already received a marketing email as part of another group' do
before do
other_group = create(:group)
other_group.add_developer(user)
create(:onboarding_progress, namespace: other_group, created_at: previous_action_completed_at, git_write_at: current_action_completed_at)
end
# For any group Notify is called exactly once
it { is_expected.to send_in_product_marketing_email(user.id, anything, :create, 0) }
end
context 'when invoked with a non existing track' do
let(:track) { :foo }
before do
stub_const("#{described_class}::TRACKS", { foo: :git_write })
end
it { expect { subject }.to raise_error(NotImplementedError, 'No ability defined for track foo') }
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