Commit d8d007da authored by Jay Swain's avatar Jay Swain

Add "New Repo" experiment to test familiarity

This experiment is testing the hypothesis that people coming to
GitLab from other products like GitHub and Bitbucket are familiar
with the "repository" term and are looking for it in our navigation.

By not having it, we are causing confusion and cognitive load for
users which may ultimately cause them to fail to adopt Create with
GitLab.

part of:
https://gitlab.com/gitlab-org/gitlab/-/issues/285286
parent 4db43ced
<script> <script>
/* eslint-disable vue/no-v-html */ /* eslint-disable vue/no-v-html */
import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { experiment } from '~/experimentation/utils';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { NEW_REPO_EXPERIMENT } from '../constants';
import blankProjectIllustration from '../illustrations/blank-project.svg'; import blankProjectIllustration from '../illustrations/blank-project.svg';
import ciCdProjectIllustration from '../illustrations/ci-cd-project.svg'; import ciCdProjectIllustration from '../illustrations/ci-cd-project.svg';
import createFromTemplateIllustration from '../illustrations/create-from-template.svg'; import createFromTemplateIllustration from '../illustrations/create-from-template.svg';
...@@ -13,8 +14,10 @@ import WelcomePage from './welcome.vue'; ...@@ -13,8 +14,10 @@ import WelcomePage from './welcome.vue';
const BLANK_PANEL = 'blank_project'; const BLANK_PANEL = 'blank_project';
const CI_CD_PANEL = 'cicd_for_external_repo'; const CI_CD_PANEL = 'cicd_for_external_repo';
const LAST_ACTIVE_TAB_KEY = 'new_project_last_active_tab'; const LAST_ACTIVE_TAB_KEY = 'new_project_last_active_tab';
const PANELS = [ const PANELS = [
{ {
key: 'blank',
name: BLANK_PANEL, name: BLANK_PANEL,
selector: '#blank-project-pane', selector: '#blank-project-pane',
title: s__('ProjectsNew|Create blank project'), title: s__('ProjectsNew|Create blank project'),
...@@ -24,6 +27,7 @@ const PANELS = [ ...@@ -24,6 +27,7 @@ const PANELS = [
illustration: blankProjectIllustration, illustration: blankProjectIllustration,
}, },
{ {
key: 'template',
name: 'create_from_template', name: 'create_from_template',
selector: '#create-from-template-pane', selector: '#create-from-template-pane',
title: s__('ProjectsNew|Create from template'), title: s__('ProjectsNew|Create from template'),
...@@ -33,6 +37,7 @@ const PANELS = [ ...@@ -33,6 +37,7 @@ const PANELS = [
illustration: createFromTemplateIllustration, illustration: createFromTemplateIllustration,
}, },
{ {
key: 'import',
name: 'import_project', name: 'import_project',
selector: '#import-project-pane', selector: '#import-project-pane',
title: s__('ProjectsNew|Import project'), title: s__('ProjectsNew|Import project'),
...@@ -42,6 +47,7 @@ const PANELS = [ ...@@ -42,6 +47,7 @@ const PANELS = [
illustration: importProjectIllustration, illustration: importProjectIllustration,
}, },
{ {
key: 'ci',
name: CI_CD_PANEL, name: CI_CD_PANEL,
selector: '#ci-cd-project-pane', selector: '#ci-cd-project-pane',
title: s__('ProjectsNew|Run CI/CD for external repository'), title: s__('ProjectsNew|Run CI/CD for external repository'),
...@@ -86,11 +92,27 @@ export default { ...@@ -86,11 +92,27 @@ export default {
computed: { computed: {
availablePanels() { availablePanels() {
const PANEL_TITLES = experiment(NEW_REPO_EXPERIMENT, {
use: () => ({
blank: s__('ProjectsNew|Create blank project'),
import: s__('ProjectsNew|Import project'),
}),
try: () => ({
blank: s__('ProjectsNew|Create blank project/repository'),
import: s__('ProjectsNew|Import project/repository'),
}),
});
const updatedPanels = PANELS.map(({ key, title, ...el }) => ({
...el,
title: PANEL_TITLES[key] !== undefined ? PANEL_TITLES[key] : title,
}));
if (this.isCiCdAvailable) { if (this.isCiCdAvailable) {
return PANELS; return updatedPanels;
} }
return PANELS.filter((p) => p.name !== CI_CD_PANEL); return updatedPanels.filter((p) => p.name !== CI_CD_PANEL);
}, },
activePanel() { activePanel() {
......
<script> <script>
/* eslint-disable vue/no-v-html */ /* eslint-disable vue/no-v-html */
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { NEW_REPO_EXPERIMENT } from '../constants';
import NewProjectPushTipPopover from './new_project_push_tip_popover.vue'; import NewProjectPushTipPopover from './new_project_push_tip_popover.vue';
const trackingMixin = Tracking.mixin(gon.tracking_data); const trackingMixin = Tracking.mixin({ ...gon.tracking_data, experiment: NEW_REPO_EXPERIMENT });
export default { export default {
components: { components: {
......
...@@ -74,6 +74,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -74,6 +74,7 @@ class ProjectsController < Projects::ApplicationController
@project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute @project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute
if @project.saved? if @project.saved?
experiment(:new_repo, user: current_user).track(:project_created)
experiment(:new_project_readme, actor: current_user).track( experiment(:new_project_readme, actor: current_user).track(
:created, :created,
property: active_new_project_tab, property: active_new_project_tab,
......
...@@ -52,6 +52,8 @@ ...@@ -52,6 +52,8 @@
= stylesheet_link_tag 'performance_bar' if performance_bar_enabled? = stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
-# Rendering this above Gon, to use in JS later
= render 'layouts/header/new_repo_experiment'
= Gon::Base.render_data(nonce: content_security_policy_nonce) = Gon::Base.render_data(nonce: content_security_policy_nonce)
= javascript_include_tag locale_path unless I18n.locale == :en = javascript_include_tag locale_path unless I18n.locale == :en
......
%li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown", track_value: "" } } %li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown", track_experiment: "new_repo" } }
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", id: "js-onboarding-new-project-link", title: _("New..."), ref: 'tooltip', aria: { label: _("New...") }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", id: "js-onboarding-new-project-link", title: _("New..."), ref: 'tooltip', aria: { label: _("New...") }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do
= sprite_icon('plus-square') = sprite_icon('plus-square')
= sprite_icon('chevron-down', css_class: 'caret-down') = sprite_icon('chevron-down', css_class: 'caret-down')
...@@ -37,8 +37,7 @@ ...@@ -37,8 +37,7 @@
= render 'layouts/header/project_invite_members_new_dropdown_item' = render 'layouts/header/project_invite_members_new_dropdown_item'
%li.divider %li.divider
%li.dropdown-bold-header GitLab %li.dropdown-bold-header GitLab
- if current_user.can_create_project? = content_for :new_repo_experiment
%li= link_to _('New project'), new_project_path, class: 'qa-global-new-project-link'
- if current_user.can_create_group? - if current_user.can_create_group?
%li= link_to _('New group'), new_group_path %li= link_to _('New group'), new_group_path
- if current_user.can?(:create_snippet) - if current_user.can?(:create_snippet)
......
- content_for :new_repo_experiment do
- if current_user&.can_create_project?
- experiment(:new_repo, user: current_user) do |e|
- e.use do
%li= link_to _('New project'), new_project_path, class: 'qa-global-new-project-link', data: { track_experiment: 'new_repo', track_event: 'click_link', track_label: 'plus_menu_dropdown' }
- e.try do
%li= link_to _('New project/repository'), new_project_path, class: 'qa-global-new-project-link', data: { track_experiment: 'new_repo', track_event: 'click_link', track_label: 'plus_menu_dropdown' }
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
-# https://gitlab.com/gitlab-org/gitlab-foss/issues/49713 for more information. -# https://gitlab.com/gitlab-org/gitlab-foss/issues/49713 for more information.
%ul.list-unstyled.navbar-sub-nav %ul.list-unstyled.navbar-sub-nav
- if dashboard_nav_link?(:projects) - if dashboard_nav_link?(:projects)
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown", data: { track_label: "projects_dropdown", track_event: "click_dropdown" } }) do = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown", data: { track_label: "projects_dropdown", track_event: "click_dropdown", track_experiment: "new_repo" } }) do
%button{ type: 'button', data: { toggle: "dropdown" } } %button{ type: 'button', data: { toggle: "dropdown" } }
= _('Projects') = _('Projects')
= sprite_icon('chevron-down', css_class: 'caret-down') = sprite_icon('chevron-down', css_class: 'caret-down')
......
...@@ -11,14 +11,21 @@ ...@@ -11,14 +11,21 @@
= nav_link(path: 'projects#trending') do = nav_link(path: 'projects#trending') do
= link_to explore_root_path, data: { track_label: "projects_dropdown_explore_projects", track_event: "click_link" } do = link_to explore_root_path, data: { track_label: "projects_dropdown_explore_projects", track_event: "click_link" } do
= _('Explore projects') = _('Explore projects')
= nav_link(path: 'projects/new#blank_project', - experiment(:new_repo, user: current_user) do |e|
html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' }, - e.use do
data: { track_label: "projects_dropdown_blank_project", track_event: "click_link" }) do = nav_link(path: 'projects/new#blank_project', html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' }) do
= link_to new_project_path(anchor: 'blank_project') do = link_to new_project_path(anchor: 'blank_project'), data: { track_label: "projects_dropdown_blank_project", track_event: "click_link", track_experiment: "new_repo" } do
= _('Create blank project') = _('Create blank project')
= nav_link(path: 'projects/new#import_project') do = nav_link(path: 'projects/new#import_project') do
= link_to new_project_path(anchor: 'import_project'), data: { track_label: "projects_dropdown_import_project", track_event: "click_link" } do = link_to new_project_path(anchor: 'import_project'), data: { track_label: "projects_dropdown_import_project", track_event: "click_link", track_experiment: "new_repo" } do
= _('Import project') = _('Import project')
- e.try do
= nav_link(path: 'projects/new#blank_project', html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' }) do
= link_to new_project_path(anchor: 'blank_project'), data: { track_label: "projects_dropdown_blank_project", track_event: "click_link", track_experiment: "new_repo" } do
= _('Create blank project/repository')
= nav_link(path: 'projects/new#import_project') do
= link_to new_project_path(anchor: 'import_project'), data: { track_label: "projects_dropdown_import_project", track_event: "click_link", track_experiment: "new_repo" } do
= _('Import project/repository')
= nav_link(path: 'projects/new#create_from_template') do = nav_link(path: 'projects/new#create_from_template') do
= link_to new_project_path(anchor: 'create_from_template'), data: { track_label: "projects_dropdown_create_from_template", track_event: "click_link" } do = link_to new_project_path(anchor: 'create_from_template'), data: { track_label: "projects_dropdown_create_from_template", track_event: "click_link" } do
= _('Create from template') = _('Create from template')
......
---
name: new_repo
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55818
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285153
milestone: '13.11'
type: experiment
group: group::adoption
default_enabled: false
...@@ -8891,6 +8891,9 @@ msgstr "" ...@@ -8891,6 +8891,9 @@ msgstr ""
msgid "Create blank project" msgid "Create blank project"
msgstr "" msgstr ""
msgid "Create blank project/repository"
msgstr ""
msgid "Create branch" msgid "Create branch"
msgstr "" msgstr ""
...@@ -15925,6 +15928,9 @@ msgstr "" ...@@ -15925,6 +15928,9 @@ msgstr ""
msgid "Import project members" msgid "Import project members"
msgstr "" msgstr ""
msgid "Import project/repository"
msgstr ""
msgid "Import projects from Bitbucket" msgid "Import projects from Bitbucket"
msgstr "" msgstr ""
...@@ -20857,6 +20863,9 @@ msgstr "" ...@@ -20857,6 +20863,9 @@ msgstr ""
msgid "New project" msgid "New project"
msgstr "" msgstr ""
msgid "New project/repository"
msgstr ""
msgid "New release" msgid "New release"
msgstr "" msgstr ""
...@@ -24704,6 +24713,9 @@ msgstr "" ...@@ -24704,6 +24713,9 @@ msgstr ""
msgid "ProjectsNew|Create blank project" msgid "ProjectsNew|Create blank project"
msgstr "" msgstr ""
msgid "ProjectsNew|Create blank project/repository"
msgstr ""
msgid "ProjectsNew|Create from template" msgid "ProjectsNew|Create from template"
msgstr "" msgstr ""
...@@ -24722,6 +24734,9 @@ msgstr "" ...@@ -24722,6 +24734,9 @@ msgstr ""
msgid "ProjectsNew|Import project" msgid "ProjectsNew|Import project"
msgstr "" msgstr ""
msgid "ProjectsNew|Import project/repository"
msgstr ""
msgid "ProjectsNew|Initialize repository with a README" msgid "ProjectsNew|Initialize repository with a README"
msgstr "" msgstr ""
......
...@@ -5,7 +5,7 @@ module QA ...@@ -5,7 +5,7 @@ module QA
module Dashboard module Dashboard
module Snippet module Snippet
class Index < Page::Base class Index < Page::Base
view 'app/views/layouts/header/_new_dropdown.haml' do view 'app/views/layouts/header/_new_dropdown.html.haml' do
element :new_menu_toggle element :new_menu_toggle
element :global_new_snippet_link element :global_new_snippet_link
end end
......
...@@ -22,7 +22,7 @@ module QA ...@@ -22,7 +22,7 @@ module QA
element :file_tree_table element :file_tree_table
end end
view 'app/views/layouts/header/_new_dropdown.haml' do view 'app/views/layouts/header/_new_dropdown.html.haml' do
element :new_menu_toggle element :new_menu_toggle
element :new_issue_link, "link_to _('New issue'), new_project_issue_path(@project)" # rubocop:disable QA/ElementWithPattern element :new_issue_link, "link_to _('New issue'), new_project_issue_path(@project)" # rubocop:disable QA/ElementWithPattern
end end
......
...@@ -448,6 +448,12 @@ RSpec.describe ProjectsController do ...@@ -448,6 +448,12 @@ RSpec.describe ProjectsController do
post :create, params: { project: project_params } post :create, params: { project: project_params }
end end
it 'tracks a created event for the new_repo experiment', :experiment do
expect(experiment(:new_repo, :candidate)).to track(:project_created).on_next_instance
post :create, params: { project: project_params }
end
end end
describe 'POST #archive' do describe 'POST #archive' do
......
...@@ -12,6 +12,72 @@ RSpec.describe 'New project', :js do ...@@ -12,6 +12,72 @@ RSpec.describe 'New project', :js do
sign_in(user) sign_in(user)
end end
context 'new repo experiment', :experiment do
it 'when in control renders "project"' do
stub_experiments(new_repo: :control)
visit new_project_path
find('li.header-new.dropdown').click
page.within('li.header-new.dropdown') do
expect(page).to have_selector('a', text: 'New project')
expect(page).to have_no_selector('a', text: 'New project/repository')
end
expect(page).to have_selector('.blank-state-title', text: 'Create blank project')
expect(page).to have_no_selector('.blank-state-title', text: 'Create blank project/repository')
end
it 'when in candidate renders "project/repository"' do
stub_experiments(new_repo: :candidate)
visit new_project_path
find('li.header-new.dropdown').click
page.within('li.header-new.dropdown') do
expect(page).to have_selector('a', text: 'New project/repository')
end
expect(page).to have_selector('.blank-state-title', text: 'Create blank project/repository')
end
context 'with combined_menu feature disabled' do
before do
stub_feature_flags(combined_menu: false)
end
it 'when in control it renders "project" in the new projects dropdown' do
stub_experiments(new_repo: :control)
visit new_project_path
find('#nav-projects-dropdown').click
page.within('#nav-projects-dropdown') do
expect(page).to have_selector('a', text: 'Create blank project')
expect(page).to have_selector('a', text: 'Import project')
expect(page).to have_no_selector('a', text: 'Create blank project/repository')
expect(page).to have_no_selector('a', text: 'Import project/repository')
end
end
it 'when in candidate it renders "project/repository" in the new projects dropdown' do
stub_experiments(new_repo: :candidate)
visit new_project_path
find('#nav-projects-dropdown').click
page.within('#nav-projects-dropdown') do
expect(page).to have_selector('a', text: 'Create blank project/repository')
expect(page).to have_selector('a', text: 'Import project/repository')
end
end
end
end
it 'shows a message if multiple levels are restricted' do it 'shows a message if multiple levels are restricted' do
Gitlab::CurrentSettings.update!( Gitlab::CurrentSettings.update!(
restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL] restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL]
......
import { GlBreadcrumb } from '@gitlab/ui'; import { GlBreadcrumb } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { assignGitlabExperiment } from 'helpers/experimentation_helper';
import App from '~/projects/experiment_new_project_creation/components/app.vue'; import App from '~/projects/experiment_new_project_creation/components/app.vue';
import LegacyContainer from '~/projects/experiment_new_project_creation/components/legacy_container.vue'; import LegacyContainer from '~/projects/experiment_new_project_creation/components/legacy_container.vue';
import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue'; import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue';
...@@ -17,6 +18,34 @@ describe('Experimental new project creation app', () => { ...@@ -17,6 +18,34 @@ describe('Experimental new project creation app', () => {
wrapper = null; wrapper = null;
}); });
const findWelcomePage = () => wrapper.findComponent(WelcomePage);
const findPanel = (panelName) =>
findWelcomePage()
.props()
.panels.find((p) => p.name === panelName);
describe('new_repo experiment', () => {
describe('when in the candidate variant', () => {
assignGitlabExperiment('new_repo', 'candidate');
it('has "repository" in the panel title', () => {
createComponent();
expect(findPanel('blank_project').title).toBe('Create blank project/repository');
});
});
describe('when in the control variant', () => {
assignGitlabExperiment('new_repo', 'control');
it('has "project" in the panel title', () => {
createComponent();
expect(findPanel('blank_project').title).toBe('Create blank project');
});
});
});
describe('with empty hash', () => { describe('with empty hash', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { mockTracking } from 'helpers/tracking_helper'; import { mockTracking } from 'helpers/tracking_helper';
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
import { getExperimentData } from '~/experimentation/utils';
import NewProjectPushTipPopover from '~/projects/experiment_new_project_creation/components/new_project_push_tip_popover.vue'; import NewProjectPushTipPopover from '~/projects/experiment_new_project_creation/components/new_project_push_tip_popover.vue';
import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue'; import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue';
jest.mock('~/experimentation/utils', () => ({ getExperimentData: jest.fn() }));
describe('Welcome page', () => { describe('Welcome page', () => {
let wrapper; let wrapper;
let trackingSpy; let trackingSpy;
...@@ -14,6 +19,7 @@ describe('Welcome page', () => { ...@@ -14,6 +19,7 @@ describe('Welcome page', () => {
beforeEach(() => { beforeEach(() => {
trackingSpy = mockTracking('_category_', document, jest.spyOn); trackingSpy = mockTracking('_category_', document, jest.spyOn);
trackingSpy.mockImplementation(() => {}); trackingSpy.mockImplementation(() => {});
getExperimentData.mockReturnValue(undefined);
}); });
afterEach(() => { afterEach(() => {
...@@ -22,14 +28,35 @@ describe('Welcome page', () => { ...@@ -22,14 +28,35 @@ describe('Welcome page', () => {
wrapper = null; wrapper = null;
}); });
it('tracks link clicks', () => { it('tracks link clicks', async () => {
createComponent({ panels: [{ name: 'test', href: '#' }] }); createComponent({ panels: [{ name: 'test', href: '#' }] });
wrapper.find('a').trigger('click'); const link = wrapper.find('a');
link.trigger('click');
await nextTick();
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', { label: 'test' }); expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', { label: 'test' });
}); });
}); });
it('adds new_repo experiment data if in experiment', async () => {
const mockExperimentData = 'data';
getExperimentData.mockReturnValue(mockExperimentData);
createComponent({ panels: [{ name: 'test', href: '#' }] });
const link = wrapper.find('a');
link.trigger('click');
await nextTick();
return wrapper.vm.$nextTick().then(() => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', {
label: 'test',
context: {
data: mockExperimentData,
schema: TRACKING_CONTEXT_SCHEMA,
},
});
});
});
it('renders new project push tip popover', () => { it('renders new project push tip popover', () => {
createComponent({ panels: [{ name: 'test', href: '#' }] }); createComponent({ panels: [{ name: 'test', href: '#' }] });
......
...@@ -163,6 +163,7 @@ RSpec.describe 'layouts/header/_new_dropdown' do ...@@ -163,6 +163,7 @@ RSpec.describe 'layouts/header/_new_dropdown' do
end end
it 'has a "New project" link' do it 'has a "New project" link' do
render('layouts/header/new_repo_experiment')
render render
expect(rendered).to have_link('New project', href: new_project_path) expect(rendered).to have_link('New project', href: new_project_path)
......
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