Commit 95e42fe5 authored by Sean McGivern's avatar Sean McGivern

Merge branch '296133-engineering-in-product-email-campaigns-in-saas' into 'master'

In Product Email Campaigns SaaS

See merge request gitlab-org/gitlab!50679
parents 0d4d8821 83f6888d
# frozen_string_literal: true
module InProductMarketingHelper
def subject_line(track, series)
{
create: [
s_('InProductMarketing|Create a project in GitLab in 5 minutes'),
s_('InProductMarketing|Import your project and code from GitHub, Bitbucket and others'),
s_('InProductMarketing|Understand repository mirroring')
],
verify: [
s_('InProductMarketing|Feel the need for speed?'),
s_('InProductMarketing|3 ways to dive into GitLab CI/CD'),
s_('InProductMarketing|Explore the power of GitLab CI/CD')
],
trial: [
s_('InProductMarketing|Go farther with GitLab'),
s_('InProductMarketing|Automated security scans directly within GitLab'),
s_('InProductMarketing|Take your source code management to the next level')
],
team: [
s_('InProductMarketing|Working in GitLab = more efficient'),
s_("InProductMarketing|Multiple owners, confusing workstreams? We've got you covered"),
s_('InProductMarketing|Your teams can be more efficient')
]
}[track][series]
end
def in_product_marketing_logo(track, series)
inline_image_link('mailers/in_product_marketing', "#{track}-#{series}.png", width: '150')
end
def about_link(folder, image, width)
link_to inline_image_link(folder, image, { width: width, alt: s_('InProductMarketing|go to about.gitlab.com') }), 'https://about.gitlab.com/'
end
def in_product_marketing_tagline(track, series)
{
create: [
s_('InProductMarketing|Get started today'),
s_('InProductMarketing|Get our import guides'),
s_('InProductMarketing|Need an alternative to importing?')
],
verify: [
s_('InProductMarketing|Use GitLab CI/CD'),
s_('InProductMarketing|Test, create, deploy'),
s_('InProductMarketing|Are your runners ready?')
],
trial: [
s_('InProductMarketing|Start a free trial of GitLab Gold – no CC required'),
s_('InProductMarketing|Improve app security with a 30-day trial'),
s_('InProductMarketing|Start with a GitLab Gold free trial')
],
team: [
s_('InProductMarketing|Invite your colleagues to join in less than one minute'),
s_('InProductMarketing|Get your team set up on GitLab'),
nil
]
}[track][series]
end
def in_product_marketing_title(track, series)
{
create: [
s_('InProductMarketing|Take your first steps with GitLab'),
s_('InProductMarketing|Start by importing your projects'),
s_('InProductMarketing|How (and why) mirroring makes sense')
],
verify: [
s_('InProductMarketing|Rapid development, simplified'),
s_('InProductMarketing|Get started with GitLab CI/CD'),
s_('InProductMarketing|Launch GitLab CI/CD in 20 minutes or less')
],
trial: [
s_('InProductMarketing|Give us one minute...'),
s_("InProductMarketing|Security that's integrated into your development lifecycle"),
s_('InProductMarketing|Improve code quality and streamline reviews')
],
team: [
s_('InProductMarketing|Team work makes the dream work'),
s_('InProductMarketing|*GitLab*, noun: a synonym for efficient teams'),
s_('InProductMarketing|Find out how your teams are really doing')
]
}[track][series]
end
def in_product_marketing_subtitle(track, series)
{
create: [
s_('InProductMarketing|Dig in and create a project and a repo'),
s_("InProductMarketing|Here's what you need to know"),
s_('InProductMarketing|Try it out')
],
verify: [
s_('InProductMarketing|How to build and test faster'),
s_('InProductMarketing|Explore the options'),
s_('InProductMarketing|Follow our steps')
],
trial: [
s_('InProductMarketing|...and you can get a free trial of GitLab Gold'),
s_('InProductMarketing|Try GitLab Gold for free'),
s_('InProductMarketing|Better code in less time')
],
team: [
s_('InProductMarketing|Actually, GitLab makes the team work (better)'),
s_('InProductMarketing|Our tool brings all the things together'),
s_("InProductMarketing|It's all in the stats")
]
}[track][series]
end
def in_product_marketing_body_line1(track, series, format: nil)
{
create: [
s_("InProductMarketing|To understand and get the most out of GitLab, start at the beginning and %{project_link}. In GitLab, repositories are part of a project, so after you've created your project you can go ahead and %{repo_link}.") % { project_link: project_link(format), repo_link: repo_link(format) },
s_("InProductMarketing|Making the switch? It's easier than you think to import your projects into GitLab. Move %{github_link}, or import something %{bitbucket_link}.") % { github_link: github_link(format), bitbucket_link: bitbucket_link(format) },
s_("InProductMarketing|Sometimes you're not ready to make a full transition to a new tool. If you're not ready to fully commit, %{mirroring_link} gives you a safe way to try out GitLab in parallel with your current tool.") % { mirroring_link: mirroring_link(format) }
],
verify: [
s_("InProductMarketing|Tired of wrestling with disparate tool chains, information silos and inefficient processes? GitLab's CI/CD is built on a DevOps platform with source code management, planning, monitoring and more ready to go. Find out %{ci_link}.") % { ci_link: ci_link(format) },
s_("InProductMarketing|GitLab's CI/CD makes software development easier. Don't believe us? Here are three ways you can take it for a fast (and satisfying) test drive:"),
s_("InProductMarketing|Get going with CI/CD quickly using our %{quick_start_link}. Start with an available runner and then create a CI .yml file – it's really that easy.") % { quick_start_link: quick_start_link(format) }
],
trial: [
[
s_("InProductMarketing|GitLab's premium tiers are designed to make you, your team and your application more efficient and more secure with features including but not limited to:"),
list([
s_('InProductMarketing|%{strong_start}Company wide portfolio management%{strong_end} — including multi-level epics, scoped labels').html_safe % strong_options(format),
s_('InProductMarketing|%{strong_start}Multiple approval roles%{strong_end} — including code owners and required merge approvals').html_safe % strong_options(format),
s_('InProductMarketing|%{strong_start}Advanced application security%{strong_end} — including SAST, DAST scanning, FUZZ testing, dependency scanning, license compliance, secrete detection').html_safe % strong_options(format),
s_('InProductMarketing|%{strong_start}Executive level insights%{strong_end} — including reporting on productivity, tasks by type, days to completion, value stream').html_safe % strong_options(format)
], format)
].join("\n"),
s_('InProductMarketing|GitLab provides static application security testing (SAST), dynamic application security testing (DAST), container scanning, and dependency scanning to help you deliver secure applications along with license compliance.'),
s_('InProductMarketing|By enabling code owners and required merge approvals the right person will review the right MR. This is a win-win: cleaner code and a more efficient review process.')
],
team: [
[
s_('InProductMarketing|Did you know teams that use GitLab are far more efficient?'),
list([
s_('InProductMarketing|Goldman Sachs went from 1 build every two weeks to thousands of builds a day'),
s_('InProductMarketing|Ticketmaster decreased their CI build time by 15X')
], format)
].join("\n"),
s_("InProductMarketing|We know a thing or two about efficiency and we don't want to keep that to ourselves. Sign up for a free trial of GitLab Gold and your teams will be on it from day one."),
[
s_('InProductMarketing|Stop wondering and use GitLab to answer questions like:'),
list([
s_('InProductMarketing|How long does it take us to close issues/MRs by types like feature requests, bugs, tech debt, security?'),
s_('InProductMarketing|How many days does it take our team to complete various tasks?'),
s_('InProductMarketing|What does our value stream timeline look like from product to development to review and production?')
], format)
].join("\n")
]
}[track][series]
end
def in_product_marketing_body_line2(track, series, format: nil)
{
create: [
s_("InProductMarketing|That's all it takes to get going with GitLab, but if you're new to working with Git, check out our %{basics_link} for helpful tips and tricks for getting started.") % { basics_link: basics_link(format) },
s_("InProductMarketing|Have a different instance you'd like to import? Here's our %{import_link}.") % { import_link: import_link(format) },
s_("InProductMarketing|It's also possible to simply %{external_repo_link} in order to take advantage of GitLab's CI/CD.") % { external_repo_link: external_repo_link(format) }
],
verify: [
nil,
list([
s_('InProductMarketing|Start by %{performance_link}').html_safe % { performance_link: performance_link(format) },
s_('InProductMarketing|Move on to easily creating a Pages website %{ci_template_link}').html_safe % { ci_template_link: ci_template_link(format) },
s_('InProductMarketing|And finally %{deploy_link} a Python application.').html_safe % { deploy_link: deploy_link(format) }
], format),
nil
],
trial: [
s_('InProductMarketing|Start a GitLab Gold trial today in less than one minute, no credit card required.'),
s_('InProductMarketing|Get started today with a 30-day GitLab Gold trial, no credit card required.'),
s_('InProductMarketing|Code owners and required merge approvals are part of the paid tiers of GitLab. You can start a free 30-day trial of GitLab Gold and enable these features in less than 5 minutes with no credit card required.')
],
team: [
s_('InProductMarketing|Invite your colleagues and start shipping code faster.'),
s_("InProductMarketing|Streamline code review, know at a glance who's unavailable, communicate in comments or in email and integrate with Slack so everyone's on the same page."),
s_('InProductMarketing|When your team is on GitLab these answers are a click away.')
]
}[track][series]
end
def cta_link(track, series, group, format: nil)
case format
when :html
link_to in_product_marketing_cta_text(track, series), in_product_marketing_cta_link(track, series, group), target: '_blank', rel: 'noopener noreferrer'
else
[in_product_marketing_cta_text(track, series), in_product_marketing_cta_link(track, series, group)].join(' >> ')
end
end
def in_product_marketing_progress(track, series)
s_('InProductMarketing|This is email %{series} of 3 in the %{track} series.') % { series: series + 1, track: track.to_s.humanize }
end
def footer_links(format: nil)
links = [
[s_('InProductMarketing|Blog'), 'https://about.gitlab.com/blog'],
[s_('InProductMarketing|Twitter'), 'https://twitter.com/gitlab'],
[s_('InProductMarketing|Facebook'), 'https://www.facebook.com/gitlab'],
[s_('InProductMarketing|YouTube'), 'https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg']
]
case format
when :html
links.map do |text, link|
link_to(text, link)
end
else
'| ' + links.map do |text, link|
[text, link].join(' ')
end.join("\n| ")
end
end
def address(format: nil)
s_('InProductMarketing|%{strong_start}GitLab Inc.%{strong_end} 268 Bush Street, #350, San Francisco, CA 94104, USA').html_safe % strong_options(format)
end
def unsubscribe(format: nil)
parts = [
s_('InProductMarketing|If you no longer wish to receive marketing emails from us,'),
s_('InProductMarketing|you may %{unsubscribe_link} at any time.') % { unsubscribe_link: unsubscribe_link(format) }
]
case format
when :html
parts.join(' ')
else
parts.join("\n" + ' ' * 16)
end
end
private
def in_product_marketing_cta_text(track, series)
{
create: [
s_('InProductMarketing|Create your first project!'),
s_('InProductMarketing|Master the art of importing!'),
s_('InProductMarketing|Understand your project options')
],
verify: [
s_('InProductMarketing|Get to know GitLab CI/CD'),
s_('InProductMarketing|Try it yourself'),
s_('InProductMarketing|Explore GitLab CI/CD')
],
trial: [
s_('InProductMarketing|Start a trial'),
s_('InProductMarketing|Beef up your security'),
s_('InProductMarketing|Go for the gold!')
],
team: [
s_('InProductMarketing|Invite your colleagues today'),
s_('InProductMarketing|Invite your team in less than 60 seconds'),
s_('InProductMarketing|Invite your team now')
]
}[track][series]
end
def in_product_marketing_cta_link(track, series, group)
{
create: [
new_project_url,
new_project_url(anchor: 'import_project'),
help_page_url('user/project/repository/repository_mirroring')
],
verify: [
project_pipelines_url(group.projects.first),
project_pipelines_url(group.projects.first),
project_pipelines_url(group.projects.first)
],
trial: [
'https://about.gitlab.com/free-trial/',
'https://about.gitlab.com/free-trial/',
'https://about.gitlab.com/free-trial/'
],
team: [
group_group_members_url(group),
group_group_members_url(group),
group_group_members_url(group)
]
}[track][series]
end
def project_link(format)
link(s_('InProductMarketing|create a project'), help_page_url('gitlab-basics/create-project'), format)
end
def repo_link(format)
link(s_('InProductMarketing|set up a repo'), help_page_url('user/project/repository/index', anchor: 'create-a-repository'), format)
end
def github_link(format)
link(s_('InProductMarketing|GitHub Enterprise projects to GitLab'), help_page_url('integration/github'), format)
end
def bitbucket_link(format)
link(s_('InProductMarketing|from Bitbucket'), help_page_url('user/project/import/bitbucket_server'), format)
end
def mirroring_link(format)
link(s_('InProductMarketing|repository mirroring'), help_page_url('user/project/repository/repository_mirroring'), format)
end
def ci_link(format)
link(s_('InProductMarketing|how easy it is to get started'), help_page_url('ci/README'), format)
end
def performance_link(format)
link(s_('InProductMarketing|testing browser performance'), help_page_url('user/project/merge_requests/browser_performance_testing'), format)
end
def ci_template_link(format)
link(s_('InProductMarketing|using a CI/CD template'), help_page_url('user/project/pages/getting_started/pages_ci_cd_template'), format)
end
def deploy_link(format)
link(s_('InProductMarketing|test and deploy'), help_page_url('ci/examples/test-and-deploy-python-application-to-heroku'), format)
end
def quick_start_link(format)
link(s_('InProductMarketing|quick start guide'), help_page_url('ci/quick_start/README'), format)
end
def basics_link(format)
link(s_('InProductMarketing|Git basics'), help_page_url('gitlab-basics/README'), format)
end
def import_link(format)
link(s_('InProductMarketing|comprehensive guide'), help_page_url('user/project/import/index'), format)
end
def external_repo_link(format)
link(s_('InProductMarketing|connect an external repository'), new_project_url(anchor: 'cicd_for_external_repo'), format)
end
def unsubscribe_link(format)
link(s_('InProductMarketing|unsubscribe'), '%tag_unsubscribe_url%', format)
end
def link(text, link, format)
case format
when :html
link_to text, link
else
"#{text} (#{link})"
end
end
def list(array, format)
case format
when :html
tag.ul { array.map { |item| concat tag.li item} }
else
'- ' + array.join("\n- ")
end
end
def strong_options(format)
case format
when :html
{ strong_start: '<b>'.html_safe, strong_end: '</b>'.html_safe }
else
{ strong_start: '', strong_end: '' }
end
end
def inline_image_link(folder, image, **options)
attachments[image] = File.read(Rails.root.join("app/assets/images", folder, image))
image_tag attachments[image].url, **options
end
end
# frozen_string_literal: true
module Emails
module InProductMarketing
include InProductMarketingHelper
FROM_ADDRESS = 'GitLab <team@gitlab.com>'.freeze
CUSTOM_HEADERS = {
'X-Mailgun-Track' => 'yes',
'X-Mailgun-Track-Clicks' => 'yes',
'X-Mailgun-Track-Opens' => 'yes',
'X-Mailgun-Tag' => 'marketing'
}.freeze
def in_product_marketing_email(recipient_id, group_id, track, series)
@track = track
@series = series
@group = Group.find(group_id)
email = User.find(recipient_id).notification_email_for(@group)
subject = subject_line(track, series)
mail_to(to: email, subject: subject)
end
private
def mail_to(to:, subject:)
mail(to: to, subject: subject, from: FROM_ADDRESS, reply_to: FROM_ADDRESS, **CUSTOM_HEADERS) do |format|
format.html { render layout: nil }
format.text { render layout: nil }
end
end
end
end
......@@ -21,6 +21,7 @@ class Notify < ApplicationMailer
include Emails::Groups
include Emails::Reviews
include Emails::ServiceDesk
include Emails::InProductMarketing
helper TimeboxesHelper
helper MergeRequestsHelper
......@@ -32,6 +33,7 @@ class Notify < ApplicationMailer
helper AvatarsHelper
helper GitlabRoutingHelper
helper IssuablesHelper
helper InProductMarketingHelper
def test_email(recipient_email, subject, body)
mail(to: recipient_email,
......
......@@ -10,6 +10,10 @@ class Experiment < ApplicationRecord
find_or_create_by!(name: name).record_user_and_group(user, group_type, context)
end
def self.add_group(name, variant:, group:)
find_or_create_by!(name: name).record_group_and_variant!(group, variant)
end
def self.record_conversion_event(name, user)
find_or_create_by!(name: name).record_conversion_event_for_user(user)
end
......@@ -24,4 +28,8 @@ class Experiment < ApplicationRecord
def record_conversion_event_for_user(user)
experiment_users.find_by(user: user, converted_at: nil)&.touch(:converted_at)
end
def record_group_and_variant!(group, variant)
experiment_subjects.find_or_initialize_by(group: group).update!(variant: variant)
end
end
......@@ -22,6 +22,24 @@ class OnboardingProgress < ApplicationRecord
:repository_mirrored
].freeze
scope :incomplete_actions, -> (actions) do
Array.wrap(actions).inject(self) { |scope, action| scope.where(column_name(action) => nil) }
end
scope :completed_actions, -> (actions) do
Array.wrap(actions).inject(self) { |scope, action| scope.where.not(column_name(action) => nil) }
end
scope :completed_actions_with_latest_in_range, -> (actions, range) do
actions = Array(actions)
if actions.size == 1
where(column_name(actions[0]) => range)
else
action_columns = actions.map { |action| arel_table[column_name(action)] }
completed_actions(actions).where(Arel::Nodes::NamedFunction.new('GREATEST', action_columns).between(range))
end
end
class << self
def onboard(namespace)
return unless root_namespace?(namespace)
......@@ -44,12 +62,12 @@ class OnboardingProgress < ApplicationRecord
where(namespace: namespace).where.not(action_column => nil).exists?
end
private
def column_name(action)
:"#{action}_at"
end
private
def root_namespace?(namespace)
namespace && namespace.root?
end
......
# 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)
......
!!!
%html{ lang: "en" }
%head
%meta{ content: "text/html; charset=utf-8", "http-equiv" => "Content-Type" }
%meta{ content: "width=device-width, initial-scale=1", name: "viewport" }
%link{ href: "https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600", rel: "stylesheet", type: "text/css" }
%title= message.subject
:css
/* CLIENT-SPECIFIC STYLES */
body,
table,
td,
a {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
/* RESET STYLES */
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
}
table {
border-collapse: collapse !important;
}
body {
height: 100% !important;
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
background-color: #ffffff;
color: #424242;
}
a {
color: #6b4fbb;
text-decoration: underline;
}
.cta_link a {
font-size: 24px;
font-family: 'Source Sans Pro', helvetica, arial, sans-serif;
color: #ffffff;
text-decoration: none;
border-radius: 5px;
-webkit-border-radius: 5px;
background-color: #6e49cb;
border-top: 15px solid #6e49cb;
border-bottom: 15px solid #6e49cb;
border-right: 40px solid #6e49cb;
border-left: 40px solid #6e49cb;
display: inline-block;
}
.footernav {
display: inline !important;
}
.footernav a {
color: #6e49cb;
}
.address {
margin: 0;
font-size: 16px;
line-height: 26px;
}
:css
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/[if gte mso 9]
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
/[if (mso)|(mso 16)]
<style type="text/css">
body, table, td, a, span { font-family: Arial, Helvetica, sans-serif !important; }
</style>
:css
@media only screen and (max-width: 595px) {
.wrapper {
width: 100% !important;
margin: 0 auto !important;
padding: 0 !important;
}
p,
li {
font-size: 18px !important;
line-height: 26px !important;
}
.stack {
width: 100% !important;
}
.stack-mobile-padding {
width: 100% !important;
margin-top: 20px !important;
}
.callout {
padding-bottom: 20px !important;
}
.redbutton {
text-align: center;
}
.stack33 {
display: block !important;
width: 100% !important;
max-width: 100% !important;
direction: ltr !important;
text-align: center !important;
}
}
@media only screen and (max-width: 480px) {
u~div {
width: 100vw !important;
}
div>u~div {
width: 100% !important;
}
}
%body#body{ width: "100%" }
%table{ border: "0", cellpadding: "0", cellspacing: "0", role: "presentation", width: "100%" }
%tr
%td{ align: "center", style: "padding: 0px;" }
%table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", role: "presentation", width: "600" }
%tr
%td{ style: "padding: 0px;" }
#main-story.mktEditable{ mktoname: "main-story" }
%table{ border: "0", cellpadding: "0", cellspacing: "0", role: "presentation", width: "100%" }
%tr
%td{ align: "left", style: "padding: 0px;" }
= about_link('mailers/in_product_marketing', 'gitlab-logo-gray-rgb.png', 200)
%tr
%td{ "aria-hidden" => "true", height: "30", style: "font-size: 0; line-height: 0;" }
%tr
%td{ bgcolor: "#ffffff", height: "auto", style: "max-width: 600px; width: 100%; text-align: center; height: 200px; padding: 25px 15px; mso-line-height-rule: exactly; min-height: 40px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;", valign: "middle", width: "100%" }
= in_product_marketing_logo(@track, @series)
%h1{ style: "font-size: 40px; line-height: 46x; color: #000000; padding: 20px 0 0 0; font-weight: normal;" }
= in_product_marketing_title(@track, @series)
%h2{ style: "font-size: 28px; line-height: 34px; color: #000000; padding: 0; font-weight: 400;" }
= in_product_marketing_subtitle(@track, @series)
%tr
%td{ style: "padding: 10px 20px 30px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; color:#000000; font-size: 18px; line-height: 24px;" }
%p{ style: "margin: 0 0 20px 0;" }
= in_product_marketing_body_line1(@track, @series, format: :html).html_safe
%p{ style: "margin: 0 0 20px 0;" }
= in_product_marketing_body_line2(@track, @series, format: :html).html_safe
%tr
%td{ align: "center", style: "padding: 10px 20px 80px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
.cta_link= cta_link(@track, @series, @group, format: :html)
%tr{ style: "background-color: #ffffff;" }
%td{ style: "color: #424242; padding: 10px 30px; text-align: center; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;font-size: 16px; line-height: 22px; border: 1px solid #dddddd" }
%p
= in_product_marketing_progress(@track, @series)
%tr{ style: "background-color: #ffffff;" }
%td{ align: "center", style: "padding:50px 20px 0 20px;" }
= about_link('', 'gitlab_logo.png', 80)
%tr{ style: "background-color: #ffffff;" }
%td{ align: "center", style: "padding:0px ;" }
%tr{ style: "background-color: #ffffff;" }
%td{ align: "center", style: "padding:0px 10px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; " }
%span.footernav{ style: "color: #6e49cb; font-size: 16px; line-height: 26px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
= footer_links(format: :html).join('&nbsp;' * 3 + '|' + '&nbsp;' * 4).html_safe
%tr{ style: "background-color:#ffffff;" }
%td{ align: "center", style: "padding: 40px 30px 20px 30px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
.address= address(format: :html)
%tr{ style: "background-color: #ffffff;" }
%td{ align: "left", style: "padding:20px 30px 20px 30px;" }
%span.footernav{ style: "color: #6e49cb; font-size: 14px; line-height: 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; color:#424242;" }
= unsubscribe(format: :html).html_safe
<%= in_product_marketing_tagline(@track, @series) %>
<%= in_product_marketing_title(@track, @series) %>
<%= in_product_marketing_subtitle(@track, @series) %>
<%= in_product_marketing_body_line1(@track, @series) %>
<%= in_product_marketing_body_line2(@track, @series) %>
<%= cta_link(@track, @series, @group) %>
<%= footer_links %>
<%= address %>
<%= unsubscribe %>
......@@ -251,6 +251,14 @@
:weight: 1
:idempotent: true
:tags: []
- :name: cronjob:namespaces_in_product_marketing_emails
:feature_category: :subgroups
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
:tags: []
- :name: cronjob:namespaces_prune_aggregation_schedules
:feature_category: :source_code_management
:has_external_dependencies:
......
# frozen_string_literal: true
module Namespaces
class InProductMarketingEmailsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :subgroups
urgency :low
def perform
return unless Gitlab::Experimentation.active?(:in_product_marketing_emails)
Namespaces::InProductMarketingEmailsService.send_for_all_tracks_and_intervals
end
end
end
---
title: Add indexes for onboarding progress table
merge_request: 50679
author:
type: performance
---
name: in_product_marketing_emails_experiment_percentage
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50679
rollout_issue_url: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/303
milestone: "13.9"
type: experiment
group: group::activation
default_enabled: false
......@@ -544,6 +544,9 @@ Settings.cron_jobs['schedule_merge_request_cleanup_refs_worker']['job_class'] =
Settings.cron_jobs['manage_evidence_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['manage_evidence_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['manage_evidence_worker']['job_class'] = 'Releases::ManageEvidenceWorker'
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.ee do
Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker'] ||= Settingslogic.new({})
......
# frozen_string_literal: true
class AddIndexesToOnboardingProgresses < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
CREATE_TRACK_INDEX_NAME = 'index_onboarding_progresses_for_create_track'
VERIFY_TRACK_INDEX_NAME = 'index_onboarding_progresses_for_verify_track'
TRIAL_TRACK_INDEX_NAME = 'index_onboarding_progresses_for_trial_track'
TEAM_TRACK_INDEX_NAME = 'index_onboarding_progresses_for_team_track'
disable_ddl_transaction!
def up
add_concurrent_index :onboarding_progresses, :created_at, where: 'git_write_at IS NULL', name: CREATE_TRACK_INDEX_NAME
add_concurrent_index :onboarding_progresses, :git_write_at, where: 'git_write_at IS NOT NULL AND pipeline_created_at IS NULL', name: VERIFY_TRACK_INDEX_NAME
add_concurrent_index :onboarding_progresses, 'GREATEST(git_write_at, pipeline_created_at)', where: 'git_write_at IS NOT NULL AND pipeline_created_at IS NOT NULL AND trial_started_at IS NULL', name: TRIAL_TRACK_INDEX_NAME
add_concurrent_index :onboarding_progresses, 'GREATEST(git_write_at, pipeline_created_at, trial_started_at)', where: 'git_write_at IS NOT NULL AND pipeline_created_at IS NOT NULL AND trial_started_at IS NOT NULL AND user_added_at IS NULL', name: TEAM_TRACK_INDEX_NAME
end
def down
remove_concurrent_index_by_name :onboarding_progresses, CREATE_TRACK_INDEX_NAME
remove_concurrent_index_by_name :onboarding_progresses, VERIFY_TRACK_INDEX_NAME
remove_concurrent_index_by_name :onboarding_progresses, TRIAL_TRACK_INDEX_NAME
remove_concurrent_index_by_name :onboarding_progresses, TEAM_TRACK_INDEX_NAME
end
end
7ef5cb1f167c133c67fc98c0abe929516ec700179747d3353d19cf8219ebd0ef
\ No newline at end of file
......@@ -22511,6 +22511,14 @@ CREATE INDEX index_on_users_lower_username ON users USING btree (lower((username
CREATE INDEX index_on_users_name_lower ON users USING btree (lower((name)::text));
CREATE INDEX index_onboarding_progresses_for_create_track ON onboarding_progresses USING btree (created_at) WHERE (git_write_at IS NULL);
CREATE INDEX index_onboarding_progresses_for_team_track ON onboarding_progresses USING btree (GREATEST(git_write_at, pipeline_created_at, trial_started_at)) WHERE ((git_write_at IS NOT NULL) AND (pipeline_created_at IS NOT NULL) AND (trial_started_at IS NOT NULL) AND (user_added_at IS NULL));
CREATE INDEX index_onboarding_progresses_for_trial_track ON onboarding_progresses USING btree (GREATEST(git_write_at, pipeline_created_at)) WHERE ((git_write_at IS NOT NULL) AND (pipeline_created_at IS NOT NULL) AND (trial_started_at IS NULL));
CREATE INDEX index_onboarding_progresses_for_verify_track ON onboarding_progresses USING btree (git_write_at) WHERE ((git_write_at IS NOT NULL) AND (pipeline_created_at IS NULL));
CREATE UNIQUE INDEX index_onboarding_progresses_on_namespace_id ON onboarding_progresses USING btree (namespace_id);
CREATE INDEX index_open_project_tracker_data_on_service_id ON open_project_tracker_data USING btree (service_id);
......
......@@ -117,6 +117,8 @@ module EE
condition(:over_storage_limit, scope: :subject) { @subject.over_storage_limit? }
condition(:eligible_for_trial, scope: :subject) { @subject.eligible_for_trial? }
rule { public_group | logged_in_viewable }.policy do
enable :read_wiki
enable :download_wiki_code
......@@ -310,6 +312,8 @@ module EE
rule { admin & is_gitlab_com }.enable :update_subscription_limit
rule { maintainer & eligible_for_trial }.enable :start_trial
rule { over_storage_limit }.policy do
prevent :create_projects
prevent :create_epic
......
......@@ -1395,5 +1395,36 @@ RSpec.describe GroupPolicy do
it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
end
end
describe ':start_trial' do
using RSpec::Parameterized::TableSyntax
let(:policy) { :start_trial }
where(:role, :eligible_for_trial, :allowed) do
:guest | true | false
:guest | false | false
:reporter | true | false
:reporter | false | false
:developer | true | false
:developer | false | false
:maintainer | true | true
:maintainer | false | false
:owner | true | true
:owner | false | false
:admin | true | true
:admin | false | false
end
with_them do
let(:current_user) { public_send(role) }
before do
allow(group).to receive(:eligible_for_trial?).and_return(eligible_for_trial)
end
it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
end
end
end
end
......@@ -109,6 +109,9 @@ module Gitlab
},
trial_onboarding_issues: {
tracking_category: 'Growth::Conversion::Experiment::TrialOnboardingIssues'
},
in_product_marketing_emails: {
tracking_category: 'Growth::Activation::Experiment::InProductMarketingEmails'
}
}.freeze
......
......@@ -14978,6 +14978,351 @@ msgstr ""
msgid "In progress"
msgstr ""
msgid "InProductMarketing|%{strong_start}Advanced application security%{strong_end} — including SAST, DAST scanning, FUZZ testing, dependency scanning, license compliance, secrete detection"
msgstr ""
msgid "InProductMarketing|%{strong_start}Company wide portfolio management%{strong_end} — including multi-level epics, scoped labels"
msgstr ""
msgid "InProductMarketing|%{strong_start}Executive level insights%{strong_end} — including reporting on productivity, tasks by type, days to completion, value stream"
msgstr ""
msgid "InProductMarketing|%{strong_start}GitLab Inc.%{strong_end} 268 Bush Street, #350, San Francisco, CA 94104, USA"
msgstr ""
msgid "InProductMarketing|%{strong_start}Multiple approval roles%{strong_end} — including code owners and required merge approvals"
msgstr ""
msgid "InProductMarketing|*GitLab*, noun: a synonym for efficient teams"
msgstr ""
msgid "InProductMarketing|...and you can get a free trial of GitLab Gold"
msgstr ""
msgid "InProductMarketing|3 ways to dive into GitLab CI/CD"
msgstr ""
msgid "InProductMarketing|Actually, GitLab makes the team work (better)"
msgstr ""
msgid "InProductMarketing|And finally %{deploy_link} a Python application."
msgstr ""
msgid "InProductMarketing|Are your runners ready?"
msgstr ""
msgid "InProductMarketing|Automated security scans directly within GitLab"
msgstr ""
msgid "InProductMarketing|Beef up your security"
msgstr ""
msgid "InProductMarketing|Better code in less time"
msgstr ""
msgid "InProductMarketing|Blog"
msgstr ""
msgid "InProductMarketing|By enabling code owners and required merge approvals the right person will review the right MR. This is a win-win: cleaner code and a more efficient review process."
msgstr ""
msgid "InProductMarketing|Code owners and required merge approvals are part of the paid tiers of GitLab. You can start a free 30-day trial of GitLab Gold and enable these features in less than 5 minutes with no credit card required."
msgstr ""
msgid "InProductMarketing|Create a project in GitLab in 5 minutes"
msgstr ""
msgid "InProductMarketing|Create your first project!"
msgstr ""
msgid "InProductMarketing|Did you know teams that use GitLab are far more efficient?"
msgstr ""
msgid "InProductMarketing|Dig in and create a project and a repo"
msgstr ""
msgid "InProductMarketing|Explore GitLab CI/CD"
msgstr ""
msgid "InProductMarketing|Explore the options"
msgstr ""
msgid "InProductMarketing|Explore the power of GitLab CI/CD"
msgstr ""
msgid "InProductMarketing|Facebook"
msgstr ""
msgid "InProductMarketing|Feel the need for speed?"
msgstr ""
msgid "InProductMarketing|Find out how your teams are really doing"
msgstr ""
msgid "InProductMarketing|Follow our steps"
msgstr ""
msgid "InProductMarketing|Get going with CI/CD quickly using our %{quick_start_link}. Start with an available runner and then create a CI .yml file – it's really that easy."
msgstr ""
msgid "InProductMarketing|Get our import guides"
msgstr ""
msgid "InProductMarketing|Get started today"
msgstr ""
msgid "InProductMarketing|Get started today with a 30-day GitLab Gold trial, no credit card required."
msgstr ""
msgid "InProductMarketing|Get started with GitLab CI/CD"
msgstr ""
msgid "InProductMarketing|Get to know GitLab CI/CD"
msgstr ""
msgid "InProductMarketing|Get your team set up on GitLab"
msgstr ""
msgid "InProductMarketing|Git basics"
msgstr ""
msgid "InProductMarketing|GitHub Enterprise projects to GitLab"
msgstr ""
msgid "InProductMarketing|GitLab provides static application security testing (SAST), dynamic application security testing (DAST), container scanning, and dependency scanning to help you deliver secure applications along with license compliance."
msgstr ""
msgid "InProductMarketing|GitLab's CI/CD makes software development easier. Don't believe us? Here are three ways you can take it for a fast (and satisfying) test drive:"
msgstr ""
msgid "InProductMarketing|GitLab's premium tiers are designed to make you, your team and your application more efficient and more secure with features including but not limited to:"
msgstr ""
msgid "InProductMarketing|Give us one minute..."
msgstr ""
msgid "InProductMarketing|Go farther with GitLab"
msgstr ""
msgid "InProductMarketing|Go for the gold!"
msgstr ""
msgid "InProductMarketing|Goldman Sachs went from 1 build every two weeks to thousands of builds a day"
msgstr ""
msgid "InProductMarketing|Have a different instance you'd like to import? Here's our %{import_link}."
msgstr ""
msgid "InProductMarketing|Here's what you need to know"
msgstr ""
msgid "InProductMarketing|How (and why) mirroring makes sense"
msgstr ""
msgid "InProductMarketing|How long does it take us to close issues/MRs by types like feature requests, bugs, tech debt, security?"
msgstr ""
msgid "InProductMarketing|How many days does it take our team to complete various tasks?"
msgstr ""
msgid "InProductMarketing|How to build and test faster"
msgstr ""
msgid "InProductMarketing|If you no longer wish to receive marketing emails from us,"
msgstr ""
msgid "InProductMarketing|Import your project and code from GitHub, Bitbucket and others"
msgstr ""
msgid "InProductMarketing|Improve app security with a 30-day trial"
msgstr ""
msgid "InProductMarketing|Improve code quality and streamline reviews"
msgstr ""
msgid "InProductMarketing|Invite your colleagues and start shipping code faster."
msgstr ""
msgid "InProductMarketing|Invite your colleagues to join in less than one minute"
msgstr ""
msgid "InProductMarketing|Invite your colleagues today"
msgstr ""
msgid "InProductMarketing|Invite your team in less than 60 seconds"
msgstr ""
msgid "InProductMarketing|Invite your team now"
msgstr ""
msgid "InProductMarketing|It's all in the stats"
msgstr ""
msgid "InProductMarketing|It's also possible to simply %{external_repo_link} in order to take advantage of GitLab's CI/CD."
msgstr ""
msgid "InProductMarketing|Launch GitLab CI/CD in 20 minutes or less"
msgstr ""
msgid "InProductMarketing|Making the switch? It's easier than you think to import your projects into GitLab. Move %{github_link}, or import something %{bitbucket_link}."
msgstr ""
msgid "InProductMarketing|Master the art of importing!"
msgstr ""
msgid "InProductMarketing|Move on to easily creating a Pages website %{ci_template_link}"
msgstr ""
msgid "InProductMarketing|Multiple owners, confusing workstreams? We've got you covered"
msgstr ""
msgid "InProductMarketing|Need an alternative to importing?"
msgstr ""
msgid "InProductMarketing|Our tool brings all the things together"
msgstr ""
msgid "InProductMarketing|Rapid development, simplified"
msgstr ""
msgid "InProductMarketing|Security that's integrated into your development lifecycle"
msgstr ""
msgid "InProductMarketing|Sometimes you're not ready to make a full transition to a new tool. If you're not ready to fully commit, %{mirroring_link} gives you a safe way to try out GitLab in parallel with your current tool."
msgstr ""
msgid "InProductMarketing|Start a GitLab Gold trial today in less than one minute, no credit card required."
msgstr ""
msgid "InProductMarketing|Start a free trial of GitLab Gold – no CC required"
msgstr ""
msgid "InProductMarketing|Start a trial"
msgstr ""
msgid "InProductMarketing|Start by %{performance_link}"
msgstr ""
msgid "InProductMarketing|Start by importing your projects"
msgstr ""
msgid "InProductMarketing|Start with a GitLab Gold free trial"
msgstr ""
msgid "InProductMarketing|Stop wondering and use GitLab to answer questions like:"
msgstr ""
msgid "InProductMarketing|Streamline code review, know at a glance who's unavailable, communicate in comments or in email and integrate with Slack so everyone's on the same page."
msgstr ""
msgid "InProductMarketing|Take your first steps with GitLab"
msgstr ""
msgid "InProductMarketing|Take your source code management to the next level"
msgstr ""
msgid "InProductMarketing|Team work makes the dream work"
msgstr ""
msgid "InProductMarketing|Test, create, deploy"
msgstr ""
msgid "InProductMarketing|That's all it takes to get going with GitLab, but if you're new to working with Git, check out our %{basics_link} for helpful tips and tricks for getting started."
msgstr ""
msgid "InProductMarketing|This is email %{series} of 3 in the %{track} series."
msgstr ""
msgid "InProductMarketing|Ticketmaster decreased their CI build time by 15X"
msgstr ""
msgid "InProductMarketing|Tired of wrestling with disparate tool chains, information silos and inefficient processes? GitLab's CI/CD is built on a DevOps platform with source code management, planning, monitoring and more ready to go. Find out %{ci_link}."
msgstr ""
msgid "InProductMarketing|To understand and get the most out of GitLab, start at the beginning and %{project_link}. In GitLab, repositories are part of a project, so after you've created your project you can go ahead and %{repo_link}."
msgstr ""
msgid "InProductMarketing|Try GitLab Gold for free"
msgstr ""
msgid "InProductMarketing|Try it out"
msgstr ""
msgid "InProductMarketing|Try it yourself"
msgstr ""
msgid "InProductMarketing|Twitter"
msgstr ""
msgid "InProductMarketing|Understand repository mirroring"
msgstr ""
msgid "InProductMarketing|Understand your project options"
msgstr ""
msgid "InProductMarketing|Use GitLab CI/CD"
msgstr ""
msgid "InProductMarketing|We know a thing or two about efficiency and we don't want to keep that to ourselves. Sign up for a free trial of GitLab Gold and your teams will be on it from day one."
msgstr ""
msgid "InProductMarketing|What does our value stream timeline look like from product to development to review and production?"
msgstr ""
msgid "InProductMarketing|When your team is on GitLab these answers are a click away."
msgstr ""
msgid "InProductMarketing|Working in GitLab = more efficient"
msgstr ""
msgid "InProductMarketing|YouTube"
msgstr ""
msgid "InProductMarketing|Your teams can be more efficient"
msgstr ""
msgid "InProductMarketing|comprehensive guide"
msgstr ""
msgid "InProductMarketing|connect an external repository"
msgstr ""
msgid "InProductMarketing|create a project"
msgstr ""
msgid "InProductMarketing|from Bitbucket"
msgstr ""
msgid "InProductMarketing|go to about.gitlab.com"
msgstr ""
msgid "InProductMarketing|how easy it is to get started"
msgstr ""
msgid "InProductMarketing|quick start guide"
msgstr ""
msgid "InProductMarketing|repository mirroring"
msgstr ""
msgid "InProductMarketing|set up a repo"
msgstr ""
msgid "InProductMarketing|test and deploy"
msgstr ""
msgid "InProductMarketing|testing browser performance"
msgstr ""
msgid "InProductMarketing|unsubscribe"
msgstr ""
msgid "InProductMarketing|using a CI/CD template"
msgstr ""
msgid "InProductMarketing|you may %{unsubscribe_link} at any time."
msgstr ""
msgid "Incident"
msgstr ""
......
......@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Experiment do
include AfterNextHelpers
subject { build(:experiment) }
describe 'associations' do
......@@ -67,6 +69,33 @@ RSpec.describe Experiment do
end
end
describe '.add_group' do
let_it_be(:experiment_name) { :experiment_key }
let_it_be(:variant) { :control }
let_it_be(:group) { build(:group) }
subject(:add_group) { described_class.add_group(experiment_name, variant: variant, group: group) }
context 'when an experiment with the provided name does not exist' do
it 'creates a new experiment record' do
allow_next(described_class, name: :experiment_key)
.to receive(:record_group_and_variant!).with(group, variant)
expect { add_group }.to change(described_class, :count).by(1)
end
end
context 'when an experiment with the provided name already exists' do
before do
create(:experiment, name: experiment_name)
end
it 'does not create a new experiment record' do
expect { add_group }.not_to change(described_class, :count)
end
end
end
describe '.record_conversion_event' do
let_it_be(:user) { build(:user) }
......@@ -136,6 +165,34 @@ RSpec.describe Experiment do
end
end
describe '#record_group_and_variant!' do
let_it_be(:group) { create(:group) }
let_it_be(:variant) { :control }
let_it_be(:experiment) { create(:experiment) }
subject(:record_group_and_variant!) { experiment.record_group_and_variant!(group, variant) }
context 'when no existing experiment_subject record exists for the given group' do
it 'creates an experiment_subject record' do
expect_next(ExperimentSubject).to receive(:update!).with(variant: variant).and_call_original
expect { record_group_and_variant! }.to change(ExperimentSubject, :count).by(1)
end
end
context 'when an existing experiment_subject exists for the given group' do
context 'but it belonged to a different variant' do
let!(:experiment_subject) do
create(:experiment_subject, experiment: experiment, group: group, user: nil, variant: :experimental)
end
it 'updates the variant value' do
expect { record_group_and_variant! }.to change { experiment_subject.reload.variant }.to('control')
end
end
end
end
describe '#record_user_and_group' do
let_it_be(:experiment) { create(:experiment) }
let_it_be(:user) { create(:user) }
......
......@@ -29,6 +29,67 @@ RSpec.describe OnboardingProgress do
end
end
describe 'scopes' do
describe '.incomplete_actions' do
subject { described_class.incomplete_actions(actions) }
let!(:no_actions_completed) { create(:onboarding_progress) }
let!(:one_action_completed_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => Time.current) }
context 'when given one action' do
let(:actions) { action }
it { is_expected.to eq [no_actions_completed] }
end
context 'when given an array of actions' do
let(:actions) { [action, :git_write] }
it { is_expected.to eq [no_actions_completed] }
end
end
describe '.completed_actions' do
subject { described_class.completed_actions(actions) }
let!(:one_action_completed_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => Time.current) }
let!(:both_actions_completed) { create(:onboarding_progress, "#{action}_at" => Time.current, git_write_at: Time.current) }
context 'when given one action' do
let(:actions) { action }
it { is_expected.to eq [one_action_completed_one_action_incompleted, both_actions_completed] }
end
context 'when given an array of actions' do
let(:actions) { [action, :git_write] }
it { is_expected.to eq [both_actions_completed] }
end
end
describe '.completed_actions_with_latest_in_range' do
subject { described_class.completed_actions_with_latest_in_range(actions, 1.day.ago.beginning_of_day..1.day.ago.end_of_day) }
let!(:one_action_completed_in_range_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day) }
let!(:git_write_action_completed_in_range) { create(:onboarding_progress, git_write_at: 1.day.ago.middle_of_day) }
let!(:both_actions_completed_latest_action_out_of_range) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: Time.current) }
let!(:both_actions_completed_latest_action_in_range) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day) }
context 'when given one action' do
let(:actions) { :git_write }
it { is_expected.to eq [git_write_action_completed_in_range] }
end
context 'when given an array of actions' do
let(:actions) { [action, :git_write] }
it { is_expected.to eq [both_actions_completed_latest_action_in_range] }
end
end
end
describe '.onboard' do
subject(:onboard) { described_class.onboard(namespace) }
......@@ -104,4 +165,10 @@ RSpec.describe OnboardingProgress do
end
end
end
describe '.column_name' do
subject { described_class.column_name(action) }
it { is_expected.to eq(:subscription_created_at) }
end
end
# 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
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Namespaces::InProductMarketingEmailsWorker, '#perform' do
context 'when the experiment is inactive' do
before do
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
end
end
context 'when the experiment is active' do
before do
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
expect(Namespaces::InProductMarketingEmailsService).to receive(:send_for_all_tracks_and_intervals)
subject.perform
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