Commit f5ccac0d authored by Nicolas Dular's avatar Nicolas Dular

Add experiment for pipeline empty state

This adds an experiment where we change the text onthe pipeline zero
state page (when there is no pipeline created yet).

We want to know if there is an overall increase in pipeline creation,
that's why we also track the pipelines created.
parent d06e9696
<script>
import { GlButton } from '@gitlab/ui';
import { isExperimentEnabled } from '~/lib/utils/experimentation';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
export default {
i18n: {
control: {
infoMessage: s__(`Pipelines|Continuous Integration can help
catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver
code to your product environment.`),
buttonMessage: s__('Pipelines|Get started with Pipelines'),
},
experiment: {
infoMessage: s__(`Pipelines|GitLab CI/CD can automatically build,
test, and deploy your code. Let GitLab take care of time
consuming tasks, so you can spend more time creating.`),
buttonMessage: s__('Pipelines|Get started with CI/CD'),
},
},
name: 'PipelinesEmptyState',
components: {
GlButton,
......@@ -20,6 +38,23 @@ export default {
required: true,
},
},
mounted() {
this.track('viewed');
},
methods: {
track(action) {
if (!gon.tracking_data) {
return;
}
const { category, value, label, property } = gon.tracking_data;
Tracking.event(category, action, { value, label, property });
},
isExperimentEnabled() {
return isExperimentEnabled('pipelinesEmptyState');
},
},
};
</script>
<template>
......@@ -29,18 +64,16 @@ export default {
</div>
<div class="col-12">
<div class="gl-text-content">
<div class="text-content">
<template v-if="canSetCi">
<h4 class="gl-text-center" data-testid="header-text">
<h4 data-testid="header-text" class="gl-text-center">
{{ s__('Pipelines|Build with confidence') }}
</h4>
<p data-testid="info-text">
{{
s__(`Pipelines|Continuous Integration can help
catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver
code to your product environment.`)
isExperimentEnabled()
? $options.i18n.experiment.infoMessage
: $options.i18n.control.infoMessage
}}
</p>
......@@ -50,8 +83,13 @@ export default {
variant="info"
category="primary"
data-testid="get-started-pipelines"
@click="track('documentation_clicked')"
>
{{ s__('Pipelines|Get started with Pipelines') }}
{{
isExperimentEnabled()
? $options.i18n.experiment.buttonMessage
: $options.i18n.control.buttonMessage
}}
</gl-button>
</div>
</template>
......
......@@ -21,6 +21,7 @@ class Projects::PipelinesController < Projects::ApplicationController
push_frontend_feature_flag(:new_pipeline_form_prefilled_vars, project, type: :development, default_enabled: true)
end
before_action :ensure_pipeline, only: [:show]
before_action :push_experiment_to_gon, only: :index, if: :html_request?
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596
before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? }
......@@ -45,7 +46,11 @@ class Projects::PipelinesController < Projects::ApplicationController
@pipelines_count = limited_pipelines_count(project)
respond_to do |format|
format.html
format.html do
record_empty_pipeline_experiment
render :index
end
format.json do
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
......@@ -313,6 +318,20 @@ class Projects::PipelinesController < Projects::ApplicationController
def index_params
params.permit(:scope, :username, :ref, :status)
end
def record_empty_pipeline_experiment
return unless @pipelines_count.to_i == 0
return if helpers.has_gitlab_ci?(@project)
record_experiment_user(:pipelines_empty_state)
end
def push_experiment_to_gon
return unless current_user
push_frontend_experiment(:pipelines_empty_state, subject: current_user)
frontend_experimentation_tracking_data(:pipelines_empty_state, 'view', project.namespace_id, subject: current_user)
end
end
Projects::PipelinesController.prepend_if_ee('EE::Projects::PipelinesController')
......@@ -26,6 +26,10 @@ module Ci
_("%{message} showing first %{warnings_displayed}") % { message: message, warnings_displayed: MAX_LIMIT }
end
def has_gitlab_ci?(project)
project.has_ci? && project.builds_enabled?
end
private
def warning_markdown(pipeline)
......
......@@ -121,6 +121,7 @@ module Ci
def record_conversion_event
Experiments::RecordConversionEventWorker.perform_async(:ci_syntax_templates, current_user.id)
Experiments::RecordConversionEventWorker.perform_async(:pipelines_empty_state, current_user.id)
end
def extra_options(content: nil, dry_run: false)
......
......@@ -17,4 +17,4 @@
"new-pipeline-path" => can?(current_user, :create_pipeline, @project) && new_project_pipeline_path(@project),
"ci-lint-path" => can?(current_user, :create_pipeline, @project) && project_ci_lint_path(@project),
"reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project) ,
"has-gitlab-ci" => (@project.has_ci? && @project.builds_enabled?).to_s } }
"has-gitlab-ci" => has_gitlab_ci?(@project).to_s } }
......@@ -93,6 +93,9 @@ module Gitlab
},
ci_syntax_templates: {
tracking_category: 'Growth::Activation::Experiment::CiSyntaxTemplates'
},
pipelines_empty_state: {
tracking_category: 'Growth::Activation::Experiment::PipelinesEmptyState'
}
}.freeze
......
......@@ -20458,9 +20458,15 @@ msgstr ""
msgid "Pipelines|Editor"
msgstr ""
msgid "Pipelines|Get started with CI/CD"
msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
msgid "Pipelines|GitLab CI/CD can automatically build, test, and deploy your code. Let GitLab take care of time consuming tasks, so you can spend more time creating."
msgstr ""
msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
......
......@@ -272,6 +272,72 @@ RSpec.describe Projects::PipelinesController do
end
end
describe 'GET #index' do
subject(:request) { get :index, params: { namespace_id: project.namespace, project_id: project } }
context 'experiment not active' do
it 'does not push tracking_data to gon' do
request
expect(Gon.tracking_data).to be_nil
end
it 'does not record experiment_user' do
expect { request }.not_to change(ExperimentUser, :count)
end
end
context 'when experiment active' do
before do
stub_experiment(pipelines_empty_state: true)
stub_experiment_for_subject(pipelines_empty_state: true)
end
it 'pushes tracking_data to Gon' do
request
expect(Gon.experiments["pipelinesEmptyState"]).to eq(true)
expect(Gon.tracking_data).to match(
{
category: 'Growth::Activation::Experiment::PipelinesEmptyState',
action: 'view',
label: anything,
property: 'experimental_group',
value: anything
}
)
end
context 'no pipelines created an no CI set up' do
before do
stub_application_setting(auto_devops_enabled: false)
end
it 'records experiment_user' do
expect { request }.to change(ExperimentUser, :count).by(1)
end
end
context 'CI set up' do
it 'does not record experiment_user' do
expect { request }.not_to change(ExperimentUser, :count)
end
end
context 'pipelines created' do
let!(:pipeline) { create(:ci_pipeline, project: project) }
before do
stub_application_setting(auto_devops_enabled: false)
end
it 'does not record experiment_user' do
expect { request }.not_to change(ExperimentUser, :count)
end
end
end
end
describe 'GET show.json' do
let(:pipeline) { create(:ci_pipeline, project: project) }
......
import { shallowMount } from '@vue/test-utils';
import { withGonExperiment } from 'helpers/experimentation_helper';
import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
import Tracking from '~/tracking';
describe('Pipelines Empty State', () => {
let wrapper;
......@@ -38,15 +40,104 @@ describe('Pipelines Empty State', () => {
expect(findGetStartedButton().attributes('href')).toBe('foo');
});
it('should render empty state information', () => {
expect(findInfoText()).toContain(
'Continuous Integration can help catch bugs by running your tests automatically',
'while Continuous Deployment can help you deliver code to your product environment',
);
describe('when in control group', () => {
it('should render empty state information', () => {
expect(findInfoText()).toContain(
'Continuous Integration can help catch bugs by running your tests automatically',
'while Continuous Deployment can help you deliver code to your product environment',
);
});
it('should render a button', () => {
expect(findGetStartedButton().text()).toBe('Get started with Pipelines');
});
});
describe('when in experiment group', () => {
withGonExperiment('pipelinesEmptyState');
beforeEach(() => {
createWrapper();
});
it('should render empty state information', () => {
expect(findInfoText()).toContain(
'GitLab CI/CD can automatically build, test, and deploy your code. Let GitLab take care of time',
'consuming tasks, so you can spend more time creating',
);
});
it('should render button text', () => {
expect(findGetStartedButton().text()).toBe('Get started with CI/CD');
});
});
it('should render a button', () => {
expect(findGetStartedButton().text()).toBe('Get started with Pipelines');
describe('tracking', () => {
let origGon;
describe('when data is set', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event').mockImplementation(() => {});
origGon = window.gon;
window.gon = {
tracking_data: {
category: 'Growth::Activation::Experiment::PipelinesEmptyState',
value: 1,
property: 'experimental_group',
label: 'label',
},
};
createWrapper();
});
afterEach(() => {
window.gon = origGon;
});
it('tracks when mounted', () => {
expect(Tracking.event).toHaveBeenCalledWith(
'Growth::Activation::Experiment::PipelinesEmptyState',
'viewed',
{
value: 1,
label: 'label',
property: 'experimental_group',
},
);
});
it('tracks when button is clicked', () => {
findGetStartedButton().vm.$emit('click');
expect(Tracking.event).toHaveBeenCalledWith(
'Growth::Activation::Experiment::PipelinesEmptyState',
'documentation_clicked',
{
value: 1,
label: 'label',
property: 'experimental_group',
},
);
});
});
describe('when no data is defined', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event').mockImplementation(() => {});
createWrapper();
});
it('does not track on view', () => {
expect(Tracking.event).not.toHaveBeenCalled();
});
it('does not track when button is clicked', () => {
findGetStartedButton().vm.$emit('click');
expect(Tracking.event).not.toHaveBeenCalled();
});
});
});
});
});
......@@ -52,4 +52,23 @@ RSpec.describe Ci::PipelinesHelper do
end
end
end
describe 'has_gitlab_ci?' do
using RSpec::Parameterized::TableSyntax
subject(:has_gitlab_ci?) { helper.has_gitlab_ci?(project) }
let(:project) { double(:project, has_ci?: has_ci?, builds_enabled?: builds_enabled?) }
where(:builds_enabled?, :has_ci?, :result) do
true | true | true
true | false | false
false | true | false
false | false | false
end
with_them do
it { expect(has_gitlab_ci?).to eq(result) }
end
end
end
......@@ -94,6 +94,7 @@ RSpec.describe Ci::CreatePipelineService do
describe 'recording a conversion event' do
it 'schedules a record conversion event worker' do
expect(Experiments::RecordConversionEventWorker).to receive(:perform_async).with(:ci_syntax_templates, user.id)
expect(Experiments::RecordConversionEventWorker).to receive(:perform_async).with(:pipelines_empty_state, user.id)
pipeline
end
......
......@@ -12,10 +12,14 @@ RSpec.describe Experiments::RecordConversionEventWorker, '#perform' do
context 'when the experiment is active' do
let(:experiment_active) { true }
it 'records the event' do
expect(Experiment).to receive(:record_conversion_event).with(:experiment_key, 1234)
include_examples 'an idempotent worker' do
subject { perform }
perform
it 'records the event' do
expect(Experiment).to receive(:record_conversion_event).with(:experiment_key, 1234)
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