Commit 05afe03a authored by Alex Buijs's avatar Alex Buijs Committed by Imre Farkas

Add ease score onboarding in product marketing email

parent 74b7bc04
...@@ -16,7 +16,7 @@ class Groups::EmailCampaignsController < Groups::ApplicationController ...@@ -16,7 +16,7 @@ class Groups::EmailCampaignsController < Groups::ApplicationController
def track_click def track_click
if Gitlab.com? if Gitlab.com?
message = Gitlab::Email::Message::InProductMarketing.for(@track).new(group: group, series: @series) message = Gitlab::Email::Message::InProductMarketing.for(@track).new(group: group, user: current_user, series: @series)
data = { data = {
namespace_id: group.id, namespace_id: group.id,
...@@ -58,8 +58,9 @@ class Groups::EmailCampaignsController < Groups::ApplicationController ...@@ -58,8 +58,9 @@ class Groups::EmailCampaignsController < Groups::ApplicationController
@series = params[:series]&.to_i @series = params[:series]&.to_i
track_valid = @track.in?(Namespaces::InProductMarketingEmailsService::TRACKS.keys) track_valid = @track.in?(Namespaces::InProductMarketingEmailsService::TRACKS.keys)
series_valid = @series.in?(0..Namespaces::InProductMarketingEmailsService::INTERVAL_DAYS.size - 1) return render_404 unless track_valid
render_404 unless track_valid && series_valid series_valid = @series.in?(0..Namespaces::InProductMarketingEmailsService::TRACKS[@track][:interval_days].size - 1)
render_404 unless series_valid
end end
end end
...@@ -14,8 +14,9 @@ module Emails ...@@ -14,8 +14,9 @@ module Emails
def in_product_marketing_email(recipient_id, group_id, track, series) def in_product_marketing_email(recipient_id, group_id, track, series)
group = Group.find(group_id) group = Group.find(group_id)
email = User.find(recipient_id).notification_email_for(group) user = User.find(recipient_id)
@message = Gitlab::Email::Message::InProductMarketing.for(track).new(group: group, series: series) email = user.notification_email_for(group)
@message = Gitlab::Email::Message::InProductMarketing.for(track).new(group: group, user: user, series: series)
mail_to(to: email, subject: @message.subject_line) mail_to(to: email, subject: @message.subject_line)
end end
......
...@@ -85,6 +85,10 @@ class OnboardingProgress < ApplicationRecord ...@@ -85,6 +85,10 @@ class OnboardingProgress < ApplicationRecord
end end
end end
def number_of_completed_actions
attributes.extract!(*ACTIONS.map { |action| self.class.column_name(action).to_s }).compact!.size
end
private private
def namespace_is_root_namespace def namespace_is_root_namespace
......
...@@ -18,7 +18,8 @@ module Users ...@@ -18,7 +18,8 @@ module Users
create: 0, create: 0,
verify: 1, verify: 1,
trial: 2, trial: 2,
team: 3 team: 3,
experience: 4
}, _suffix: true }, _suffix: true
scope :without_track_and_series, -> (track, series) do scope :without_track_and_series, -> (track, series) do
......
...@@ -4,17 +4,37 @@ module Namespaces ...@@ -4,17 +4,37 @@ module Namespaces
class InProductMarketingEmailsService class InProductMarketingEmailsService
include Gitlab::Experimentation::GroupTypes include Gitlab::Experimentation::GroupTypes
INTERVAL_DAYS = [1, 5, 10].freeze
TRACKS = { TRACKS = {
create: :git_write, create: {
verify: :pipeline_created, interval_days: [1, 5, 10],
trial: :trial_started, completed_actions: [:created],
team: :user_added incomplete_actions: [:git_write]
},
verify: {
interval_days: [1, 5, 10],
completed_actions: [:git_write],
incomplete_actions: [:pipeline_created]
},
trial: {
interval_days: [1, 5, 10],
completed_actions: [:git_write, :pipeline_created],
incomplete_actions: [:trial_started]
},
team: {
interval_days: [1, 5, 10],
completed_actions: [:git_write, :pipeline_created, :trial_started],
incomplete_actions: [:user_added]
},
experience: {
interval_days: [30],
completed_actions: [:created, :git_write],
incomplete_actions: []
}
}.freeze }.freeze
def self.send_for_all_tracks_and_intervals def self.send_for_all_tracks_and_intervals
TRACKS.each_key do |track| TRACKS.each_key do |track|
INTERVAL_DAYS.each do |interval| TRACKS[track][:interval_days].each do |interval|
new(track, interval).execute new(track, interval).execute
end end
end end
...@@ -69,7 +89,7 @@ module Namespaces ...@@ -69,7 +89,7 @@ module Namespaces
def groups_for_track def groups_for_track
onboarding_progress_scope = OnboardingProgress onboarding_progress_scope = OnboardingProgress
.completed_actions_with_latest_in_range(completed_actions, range) .completed_actions_with_latest_in_range(completed_actions, range)
.incomplete_actions(incomplete_action) .incomplete_actions(incomplete_actions)
# Filtering out sub-groups is a temporary fix to prevent calling # Filtering out sub-groups is a temporary fix to prevent calling
# `.root_ancestor` on groups that are not root groups. # `.root_ancestor` on groups that are not root groups.
...@@ -103,6 +123,8 @@ module Namespaces ...@@ -103,6 +123,8 @@ module Namespaces
user.can?(:start_trial, group) user.can?(:start_trial, group)
when :team when :team
user.can?(:admin_group_member, group) user.can?(:admin_group_member, group)
when :experience
true
end end
end end
...@@ -111,8 +133,7 @@ module Namespaces ...@@ -111,8 +133,7 @@ module Namespaces
end end
def completed_actions def completed_actions
index = TRACKS.keys.index(track) TRACKS[track][:completed_actions]
index == 0 ? [:created] : TRACKS.values[0..index - 1]
end end
def range def range
...@@ -120,12 +141,12 @@ module Namespaces ...@@ -120,12 +141,12 @@ module Namespaces
date.beginning_of_day..date.end_of_day date.beginning_of_day..date.end_of_day
end end
def incomplete_action def incomplete_actions
TRACKS[track] TRACKS[track][:incomplete_actions]
end end
def series def series
INTERVAL_DAYS.index(interval) TRACKS[track][:interval_days].index(interval)
end end
def save_tracked_emails! def save_tracked_emails!
......
...@@ -184,9 +184,32 @@ ...@@ -184,9 +184,32 @@
- @message.body_line2&.tap do |line| - @message.body_line2&.tap do |line|
%p{ style: "margin: 0 0 20px 0;" } %p{ style: "margin: 0 0 20px 0;" }
= line.html_safe = line.html_safe
- if @message.cta_text
%tr %tr
%td{ align: "center", style: "padding: 10px 20px 80px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" } %td{ align: "center", style: "padding: 10px 20px 80px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
.cta_link= @message.cta_link .cta_link= @message.cta_link
- else
%tr
%td{ style: "padding: 10px 20px 10px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; color:#000000; font-size: 16px; line-height: 20px;" }
%table{ border: "0", cellpadding: "0", cellspacing: "0", width: "100%", style: "width: 100%; min-width: 100%;" }
%tr
%td{ width: "50%", style: "width: 50%; min-width: 50%; color: #000000; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; font-size: 16px; line-height: 100%; padding-bottom: 16px; text-align: left;", align: "left" }
= @message.feedback_ratings(1)
%td{ width: "50%", style: "width: 50%; min-width: 50%; color: #000000; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; font-size: 16px; line-height: 100%; padding-bottom: 16px; text-align: right;", align: "right" }
= @message.feedback_ratings(5)
%tr
%td{ align: "center", style: "padding: 10px 1px 30px 1px;" }
%table{ align: "center", cellpadding: "5", cellspacing: "0", width: "100%", style: "width: 100%; min-width: 100%; border: 1px solid #dae0ea; border-radius: 0; min-width: 100%; text-align: center; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; font-size: 16px;" }
%tr
- (1..5).each do |rating|
%td{ height: "54", style: "border-left: 1px solid #dae0ea; padding-bottom: 0; width: 9% !important;", width: "9%" }
%a{ href: @message.feedback_link(rating), style: "color: #424242; display: block; text-decoration: none;" }
%span{ height: "54", style: "display: block; font-size: 18px; height: 22px; line-height: 22px; padding: 16px 0; width: 100%; text-decoration: none;" }
= rating
%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 50px 0;" }
= @message.feedback_thanks
%tr{ style: "background-color: #ffffff;" } %tr{ style: "background-color: #ffffff;" }
%td{ align: "center", style: "padding:75px 20px 25px;" } %td{ align: "center", style: "padding:75px 20px 25px;" }
= about_link('gitlab_logo.png', 80) = about_link('gitlab_logo.png', 80)
......
...@@ -8,10 +8,19 @@ ...@@ -8,10 +8,19 @@
<%= @message.body_line2 %> <%= @message.body_line2 %>
<% if @message.cta_text %>
<%= @message.cta_link %> <%= @message.cta_link %>
<% else %>
<% (1..5).each do |rating| %>
<%= "#{rating} - #{@message.feedback_ratings(rating).upcase} - #{@message.feedback_link(rating)}" %>
<% end %>
<%= @message.feedback_thanks %>
<% end %>
......
---
title: Add ease score onboarding in-product marketing email
merge_request: 61347
author:
type: changed
---
key_path: counts.in_product_marketing_email_experience_0_sent
name: "count_sent_first_email_of_the_experience_track_for_in_product_marketing_emails"
description: Total sent emails of the experience track's first email
product_section:
product_stage: growth
product_group: group::activation
product_category: onboarding
value_type: number
status: implemented
milestone: "13.12"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61347
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
...@@ -2422,6 +2422,18 @@ Status: `implemented` ...@@ -2422,6 +2422,18 @@ Status: `implemented`
Tiers: `free`, `premium`, `ultimate` Tiers: `free`, `premium`, `ultimate`
### `counts.in_product_marketing_email_experience_0_sent`
Total sent emails of the experience track's first email
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210518081225_in_product_marketing_email_experience_0_sent.yml)
Group: `group::activation`
Status: `implemented`
Tiers: `free`, `premium`, `ultimate`
### `counts.in_review_folder` ### `counts.in_review_folder`
Missing description Missing description
......
# frozen_string_literal: true # frozen_string_literal: true
class SurveyResponsesController < ApplicationController class SurveyResponsesController < ApplicationController
SURVEY_RESPONSE_SCHEMA_URL = 'iglu:com.gitlab/survey_response/jsonschema/1-0-0' SURVEY_RESPONSE_SCHEMA_URL = 'iglu:com.gitlab/survey_response/jsonschema/1-0-1'
CALENDLY_INVITE_LINK = 'https://calendly.com/mkarampalas/gitlab-user-onboarding-research' CALENDLY_INVITE_LINK = 'https://calendly.com/mkarampalas/gitlab-user-onboarding-research'
before_action :track_response, only: :index before_action :track_response, only: :index
...@@ -27,7 +27,8 @@ class SurveyResponsesController < ApplicationController ...@@ -27,7 +27,8 @@ class SurveyResponsesController < ApplicationController
email: params[:email], email: params[:email],
name: params[:name], name: params[:name],
username: params[:username], username: params[:username],
response: params[:response] response: params[:response],
onboarding_progress: to_number(params[:onboarding_progress])
}.compact }.compact
context = SnowplowTracker::SelfDescribingJson.new(SURVEY_RESPONSE_SCHEMA_URL, data) context = SnowplowTracker::SelfDescribingJson.new(SURVEY_RESPONSE_SCHEMA_URL, data)
......
...@@ -17,7 +17,8 @@ RSpec.describe SurveyResponsesController do ...@@ -17,7 +17,8 @@ RSpec.describe SurveyResponsesController do
instance_id: 'foo', instance_id: 'foo',
response: 'response text', response: 'response text',
bla: 'bar', bla: 'bar',
show_invite_link: 'true' show_invite_link: 'true',
onboarding_progress: '4'
} }
end end
...@@ -43,11 +44,14 @@ RSpec.describe SurveyResponsesController do ...@@ -43,11 +44,14 @@ RSpec.describe SurveyResponsesController do
data: data:
{ {
survey_id: 123, survey_id: 123,
response: 'response text' response: 'response text',
onboarding_progress: 4
} }
} }
] ]
) )
match_snowplow_context_schema(schema_path: 'survey_response_schema', context: { response: 'response text', survey_id: 123, onboarding_progress: 4 } )
end end
end end
end end
......
...@@ -6,10 +6,8 @@ module Gitlab ...@@ -6,10 +6,8 @@ module Gitlab
module InProductMarketing module InProductMarketing
UnknownTrackError = Class.new(StandardError) UnknownTrackError = Class.new(StandardError)
TRACKS = [:create, :verify, :team, :trial].freeze
def self.for(track) def self.for(track)
raise UnknownTrackError unless TRACKS.include?(track) raise UnknownTrackError unless Namespaces::InProductMarketingEmailsService::TRACKS.key?(track)
"Gitlab::Email::Message::InProductMarketing::#{track.to_s.classify}".constantize "Gitlab::Email::Message::InProductMarketing::#{track.to_s.classify}".constantize
end end
......
...@@ -10,10 +10,11 @@ module Gitlab ...@@ -10,10 +10,11 @@ module Gitlab
attr_accessor :format attr_accessor :format
def initialize(group:, series:, format: :html) def initialize(group:, user:, series:, format: :html)
raise ArgumentError, "Only #{total_series} series available for this track." unless series.between?(0, total_series - 1) raise ArgumentError, "Only #{total_series} series available for this track." unless series.between?(0, total_series - 1)
@group = group @group = group
@user = user
@series = series @series = series
@format = format @format = format
end end
...@@ -103,11 +104,7 @@ module Gitlab ...@@ -103,11 +104,7 @@ module Gitlab
protected protected
attr_reader :group, :series attr_reader :group, :user, :series
def total_series
3
end
private private
...@@ -115,6 +112,10 @@ module Gitlab ...@@ -115,6 +112,10 @@ module Gitlab
self.class.name.demodulize.downcase.to_sym self.class.name.demodulize.downcase.to_sym
end end
def total_series
Namespaces::InProductMarketingEmailsService::TRACKS[track][:interval_days].size
end
def unsubscribe_com def unsubscribe_com
[ [
s_('InProductMarketing|If you no longer wish to receive marketing emails from us,'), s_('InProductMarketing|If you no longer wish to receive marketing emails from us,'),
......
# frozen_string_literal: true
module Gitlab
module Email
module Message
module InProductMarketing
class Experience < Base
include Gitlab::Utils::StrongMemoize
EASE_SCORE_SURVEY_ID = 1
def subject_line
s_('InProductMarketing|Do you have a minute?')
end
def tagline
end
def title
s_('InProductMarketing|We want your GitLab experience to be great')
end
def subtitle
s_('InProductMarketing|Take this 1-question survey!')
end
def body_line1
s_('InProductMarketing|%{strong_start}Overall, how difficult or easy was it to get started with GitLab?%{strong_end}').html_safe % strong_options
end
def body_line2
s_('InProductMarketing|Click on the number below that corresponds with your answer — 1 being very difficult, 5 being very easy.')
end
def cta_text
end
def feedback_link(rating)
params = {
onboarding_progress: onboarding_progress,
response: rating,
show_invite_link: show_invite_link,
survey_id: EASE_SCORE_SURVEY_ID
}
"#{Gitlab::COM_URL}/-/survey_responses?#{params.to_query}"
end
def feedback_ratings(rating)
[
s_('InProductMarketing|Very difficult'),
s_('InProductMarketing|Difficult'),
s_('InProductMarketing|Neutral'),
s_('InProductMarketing|Easy'),
s_('InProductMarketing|Very easy')
][rating - 1]
end
def feedback_thanks
s_('InProductMarketing|Feedback from users like you really improves our product. Thanks for your help!')
end
private
def onboarding_progress
strong_memoize(:onboarding_progress) do
group.onboarding_progress.number_of_completed_actions
end
end
def show_invite_link
strong_memoize(:show_invite_link) do
group.member_count > 1 && group.max_member_access_for_user(user) >= GroupMember::DEVELOPER && user.preferred_language == 'en'
end
end
end
end
end
end
end
...@@ -852,17 +852,16 @@ module Gitlab ...@@ -852,17 +852,16 @@ module Gitlab
sent_emails = count(Users::InProductMarketingEmail.group(:track, :series)) sent_emails = count(Users::InProductMarketingEmail.group(:track, :series))
clicked_emails = count(Users::InProductMarketingEmail.where.not(cta_clicked_at: nil).group(:track, :series)) clicked_emails = count(Users::InProductMarketingEmail.where.not(cta_clicked_at: nil).group(:track, :series))
series_amount = Namespaces::InProductMarketingEmailsService::INTERVAL_DAYS.count
Users::InProductMarketingEmail.tracks.keys.each_with_object({}) do |track, result| Users::InProductMarketingEmail.tracks.keys.each_with_object({}) do |track, result|
# rubocop: enable UsageData/LargeTable: # rubocop: enable UsageData/LargeTable:
series_amount = Namespaces::InProductMarketingEmailsService::TRACKS[track.to_sym][:interval_days].count
0.upto(series_amount - 1).map do |series| 0.upto(series_amount - 1).map do |series|
# When there is an error with the query and it's not the Hash we expect, we return what we got from `count`. # When there is an error with the query and it's not the Hash we expect, we return what we got from `count`.
sent_count = sent_emails.is_a?(Hash) ? sent_emails.fetch([track, series], 0) : sent_emails sent_count = sent_emails.is_a?(Hash) ? sent_emails.fetch([track, series], 0) : sent_emails
clicked_count = clicked_emails.is_a?(Hash) ? clicked_emails.fetch([track, series], 0) : clicked_emails clicked_count = clicked_emails.is_a?(Hash) ? clicked_emails.fetch([track, series], 0) : clicked_emails
result["in_product_marketing_email_#{track}_#{series}_sent"] = sent_count result["in_product_marketing_email_#{track}_#{series}_sent"] = sent_count
result["in_product_marketing_email_#{track}_#{series}_cta_clicked"] = clicked_count result["in_product_marketing_email_#{track}_#{series}_cta_clicked"] = clicked_count unless track == 'experience'
end end
end end
end end
......
...@@ -16911,6 +16911,9 @@ msgstr "" ...@@ -16911,6 +16911,9 @@ msgstr ""
msgid "InProductMarketing|%{strong_start}Multiple approval roles%{strong_end} — including code owners and required merge approvals" msgid "InProductMarketing|%{strong_start}Multiple approval roles%{strong_end} — including code owners and required merge approvals"
msgstr "" msgstr ""
msgid "InProductMarketing|%{strong_start}Overall, how difficult or easy was it to get started with GitLab?%{strong_end}"
msgstr ""
msgid "InProductMarketing|*GitLab*, noun: a synonym for efficient teams" msgid "InProductMarketing|*GitLab*, noun: a synonym for efficient teams"
msgstr "" msgstr ""
...@@ -16944,6 +16947,9 @@ msgstr "" ...@@ -16944,6 +16947,9 @@ 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." 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 "" msgstr ""
msgid "InProductMarketing|Click on the number below that corresponds with your answer — 1 being very difficult, 5 being very easy."
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 Ultimate and enable these features in less than 5 minutes with no credit card required." 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 Ultimate and enable these features in less than 5 minutes with no credit card required."
msgstr "" msgstr ""
...@@ -16956,9 +16962,18 @@ msgstr "" ...@@ -16956,9 +16962,18 @@ msgstr ""
msgid "InProductMarketing|Did you know teams that use GitLab are far more efficient?" msgid "InProductMarketing|Did you know teams that use GitLab are far more efficient?"
msgstr "" msgstr ""
msgid "InProductMarketing|Difficult"
msgstr ""
msgid "InProductMarketing|Dig in and create a project and a repo" msgid "InProductMarketing|Dig in and create a project and a repo"
msgstr "" msgstr ""
msgid "InProductMarketing|Do you have a minute?"
msgstr ""
msgid "InProductMarketing|Easy"
msgstr ""
msgid "InProductMarketing|Explore GitLab CI/CD" msgid "InProductMarketing|Explore GitLab CI/CD"
msgstr "" msgstr ""
...@@ -16971,6 +16986,9 @@ msgstr "" ...@@ -16971,6 +16986,9 @@ msgstr ""
msgid "InProductMarketing|Facebook" msgid "InProductMarketing|Facebook"
msgstr "" msgstr ""
msgid "InProductMarketing|Feedback from users like you really improves our product. Thanks for your help!"
msgstr ""
msgid "InProductMarketing|Feel the need for speed?" msgid "InProductMarketing|Feel the need for speed?"
msgstr "" msgstr ""
...@@ -17097,6 +17115,9 @@ msgstr "" ...@@ -17097,6 +17115,9 @@ msgstr ""
msgid "InProductMarketing|Need an alternative to importing?" msgid "InProductMarketing|Need an alternative to importing?"
msgstr "" msgstr ""
msgid "InProductMarketing|Neutral"
msgstr ""
msgid "InProductMarketing|Our tool brings all the things together" msgid "InProductMarketing|Our tool brings all the things together"
msgstr "" msgstr ""
...@@ -17136,6 +17157,9 @@ msgstr "" ...@@ -17136,6 +17157,9 @@ 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." 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 "" msgstr ""
msgid "InProductMarketing|Take this 1-question survey!"
msgstr ""
msgid "InProductMarketing|Take your first steps with GitLab" msgid "InProductMarketing|Take your first steps with GitLab"
msgstr "" msgstr ""
...@@ -17190,9 +17214,18 @@ msgstr "" ...@@ -17190,9 +17214,18 @@ msgstr ""
msgid "InProductMarketing|Use GitLab CI/CD" msgid "InProductMarketing|Use GitLab CI/CD"
msgstr "" msgstr ""
msgid "InProductMarketing|Very difficult"
msgstr ""
msgid "InProductMarketing|Very easy"
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 Ultimate and your teams will be on it from day one." 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 Ultimate and your teams will be on it from day one."
msgstr "" msgstr ""
msgid "InProductMarketing|We want your GitLab experience to be great"
msgstr ""
msgid "InProductMarketing|What does our value stream timeline look like from product to development to review and production?" msgid "InProductMarketing|What does our value stream timeline look like from product to development to review and production?"
msgstr "" msgstr ""
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
"self": { "self": {
"vendor": "com.gitlab", "vendor": "com.gitlab",
"name": "survey_response", "name": "survey_response",
"version": "1-0-0", "version": "1-0-1",
"format": "jsonschema" "format": "jsonschema"
}, },
"type": "object", "type": "object",
...@@ -47,6 +47,12 @@ ...@@ -47,6 +47,12 @@
"description": "Username", "description": "Username",
"type": ["string", "null"], "type": ["string", "null"],
"maxLength": 255 "maxLength": 255
},
"onboarding_progress": {
"description": "Onboarding progress",
"type": ["integer", "null"],
"minimum": 0,
"maximum": 2147483647
} }
} }
} }
...@@ -4,12 +4,13 @@ require 'spec_helper' ...@@ -4,12 +4,13 @@ require 'spec_helper'
RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do
let_it_be(:group) { build(:group) } let_it_be(:group) { build(:group) }
let_it_be(:user) { build(:user) }
let(:series) { 0 } let(:series) { 0 }
let(:test_class) { Gitlab::Email::Message::InProductMarketing::Create } let(:test_class) { Gitlab::Email::Message::InProductMarketing::Create }
describe 'initialize' do describe 'initialize' do
subject { test_class.new(group: group, series: series) } subject { test_class.new(group: group, user: user, series: series) }
context 'when series does not exist' do context 'when series does not exist' do
let(:series) { 3 } let(:series) { 3 }
...@@ -29,13 +30,13 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do ...@@ -29,13 +30,13 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do
end end
describe '#logo_path' do describe '#logo_path' do
subject { test_class.new(group: group, series: series).logo_path } subject { test_class.new(group: group, user: user, series: series).logo_path }
it { is_expected.to eq('mailers/in_product_marketing/create-0.png') } it { is_expected.to eq('mailers/in_product_marketing/create-0.png') }
end end
describe '#unsubscribe' do describe '#unsubscribe' do
subject { test_class.new(group: group, series: series).unsubscribe } subject { test_class.new(group: group, user: user, series: series).unsubscribe }
before do before do
allow(Gitlab).to receive(:com?).and_return(is_gitlab_com) allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
...@@ -55,7 +56,7 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do ...@@ -55,7 +56,7 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do
end end
describe '#cta_link' do describe '#cta_link' do
subject(:cta_link) { test_class.new(group: group, series: series).cta_link } subject(:cta_link) { test_class.new(group: group, user: user, series: series).cta_link }
it 'renders link' do it 'renders link' do
expect(CGI.unescapeHTML(cta_link)).to include(Gitlab::Routing.url_helpers.group_email_campaigns_url(group, track: :create, series: series)) expect(CGI.unescapeHTML(cta_link)).to include(Gitlab::Routing.url_helpers.group_email_campaigns_url(group, track: :create, series: series))
...@@ -63,7 +64,7 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do ...@@ -63,7 +64,7 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do
end end
describe '#progress' do describe '#progress' do
subject { test_class.new(group: group, series: series).progress } subject { test_class.new(group: group, user: user, series: series).progress }
before do before do
allow(Gitlab).to receive(:com?).and_return(is_gitlab_com) allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
......
...@@ -6,8 +6,9 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Create do ...@@ -6,8 +6,9 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Create do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let_it_be(:group) { build(:group) } let_it_be(:group) { build(:group) }
let_it_be(:user) { build(:user) }
subject(:message) { described_class.new(group: group, series: series)} subject(:message) { described_class.new(group: group, user: user, series: series)}
describe "public methods" do describe "public methods" do
where(series: [0, 1, 2]) where(series: [0, 1, 2])
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Email::Message::InProductMarketing::Experience do
let_it_be(:group) { build(:group) }
let_it_be(:user) { build(:user) }
subject(:message) { described_class.new(group: group, user: user, series: series)}
describe 'public methods' do
context 'with series 0' do
let(:series) { 0 }
it 'returns value for series', :aggregate_failures do
expect(message.subject_line).to be_present
expect(message.tagline).to be_nil
expect(message.title).to be_present
expect(message.subtitle).to be_present
expect(message.body_line1).to be_present
expect(message.body_line2).to be_present
expect(message.cta_text).to be_nil
end
describe '#feedback_link' do
let(:member_count) { 2 }
let(:user_access) { GroupMember::DEVELOPER }
let(:preferred_language) { 'en' }
before do
allow(message).to receive(:onboarding_progress).and_return(1)
allow(group).to receive(:member_count).and_return(member_count)
allow(group).to receive(:max_member_access_for_user).and_return(user_access)
allow(user).to receive(:preferred_language).and_return(preferred_language)
end
subject do
uri = URI.parse(message.feedback_link(1))
Rack::Utils.parse_query(uri.query).with_indifferent_access[:show_invite_link]
end
it { is_expected.to eq('true') }
context 'with only one member' do
let(:member_count) { 1 }
it { is_expected.to eq('false') }
end
context 'with less than developer access' do
let(:user_access) { GroupMember::GUEST }
it { is_expected.to eq('false') }
end
context 'with preferred language other than English' do
let(:preferred_language) { 'nl' }
it { is_expected.to eq('false') }
end
end
end
end
end
...@@ -6,8 +6,9 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Team do ...@@ -6,8 +6,9 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Team do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let_it_be(:group) { build(:group) } let_it_be(:group) { build(:group) }
let_it_be(:user) { build(:user) }
subject(:message) { described_class.new(group: group, series: series)} subject(:message) { described_class.new(group: group, user: user, series: series)}
describe "public methods" do describe "public methods" do
where(series: [0, 1]) where(series: [0, 1])
......
...@@ -6,8 +6,9 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Trial do ...@@ -6,8 +6,9 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Trial do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let_it_be(:group) { build(:group) } let_it_be(:group) { build(:group) }
let_it_be(:user) { build(:user) }
subject(:message) { described_class.new(group: group, series: series)} subject(:message) { described_class.new(group: group, user: user, series: series)}
describe "public methods" do describe "public methods" do
where(series: [0, 1, 2]) where(series: [0, 1, 2])
......
...@@ -4,8 +4,9 @@ require 'spec_helper' ...@@ -4,8 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Email::Message::InProductMarketing::Verify do RSpec.describe Gitlab::Email::Message::InProductMarketing::Verify do
let_it_be(:group) { build(:group) } let_it_be(:group) { build(:group) }
let_it_be(:user) { build(:user) }
subject(:message) { described_class.new(group: group, series: series)} subject(:message) { described_class.new(group: group, user: user, series: series)}
describe "public methods" do describe "public methods" do
context 'with series 0' do context 'with series 0' do
......
...@@ -1500,7 +1500,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do ...@@ -1500,7 +1500,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
"in_product_marketing_email_team_1_sent" => -1, "in_product_marketing_email_team_1_sent" => -1,
"in_product_marketing_email_team_1_cta_clicked" => -1, "in_product_marketing_email_team_1_cta_clicked" => -1,
"in_product_marketing_email_team_2_sent" => -1, "in_product_marketing_email_team_2_sent" => -1,
"in_product_marketing_email_team_2_cta_clicked" => -1 "in_product_marketing_email_team_2_cta_clicked" => -1,
"in_product_marketing_email_experience_0_sent" => -1
} }
expect(subject).to eq(expected_data) expect(subject).to eq(expected_data)
...@@ -1538,7 +1539,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do ...@@ -1538,7 +1539,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
"in_product_marketing_email_team_1_sent" => 0, "in_product_marketing_email_team_1_sent" => 0,
"in_product_marketing_email_team_1_cta_clicked" => 0, "in_product_marketing_email_team_1_cta_clicked" => 0,
"in_product_marketing_email_team_2_sent" => 0, "in_product_marketing_email_team_2_sent" => 0,
"in_product_marketing_email_team_2_cta_clicked" => 0 "in_product_marketing_email_team_2_cta_clicked" => 0,
"in_product_marketing_email_experience_0_sent" => 0
} }
expect(subject).to eq(expected_data) expect(subject).to eq(expected_data)
......
...@@ -9,6 +9,8 @@ RSpec.describe Emails::InProductMarketing do ...@@ -9,6 +9,8 @@ RSpec.describe Emails::InProductMarketing do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let!(:onboarding_progress) { create(:onboarding_progress, namespace: group) }
describe '#in_product_marketing_email' do describe '#in_product_marketing_email' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
...@@ -57,19 +59,25 @@ RSpec.describe Emails::InProductMarketing do ...@@ -57,19 +59,25 @@ RSpec.describe Emails::InProductMarketing do
:team | 0 :team | 0
:team | 1 :team | 1
:team | 2 :team | 2
:experience | 0
end end
with_them do with_them do
it 'has the correct subject and content' do it 'has the correct subject and content' do
message = Gitlab::Email::Message::InProductMarketing.for(track).new(group: group, series: series) message = Gitlab::Email::Message::InProductMarketing.for(track).new(group: group, user: user, series: series)
aggregate_failures do aggregate_failures do
is_expected.to have_subject(message.subject_line) is_expected.to have_subject(message.subject_line)
is_expected.to have_body_text(message.title) is_expected.to have_body_text(message.title)
is_expected.to have_body_text(message.subtitle) is_expected.to have_body_text(message.subtitle)
if track == :experience
is_expected.to have_body_text(CGI.unescapeHTML(message.feedback_link(1)))
else
is_expected.to have_body_text(CGI.unescapeHTML(message.cta_link)) is_expected.to have_body_text(CGI.unescapeHTML(message.cta_link))
end end
end end
end end
end end
end
end end
...@@ -211,4 +211,26 @@ RSpec.describe OnboardingProgress do ...@@ -211,4 +211,26 @@ RSpec.describe OnboardingProgress do
it { is_expected.to eq(:subscription_created_at) } it { is_expected.to eq(:subscription_created_at) }
end end
describe '#number_of_completed_actions' do
subject { build(:onboarding_progress, actions.map { |x| { x => Time.current } }.inject(:merge)).number_of_completed_actions }
context '0 completed actions' do
let(:actions) { [:created_at, :updated_at] }
it { is_expected.to eq(0) }
end
context '1 completed action' do
let(:actions) { [:created_at, :subscription_created_at] }
it { is_expected.to eq(1) }
end
context '2 completed actions' do
let(:actions) { [:subscription_created_at, :git_write_at] }
it { is_expected.to eq(2) }
end
end
end end
...@@ -9,10 +9,11 @@ RSpec.describe Groups::EmailCampaignsController do ...@@ -9,10 +9,11 @@ RSpec.describe Groups::EmailCampaignsController do
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) } let_it_be(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:track) { 'create' } let(:track) { 'create' }
let(:series) { '0' } let(:series) { '0' }
let(:schema) { described_class::EMAIL_CAMPAIGNS_SCHEMA_URL } let(:schema) { described_class::EMAIL_CAMPAIGNS_SCHEMA_URL }
let(:subject_line_text) { Gitlab::Email::Message::InProductMarketing.for(track.to_sym).new(group: group, series: series.to_i).subject_line } let(:subject_line_text) { Gitlab::Email::Message::InProductMarketing.for(track.to_sym).new(group: group, user: user, series: series.to_i).subject_line }
let(:data) do let(:data) do
{ {
namespace_id: group.id, namespace_id: group.id,
...@@ -91,7 +92,7 @@ RSpec.describe Groups::EmailCampaignsController do ...@@ -91,7 +92,7 @@ RSpec.describe Groups::EmailCampaignsController do
describe 'track parameter' do describe 'track parameter' do
context 'when valid' do context 'when valid' do
where(track: Namespaces::InProductMarketingEmailsService::TRACKS.keys) where(track: Namespaces::InProductMarketingEmailsService::TRACKS.keys.without(:experience))
with_them do with_them do
it_behaves_like 'track and redirect' it_behaves_like 'track and redirect'
...@@ -109,7 +110,7 @@ RSpec.describe Groups::EmailCampaignsController do ...@@ -109,7 +110,7 @@ RSpec.describe Groups::EmailCampaignsController do
describe 'series parameter' do describe 'series parameter' do
context 'when valid' do context 'when valid' do
where(series: (0..Namespaces::InProductMarketingEmailsService::INTERVAL_DAYS.length - 1).to_a) where(series: (0..Namespaces::InProductMarketingEmailsService::TRACKS[:create][:interval_days].length - 1).to_a)
with_them do with_them do
it_behaves_like 'track and redirect' it_behaves_like 'track and redirect'
...@@ -117,7 +118,7 @@ RSpec.describe Groups::EmailCampaignsController do ...@@ -117,7 +118,7 @@ RSpec.describe Groups::EmailCampaignsController do
end end
context 'when invalid' do context 'when invalid' do
where(series: [-1, nil, Namespaces::InProductMarketingEmailsService::INTERVAL_DAYS.length]) where(series: [-1, nil, Namespaces::InProductMarketingEmailsService::TRACKS[:create][:interval_days].length])
with_them do with_them do
it_behaves_like 'no track and 404' it_behaves_like 'no track and 404'
......
...@@ -53,10 +53,11 @@ RSpec.describe Namespaces::InProductMarketingEmailsService, '#execute' do ...@@ -53,10 +53,11 @@ RSpec.describe Namespaces::InProductMarketingEmailsService, '#execute' do
:team | 1 | { created_at: frozen_time - 2.days, git_write_at: frozen_time - 2.days, pipeline_created_at: frozen_time - 2.days, trial_started_at: frozen_time - 2.days } :team | 1 | { created_at: frozen_time - 2.days, git_write_at: frozen_time - 2.days, pipeline_created_at: frozen_time - 2.days, trial_started_at: frozen_time - 2.days }
:team | 5 | { created_at: frozen_time - 6.days, git_write_at: frozen_time - 6.days, pipeline_created_at: frozen_time - 6.days, trial_started_at: frozen_time - 6.days } :team | 5 | { created_at: frozen_time - 6.days, git_write_at: frozen_time - 6.days, pipeline_created_at: frozen_time - 6.days, trial_started_at: frozen_time - 6.days }
:team | 10 | { created_at: frozen_time - 11.days, git_write_at: frozen_time - 11.days, pipeline_created_at: frozen_time - 11.days, trial_started_at: frozen_time - 11.days } :team | 10 | { created_at: frozen_time - 11.days, git_write_at: frozen_time - 11.days, pipeline_created_at: frozen_time - 11.days, trial_started_at: frozen_time - 11.days }
:experience | 30 | { created_at: frozen_time - 31.days, git_write_at: frozen_time - 31.days }
end end
with_them do with_them do
it { is_expected.to send_in_product_marketing_email(user.id, group.id, track, described_class::INTERVAL_DAYS.index(interval)) } it { is_expected.to send_in_product_marketing_email(user.id, group.id, track, described_class::TRACKS[track][:interval_days].index(interval)) }
end end
end end
...@@ -235,7 +236,7 @@ RSpec.describe Namespaces::InProductMarketingEmailsService, '#execute' do ...@@ -235,7 +236,7 @@ RSpec.describe Namespaces::InProductMarketingEmailsService, '#execute' do
let(:track) { :foo } let(:track) { :foo }
before do before do
stub_const("#{described_class}::TRACKS", { bar: :git_write }) stub_const("#{described_class}::TRACKS", { bar: {} })
end end
it { expect { subject }.to raise_error(ArgumentError, 'Track foo not defined') } it { expect { subject }.to raise_error(ArgumentError, 'Track foo not defined') }
......
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