From 6b9d987157fdb1ba0bf92ca65476ea3f31e2ae6e Mon Sep 17 00:00:00 2001 From: Ross Byrne <robyrne@gitlab.com> Date: Wed, 14 Feb 2024 12:27:58 +0000 Subject: [PATCH] Implementing Trial Discover Page Experiment Stage 2 --- .../page_bundles/trial_discover_page.scss | 16 ++ .../components/constants.js | 4 + .../components/trial_status_popover.vue | 3 +- .../components/trial_status_widget.vue | 36 +++- .../groups/discovers_controller.rb | 3 +- .../trial_discover_page_experiment.rb | 11 ++ .../groups/trial_discover_page_helper.rb | 160 ++++++++++++++++++ .../discovers/_discover_detail.html.haml | 9 + .../discovers/_discover_feature.html.haml | 24 +++ .../_discover_page_actions.html.haml | 9 + ee/app/views/groups/discovers/show.html.haml | 17 ++ .../trial_discover_page_experiment_spec.rb | 24 ++- .../trial_status_widget_spec.js.snap | 39 ++--- .../trial_status_popover_spec.js | 17 ++ .../trial_status_widget_spec.js | 59 +++++-- .../groups/trial_discover_page_helper_spec.rb | 85 ++++++++++ .../groups/discovers/show.html.haml_spec.rb | 117 +++++++++++++ locale/gitlab.pot | 99 +++++++++++ 18 files changed, 673 insertions(+), 59 deletions(-) create mode 100644 app/assets/stylesheets/page_bundles/trial_discover_page.scss create mode 100644 ee/app/helpers/groups/trial_discover_page_helper.rb create mode 100644 ee/app/views/groups/discovers/_discover_detail.html.haml create mode 100644 ee/app/views/groups/discovers/_discover_feature.html.haml create mode 100644 ee/app/views/groups/discovers/_discover_page_actions.html.haml create mode 100644 ee/spec/helpers/groups/trial_discover_page_helper_spec.rb create mode 100644 ee/spec/views/groups/discovers/show.html.haml_spec.rb diff --git a/app/assets/stylesheets/page_bundles/trial_discover_page.scss b/app/assets/stylesheets/page_bundles/trial_discover_page.scss new file mode 100644 index 0000000000000..bfcbf2b12b1ca --- /dev/null +++ b/app/assets/stylesheets/page_bundles/trial_discover_page.scss @@ -0,0 +1,16 @@ +@import 'mixins_and_variables_and_functions'; + +.trial-discover-page-card { + @include media-breakpoint-down(md) { + .description-text { + height: 6em; + } + } +} + +.trial-discover-page-card.gl-flex-basis-third, +.trial-discover-page-card.gl-flex-basis-half { + @include media-breakpoint-down(md) { + flex-basis: 100%; + } +} diff --git a/ee/app/assets/javascripts/contextual_sidebar/components/constants.js b/ee/app/assets/javascripts/contextual_sidebar/components/constants.js index 74daffbfcfdc2..aeb3537f6b442 100644 --- a/ee/app/assets/javascripts/contextual_sidebar/components/constants.js +++ b/ee/app/assets/javascripts/contextual_sidebar/components/constants.js @@ -55,6 +55,10 @@ export const POPOVER = { action: CLICK_BUTTON_ACTION, label: 'compare_all_plans', }, + learnAboutFeaturesClick: { + action: CLICK_BUTTON_ACTION, + label: 'learn_about_features', + }, }, resizeEventDebounceMS: RESIZE_EVENT_DEBOUNCE_MS, disabledBreakpoints: ['xs', 'sm'], diff --git a/ee/app/assets/javascripts/contextual_sidebar/components/trial_status_popover.vue b/ee/app/assets/javascripts/contextual_sidebar/components/trial_status_popover.vue index e51e6b7df4788..750b8b5c2b12f 100644 --- a/ee/app/assets/javascripts/contextual_sidebar/components/trial_status_popover.vue +++ b/ee/app/assets/javascripts/contextual_sidebar/components/trial_status_popover.vue @@ -17,7 +17,7 @@ const { resizeEventDebounceMS, disabledBreakpoints, } = POPOVER; -const trackingMixin = Tracking.mixin(); +const trackingMixin = Tracking.mixin({ experiment: 'trial_discover_page' }); export default { components: { @@ -198,6 +198,7 @@ export default { block data-testid="learn-about-features-btn" :title="$options.i18n.learnAboutButtonTitle" + @click="trackPageAction('learnAboutFeaturesClick')" > <span class="gl-font-sm">{{ $options.i18n.learnAboutButtonTitle }}</span> </gl-button> diff --git a/ee/app/assets/javascripts/contextual_sidebar/components/trial_status_widget.vue b/ee/app/assets/javascripts/contextual_sidebar/components/trial_status_widget.vue index 224710f093b08..121a3a482df53 100644 --- a/ee/app/assets/javascripts/contextual_sidebar/components/trial_status_widget.vue +++ b/ee/app/assets/javascripts/contextual_sidebar/components/trial_status_widget.vue @@ -4,10 +4,11 @@ import { removeTrialSuffix } from 'ee/billings/billings_util'; import { sprintf } from '~/locale'; import Tracking from '~/tracking'; import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue'; +import { isExperimentVariant } from '~/experimentation/utils'; import { WIDGET } from './constants'; const { i18n, trackingEvents } = WIDGET; -const trackingMixin = Tracking.mixin(); +const trackingMixin = Tracking.mixin({ experiment: 'trial_discover_page' }); export default { components: { @@ -30,6 +31,11 @@ export default { }, i18n, computed: { + widgetLink() { + return isExperimentVariant('trial_discover_page', 'candidate') + ? this.trialDiscoverPagePath + : this.plansHref; + }, isTrialActive() { return this.percentageComplete <= 100; }, @@ -45,21 +51,25 @@ export default { duration: this.trialDuration, }); }, + trackingOptions() { + return this.isTrialActive + ? trackingEvents.activeTrialOptions + : trackingEvents.trialEndedOptions; + }, }, methods: { + onLearnAboutFeaturesClick() { + this.track(trackingEvents.action, { ...this.trackingOptions, label: 'learn_about_features' }); + }, onWidgetClick() { - const options = this.isTrialActive - ? trackingEvents.activeTrialOptions - : trackingEvents.trialEndedOptions; - - this.track(trackingEvents.action, { ...options }); + this.track(trackingEvents.action, { ...this.trackingOptions }); }, }, }; </script> <template> - <gl-link :id="containerId" :title="widgetTitle" :href="plansHref"> + <gl-link :id="containerId" :title="widgetTitle" :href="widgetLink"> <div data-testid="widget-menu" class="gl-display-flex gl-flex-direction-column gl-align-items-stretch gl-w-full" @@ -91,6 +101,7 @@ export default { class="gl-mt-3" data-testid="learn-about-features-btn" :title="$options.i18n.learnAboutButtonTitle" + @click.stop="onLearnAboutFeaturesClick()" > {{ $options.i18n.learnAboutButtonTitle }} </gl-button> @@ -107,9 +118,16 @@ export default { {{ $options.i18n.widgetBodyExpiredTrial }} <gitlab-experiment name="trial_discover_page"> <template #candidate> - <a data-testid="learn-about-features-link" :href="trialDiscoverPagePath"> + <gl-button + :href="trialDiscoverPagePath" + variant="link" + size="small" + data-testid="learn-about-features-btn" + :title="$options.i18n.learnAboutButtonTitle" + @click.stop="onLearnAboutFeaturesClick()" + > {{ $options.i18n.learnAboutButtonTitle }} - </a> + </gl-button> </template> </gitlab-experiment> </div> diff --git a/ee/app/controllers/groups/discovers_controller.rb b/ee/app/controllers/groups/discovers_controller.rb index c8cca5d0beeca..b80fe2b3cafff 100644 --- a/ee/app/controllers/groups/discovers_controller.rb +++ b/ee/app/controllers/groups/discovers_controller.rb @@ -16,7 +16,8 @@ def show; end private def authorize_discover_page - render_404 if experiment(:trial_discover_page, actor: current_user).assigned[:name] == :control + return render_404 if experiment(:trial_discover_page, actor: current_user).assigned[:name] == :control + render_404 unless group.trial_active? end end diff --git a/ee/app/experiments/trial_discover_page_experiment.rb b/ee/app/experiments/trial_discover_page_experiment.rb index 157612792dad5..cd5bbaa6a788f 100644 --- a/ee/app/experiments/trial_discover_page_experiment.rb +++ b/ee/app/experiments/trial_discover_page_experiment.rb @@ -1,11 +1,22 @@ # frozen_string_literal: true class TrialDiscoverPageExperiment < ApplicationExperiment + EXCLUDE_USERS_OLDER_THAN = Date.new(2024, 2, 14) + control variant(:candidate) + exclude :previously_existing_users + private def control_behavior; end def candidate_behavior; end + + def previously_existing_users + actor_created_at = context&.actor&.created_at + return true if actor_created_at.nil? + + actor_created_at < EXCLUDE_USERS_OLDER_THAN + end end diff --git a/ee/app/helpers/groups/trial_discover_page_helper.rb b/ee/app/helpers/groups/trial_discover_page_helper.rb new file mode 100644 index 0000000000000..625ae9b056ba7 --- /dev/null +++ b/ee/app/helpers/groups/trial_discover_page_helper.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +module Groups + module TrialDiscoverPageHelper + include Gitlab::Utils::StrongMemoize + + def trial_discover_page_features + [ + { + icon: "epic", + title: s_("TrialDiscoverPage|Epics"), + description: s_("TrialDiscoverPage|Collaborate on high-level ideas that share a common theme. " \ + "Use epics to group issues that cross milestones and projects."), + plan: s_("TrialDiscoverPage|Premium"), + doc_url: help_page_url("user/group/epics/index"), + video_url: "https://vimeo.com/693759778", + container_class: 'gl-flex-basis-third', + description_class: 'gl-h-13', + tracking_label: 'epics_feature' + }, + { + icon: "location", + title: s_("TrialDiscoverPage|Roadmaps"), + description: s_("TrialDiscoverPage|Visualize your epics and milestones in a timeline."), + plan: s_("TrialDiscoverPage|Premium"), + doc_url: help_page_url("user/group/roadmap/index"), + video_url: "https://vimeo.com/670922063", + container_class: 'gl-flex-basis-third', + description_class: 'gl-h-13', + tracking_label: 'roadmaps_feature' + }, + { + icon: "label", + title: s_("TrialDiscoverPage|Scoped Labels"), + description: s_("TrialDiscoverPage|Create a more advanced workflow for issues, merge requests, " \ + "and epics by using scoped, mutually exclusive labels."), + plan: s_("TrialDiscoverPage|Premium"), + doc_url: help_page_url("user/project/labels", anchor: "scoped-labels"), + video_url: "https://vimeo.com/670906315", + container_class: 'gl-flex-basis-third', + description_class: 'gl-h-13', + tracking_label: 'scoped_labels_feature' + }, + { + icon: "merge-request", + title: s_("TrialDiscoverPage|Merge request approval rule"), + description: s_("TrialDiscoverPage|Maintain high quality code by requiring approval " \ + "from specific users on your merge requests."), + plan: s_("TrialDiscoverPage|Premium"), + doc_url: help_page_url("user/project/merge_requests/approvals/rules"), + video_url: "https://vimeo.com/670904904", + container_class: 'gl-flex-basis-third', + description_class: 'gl-h-13', + tracking_label: 'merge_request_rule_feature' + }, + { + icon: "progress", + title: s_("TrialDiscoverPage|Burn down charts"), + description: s_("TrialDiscoverPage|Track your development progress by viewing issues in a burndown chart."), + plan: s_("TrialDiscoverPage|Premium"), + doc_url: help_page_url("user/project/milestones/burndown_and_burnup_charts"), + video_url: "https://vimeo.com/670905639", + container_class: 'gl-flex-basis-third', + description_class: 'gl-h-13', + tracking_label: 'burn_down_chart_feature' + }, + { + icon: "account", + title: s_("TrialDiscoverPage|Code owners"), + description: s_("TrialDiscoverPage|Target the right approvers for your merge request by assigning " \ + "owners to specific files."), + plan: s_("TrialDiscoverPage|Premium"), + doc_url: help_page_url("user/project/codeowners/index"), + video_url: "https://vimeo.com/670896787", + container_class: 'gl-flex-basis-third', + description_class: 'gl-h-13', + tracking_label: 'code_owners_feature' + }, + { + icon: "chart", + title: s_("TrialDiscoverPage|Code review analytics"), + description: s_("TrialDiscoverPage|Find and fix bottlenecks in your code review process by understanding " \ + "how long open merge requests have been in review."), + plan: s_("TrialDiscoverPage|Premium"), + doc_url: help_page_url("user/analytics/code_review_analytics"), + video_url: "https://vimeo.com/670893940", + container_class: 'gl-flex-basis-half', + description_class: 'gl-h-10', + tracking_label: 'code_review_feature' + }, + { + icon: "shield", + title: s_("TrialDiscoverPage|Free guest users"), + description: s_("TrialDiscoverPage|Let users view what GitLab has to offer without using a " \ + "subscription seat."), + plan: s_("TrialDiscoverPage|Ultimate"), + doc_url: help_page_url("user/permissions"), + calculator_url: "https://about.gitlab.com/pricing/ultimate/#wu-guest-calculator", + container_class: 'gl-flex-basis-half', + description_class: 'gl-h-10', + tracking_label: 'free_guests_feature' + }, + { + icon: "shield", + title: s_("TrialDiscoverPage|Dependency scanning"), + description: s_("TrialDiscoverPage|Keep your application secure by checking your libraries for " \ + "vulnerabilities."), + plan: s_("TrialDiscoverPage|Ultimate"), + doc_url: help_page_url("user/application_security/dependency_scanning/index"), + video_url: "https://vimeo.com/670886968", + container_class: 'gl-flex-basis-half', + description_class: 'gl-h-10', + tracking_label: 'dependency_scanning_feature' + }, + { + icon: "issue-type-test-case", + title: s_("TrialDiscoverPage|Dynamic application security testing (DAST)"), + description: s_("TrialDiscoverPage|Keep your application secure by checking your deployed environments " \ + "for vulnerabilities."), + plan: s_("TrialDiscoverPage|Ultimate"), + doc_url: help_page_url("user/application_security/dast/index"), + video_url: "https://vimeo.com/670891385", + container_class: 'gl-flex-basis-half', + description_class: 'gl-h-10', + tracking_label: 'dast_feature' + } + ] + end + + def trial_discover_page_details + [ + { + icon: "users", + title: s_("TrialDiscoverPage|Collaboration made easy"), + description: s_("TrialDiscoverPage|Break down silos to coordinate seamlessly across development, " \ + "operations, and security with a consistent experience across the development lifecycle.") + }, + { + icon: "code", + title: s_("TrialDiscoverPage|Lower cost of development"), + description: s_("TrialDiscoverPage|A single application eliminates complex integrations, data " \ + "checkpoints, and toolchain maintenance, resulting in greater productivity and lower cost.") + }, + { + icon: "deployments", + title: s_("TrialDiscoverPage|Your software, deployed your way"), + description: s_("TrialDiscoverPage|GitLab is infrastructure agnostic. GitLab supports GCP, AWS, " \ + "OpenShift, VMware, on-premises, bare metal, and more.") + + } + ] + end + + def group_trial_status(group) + strong_memoize_with(:group_trial_status, group) do + group.trial_active? ? 'trial_active' : 'trial_expired' + end + end + end +end diff --git a/ee/app/views/groups/discovers/_discover_detail.html.haml b/ee/app/views/groups/discovers/_discover_detail.html.haml new file mode 100644 index 0000000000000..8df4f05da5404 --- /dev/null +++ b/ee/app/views/groups/discovers/_discover_detail.html.haml @@ -0,0 +1,9 @@ +.trial-discover-page-card.gl-flex-grow-1.gl-pr-6.gl-py-3.gl-flex-basis-third + = render Pajamas::CardComponent.new(card_options: { class: 'gl-rounded-lg' }) do |c| + - c.with_body do + .gl-mb-3 + = sprite_icon(item[:icon], css_class: "gl-text-blue-400") + %h5.gl-mt-3 + = item[:title] + .gl-h-13 + = item[:description] diff --git a/ee/app/views/groups/discovers/_discover_feature.html.haml b/ee/app/views/groups/discovers/_discover_feature.html.haml new file mode 100644 index 0000000000000..f47cfca160dbc --- /dev/null +++ b/ee/app/views/groups/discovers/_discover_feature.html.haml @@ -0,0 +1,24 @@ +.trial-discover-page-card.gl-flex-grow-1.gl-pr-6.gl-py-4{ class: item[:container_class] } + = render Pajamas::CardComponent.new(card_options: { class: 'gl-rounded-lg' }) do |c| + - c.with_body do + .gl-display-flex.gl-justify-content-space-between + = sprite_icon(item[:icon], css_class: "gl-text-blue-400") + = gl_badge_tag(item[:plan], variant: :tier) + %h5.gl-mt-1 + = item[:title] + .description-text{ class: item[:description_class] } + = item[:description] + .gl-display-flex + - if item[:video_url].present? + = render Pajamas::ButtonComponent.new(icon: "live-preview", href: item[:video_url], variant: :link, target: "_blank", + button_options: { data: { track_action: "click_video_link_#{group_trial_status(group)}", track_label: item[:tracking_label] } }) + - else + = render Pajamas::ButtonComponent.new(href: item[:calculator_url], variant: :link, target: "_blank", + button_options: { data: { track_action: "click_calculate_seats_#{group_trial_status(group)}", track_label: item[:tracking_label] } }) do + = s_("TrialDiscoverPage|Calculate seats") + + .gl-px-2 + | + = render Pajamas::ButtonComponent.new(href: item[:doc_url], variant: :link, target: "_blank", + button_options: { data: { track_action: "click_documentation_link_#{group_trial_status(group)}", track_label: item[:tracking_label] } }) do + = s_("TrialDiscoverPage|Documentation") diff --git a/ee/app/views/groups/discovers/_discover_page_actions.html.haml b/ee/app/views/groups/discovers/_discover_page_actions.html.haml new file mode 100644 index 0000000000000..57b0c0933c39f --- /dev/null +++ b/ee/app/views/groups/discovers/_discover_page_actions.html.haml @@ -0,0 +1,9 @@ +.gl-display-flex + = render Pajamas::ButtonComponent.new(href: group_billings_path(group), variant: :confirm, + button_options: { class: 'gl-mr-3', data: { track_action: "click_compare_plans", track_label: group_trial_status(group) } }) do + = s_('TrialDiscoverPage|Compare all plans') + + .js-hand-raise-lead-button{ data: { **hand_raise_props(group, glm_content: 'trial_discover_page'), + button_attributes: { variant: 'confirm', category: 'secondary' }.to_json, + track_action: 'click_contact_sales', + track_label: group_trial_status(group) } } diff --git a/ee/app/views/groups/discovers/show.html.haml b/ee/app/views/groups/discovers/show.html.haml index 21c88bfd7c938..f362de8d9a1ae 100644 --- a/ee/app/views/groups/discovers/show.html.haml +++ b/ee/app/views/groups/discovers/show.html.haml @@ -1,4 +1,21 @@ - page_title s_("TrialDiscoverPage|Discover") +- add_page_specific_style 'page_bundles/trial_discover_page', defer: false %h2= s_('TrialDiscoverPage|Discover Premium & Ultimate') %p= s_('TrialDiscoverPage|Access advanced features, build more efficiently, strengthen security and compliance.') + += render 'discover_page_actions', group: @group + +%h4.gl-mt-8= s_('TrialDiscoverPage|Access advanced features') + +.gl-display-flex.gl-flex-wrap + = render partial: 'discover_feature', collection: trial_discover_page_features, as: :item, locals: { group: @group } + +%h4.gl-mt-8 + = s_('TrialDiscoverPage|Speed. Efficiency. Trust.') + +.gl-display-flex.gl-flex-wrap + = render partial: 'discover_detail', collection: trial_discover_page_details, as: :item + +.gl-mt-5 + = render 'discover_page_actions', group: @group diff --git a/ee/spec/experiments/trial_discover_page_experiment_spec.rb b/ee/spec/experiments/trial_discover_page_experiment_spec.rb index 1cf3e02b3a956..ff7b9841cda17 100644 --- a/ee/spec/experiments/trial_discover_page_experiment_spec.rb +++ b/ee/spec/experiments/trial_discover_page_experiment_spec.rb @@ -3,15 +3,29 @@ require 'spec_helper' RSpec.describe TrialDiscoverPageExperiment, :experiment, feature_category: :activation do + let_it_be(:excluded) { build_stubbed(:user, created_at: Date.new(2024, 1, 1)) } + let_it_be(:assigned) do + build_stubbed(:user, created_at: TrialDiscoverPageExperiment::EXCLUDE_USERS_OLDER_THAN) + end + + shared_examples 'existing_users_are_excluded' do + it "excludes existing users" do + expect(experiment(:trial_discover_page, actor: excluded)).to exclude(actor: excluded) + expect(experiment(:trial_discover_page, actor: assigned)).not_to exclude(actor: assigned) + end + end + context 'with control experience' do before do stub_experiments(trial_discover_page: :control) end it 'registers control behavior' do - expect(experiment(:trial_discover_page)).to register_behavior(:control).with(nil) - expect { experiment(:trial_discover_page).run }.not_to raise_error + expect(experiment(:trial_discover_page, actor: assigned)).to register_behavior(:control).with(nil) + expect { experiment(:trial_discover_page, actor: assigned).run }.not_to raise_error end + + it_behaves_like 'existing_users_are_excluded' end context 'with candidate experience' do @@ -20,8 +34,10 @@ end it 'registers candidate behavior' do - expect(experiment(:trial_discover_page)).to register_behavior(:candidate).with(nil) - expect { experiment(:trial_discover_page).run }.not_to raise_error + expect(experiment(:trial_discover_page, actor: assigned)).to register_behavior(:candidate).with(nil) + expect { experiment(:trial_discover_page, actor: assigned).run }.not_to raise_error end + + it_behaves_like 'existing_users_are_excluded' end end diff --git a/ee/spec/frontend/contextual_sidebar/__snapshots__/trial_status_widget_spec.js.snap b/ee/spec/frontend/contextual_sidebar/__snapshots__/trial_status_widget_spec.js.snap index ab9054c15a947..3322edc51474c 100644 --- a/ee/spec/frontend/contextual_sidebar/__snapshots__/trial_status_widget_spec.js.snap +++ b/ee/spec/frontend/contextual_sidebar/__snapshots__/trial_status_widget_spec.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TrialStatusWidget component without the optional containerId prop matches the snapshot for namespace in active trial 1`] = ` -<gllink-stub +<gl-link-stub href="billing/path-for/group" title="Ultimate Trial" > @@ -37,27 +37,19 @@ exports[`TrialStatusWidget component without the optional containerId prop match <div class="gl-align-items-stretch gl-display-flex gl-mt-2" > - <div + <gl-progress-bar-stub aria-hidden="true" - class="gl-flex-grow-1 progress" - > - <div - aria-valuemax="100" - aria-valuemin="0" - aria-valuenow="10" - class="progress-bar" - role="progressbar" - style="width: 10%;" - /> - </div> + class="gl-flex-grow-1" + value="10" + /> </div> </div> </div> -</gllink-stub> +</gl-link-stub> `; exports[`TrialStatusWidget component without the optional containerId prop matches the snapshot for namespace not in active trial 1`] = ` -<gllink-stub +<gl-link-stub href="billing/path-for/group" title="Your 30-day trial has ended" > @@ -68,16 +60,11 @@ exports[`TrialStatusWidget component without the optional containerId prop match <div class="gl-display-flex gl-gap-5 gl-px-2 gl-w-full" > - <svg - aria-hidden="true" - class="gl-icon gl-text-blue-600! s16" - data-testid="information-o-icon" - role="img" - > - <use - href="file-mock#information-o" - /> - </svg> + <gl-icon-stub + class="gl-text-blue-600!" + name="information-o" + size="16" + /> <div> <div class="gl-font-weight-bold" @@ -92,5 +79,5 @@ exports[`TrialStatusWidget component without the optional containerId prop match </div> </div> </div> -</gllink-stub> +</gl-link-stub> `; diff --git a/ee/spec/frontend/contextual_sidebar/trial_status_popover_spec.js b/ee/spec/frontend/contextual_sidebar/trial_status_popover_spec.js index 8d2c1a8e04a01..db271798580f5 100644 --- a/ee/spec/frontend/contextual_sidebar/trial_status_popover_spec.js +++ b/ee/spec/frontend/contextual_sidebar/trial_status_popover_spec.js @@ -255,6 +255,23 @@ describe('TrialStatusPopover component', () => { expect(findLearnAboutFeaturesBtn().exists()).toBe(true); expect(findLearnAboutFeaturesBtn().attributes('href')).toBe('discover-path'); }); + + it('tracks click event', () => { + wrapper = createComponent({ providers: { daysRemaining: 5 } }); + + findLearnAboutFeaturesBtn().vm.$emit('click'); + + expectTracking(trackingEvents.activeTrialCategory, { + ...trackingEvents.learnAboutFeaturesClick, + context: { + data: { + experiment: 'trial_discover_page', + variant: 'candidate', + }, + schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0', + }, + }); + }); }); }); }); diff --git a/ee/spec/frontend/contextual_sidebar/trial_status_widget_spec.js b/ee/spec/frontend/contextual_sidebar/trial_status_widget_spec.js index a0eb1f219f2ba..d02114c95db3b 100644 --- a/ee/spec/frontend/contextual_sidebar/trial_status_widget_spec.js +++ b/ee/spec/frontend/contextual_sidebar/trial_status_widget_spec.js @@ -1,13 +1,15 @@ -import { GlLink } from '@gitlab/ui'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { GlLink, GlButton } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { WIDGET } from 'ee/contextual_sidebar/components/constants'; import TrialStatusWidget from 'ee/contextual_sidebar/components/trial_status_widget.vue'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { stubExperiments } from 'helpers/experimentation_helper'; import { __ } from '~/locale'; +import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue'; describe('TrialStatusWidget component', () => { let wrapper; + let trackingSpy; const { trackingEvents } = WIDGET; const trialDaysUsed = 10; @@ -15,10 +17,9 @@ describe('TrialStatusWidget component', () => { const findGlLink = () => wrapper.findComponent(GlLink); const findLearnAboutFeaturesBtn = () => wrapper.findByTestId('learn-about-features-btn'); - const findLearnAboutFeaturesLink = () => wrapper.findByTestId('learn-about-features-link'); const createComponent = (providers = {}) => { - return mountExtended(TrialStatusWidget, { + return shallowMountExtended(TrialStatusWidget, { provide: { trialDaysUsed, trialDuration, @@ -29,10 +30,18 @@ describe('TrialStatusWidget component', () => { trialDiscoverPagePath: 'discover-path', ...providers, }, - stubs: { GlLink: true }, + stubs: { GitlabExperiment, GlButton }, }); }; + beforeEach(() => { + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); + }); + describe('interpolated strings', () => { it('correctly interpolates them all', () => { wrapper = createComponent(); @@ -61,16 +70,6 @@ describe('TrialStatusWidget component', () => { }); describe('tracks when the widget menu is clicked', () => { - let trackingSpy; - - beforeEach(() => { - trackingSpy = mockTracking(undefined, undefined, jest.spyOn); - }); - - afterEach(() => { - unmockTracking(); - }); - it('tracks with correct information when namespace is in an active trial', async () => { const { category, label } = trackingEvents.activeTrialOptions; await wrapper.findByTestId('widget-menu').trigger('click'); @@ -143,7 +142,7 @@ describe('TrialStatusWidget component', () => { wrapper = createComponent({ percentageComplete: 110 }); expect(wrapper.text()).not.toContain(__('Learn about features')); - expect(findLearnAboutFeaturesLink().exists()).toBe(false); + expect(findLearnAboutFeaturesBtn().exists()).toBe(false); }); }); }); @@ -161,6 +160,18 @@ describe('TrialStatusWidget component', () => { expect(findLearnAboutFeaturesBtn().exists()).toBe(true); expect(findLearnAboutFeaturesBtn().attributes('href')).toBe('discover-path'); }); + + it('tracks clicking learn about features button', async () => { + wrapper = createComponent(); + + const { category } = trackingEvents.activeTrialOptions; + await findLearnAboutFeaturesBtn().trigger('click'); + + expect(trackingSpy).toHaveBeenCalledWith(category, trackingEvents.action, { + category, + label: 'learn_about_features', + }); + }); }); describe('when trial is expired', () => { @@ -168,8 +179,20 @@ describe('TrialStatusWidget component', () => { wrapper = createComponent({ percentageComplete: 110 }); expect(wrapper.text()).toContain(__('Learn about features')); - expect(findLearnAboutFeaturesLink().exists()).toBe(true); - expect(findLearnAboutFeaturesLink().attributes('href')).toBe('discover-path'); + expect(findLearnAboutFeaturesBtn().exists()).toBe(true); + expect(findLearnAboutFeaturesBtn().attributes('href')).toBe('discover-path'); + }); + + it('tracks clicking learn about features link', async () => { + wrapper = createComponent({ percentageComplete: 110 }); + + const { category } = trackingEvents.trialEndedOptions; + await findLearnAboutFeaturesBtn().trigger('click'); + + expect(trackingSpy).toHaveBeenCalledWith(category, trackingEvents.action, { + category, + label: 'learn_about_features', + }); }); }); }); diff --git a/ee/spec/helpers/groups/trial_discover_page_helper_spec.rb b/ee/spec/helpers/groups/trial_discover_page_helper_spec.rb new file mode 100644 index 0000000000000..b5b641d4ab171 --- /dev/null +++ b/ee/spec/helpers/groups/trial_discover_page_helper_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::TrialDiscoverPageHelper, feature_category: :activation do + describe '#discover_details' do + subject(:discover_details) { helper.trial_discover_page_details } + + it 'returns an array' do + expect(discover_details).to be_a(Array) + end + + it 'contains correct number of items' do + expect(discover_details.count).to be(3) + end + + it 'returns correct features' do + expect(discover_details[0][:title]).to eq(s_("TrialDiscoverPage|Collaboration made easy")) + expect(discover_details[1][:title]).to eq(s_("TrialDiscoverPage|Lower cost of development")) + expect(discover_details[2][:title]).to eq(s_("TrialDiscoverPage|Your software, deployed your way")) + end + end + + describe '#discover_features' do + subject(:discover_features) { helper.trial_discover_page_features } + + it 'returns an array' do + expect(discover_features).to be_a(Array) + end + + it 'contains correct number of items' do + expect(discover_features.count).to be(10) + end + + it 'returns correct features' do + expect(discover_features[0][:title]).to eq(s_("TrialDiscoverPage|Epics")) + expect(discover_features[1][:title]).to eq(s_("TrialDiscoverPage|Roadmaps")) + expect(discover_features[2][:title]).to eq(s_("TrialDiscoverPage|Scoped Labels")) + expect(discover_features[3][:title]).to eq(s_("TrialDiscoverPage|Merge request approval rule")) + expect(discover_features[4][:title]).to eq(s_("TrialDiscoverPage|Burn down charts")) + expect(discover_features[5][:title]).to eq(s_("TrialDiscoverPage|Code owners")) + expect(discover_features[6][:title]).to eq(s_("TrialDiscoverPage|Code review analytics")) + expect(discover_features[7][:title]).to eq(s_("TrialDiscoverPage|Free guest users")) + expect(discover_features[8][:title]).to eq(s_("TrialDiscoverPage|Dependency scanning")) + expect(discover_features[9][:title]).to eq(s_("TrialDiscoverPage|Dynamic application security testing (DAST)")) + end + + it 'returns correct tracking labels' do + expect(discover_features[0][:tracking_label]).to eq("epics_feature") + expect(discover_features[1][:tracking_label]).to eq("roadmaps_feature") + expect(discover_features[2][:tracking_label]).to eq("scoped_labels_feature") + expect(discover_features[3][:tracking_label]).to eq("merge_request_rule_feature") + expect(discover_features[4][:tracking_label]).to eq("burn_down_chart_feature") + expect(discover_features[5][:tracking_label]).to eq("code_owners_feature") + expect(discover_features[6][:tracking_label]).to eq("code_review_feature") + expect(discover_features[7][:tracking_label]).to eq("free_guests_feature") + expect(discover_features[8][:tracking_label]).to eq("dependency_scanning_feature") + expect(discover_features[9][:tracking_label]).to eq("dast_feature") + end + end + + describe '#group_trial_status' do + let_it_be(:group) { build_stubbed(:group) } + + context 'when trial is active' do + before do + allow(group).to receive(:trial_active?).and_return(true) + end + + it 'returns correct status' do + expect(helper.group_trial_status(group)).to eq 'trial_active' + end + end + + context 'when trial is expired' do + before do + allow(group).to receive(:trial_active?).and_return(false) + end + + it 'returns correct status' do + expect(helper.group_trial_status(group)).to eq 'trial_expired' + end + end + end +end diff --git a/ee/spec/views/groups/discovers/show.html.haml_spec.rb b/ee/spec/views/groups/discovers/show.html.haml_spec.rb new file mode 100644 index 0000000000000..d843d5cb983fa --- /dev/null +++ b/ee/spec/views/groups/discovers/show.html.haml_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'groups/discovers/show', :saas, :aggregate_failures, feature_category: :activation do + let_it_be(:user) { build_stubbed(:user) } + let_it_be(:group) { build_stubbed(:group) } + let(:tracking_labels) do + [ + :epics_feature, + :roadmaps_feature, + :scoped_labels_feature, + :merge_request_rule_feature, + :burn_down_chart_feature, + :code_owners_feature, + :code_review_feature, + :dependency_scanning_feature, + :dast_feature + ] + end + + before do + allow(view).to receive(:current_user).and_return(user) + assign(:group, group) + end + + it 'renders the discover trial page' do + render + + expect(rendered).to have_text(s_('TrialDiscoverPage|Discover Premium & Ultimate')) + expect(rendered).to have_text(s_('TrialDiscoverPage|Access advanced features')) + expect(rendered).to render_template('groups/discovers/_discover_page_actions') + end + + it 'renders all feature cards' do + render + + expect(rendered).to have_text(s_("TrialDiscoverPage|Epics")) + expect(rendered).to have_text(s_("TrialDiscoverPage|Roadmaps")) + expect(rendered).to have_text(s_("TrialDiscoverPage|Scoped Labels")) + expect(rendered).to have_text(s_("TrialDiscoverPage|Merge request approval rule")) + expect(rendered).to have_text(s_("TrialDiscoverPage|Burn down charts")) + expect(rendered).to have_text(s_("TrialDiscoverPage|Code owners")) + expect(rendered).to have_text(s_("TrialDiscoverPage|Code review analytics")) + expect(rendered).to have_text(s_("TrialDiscoverPage|Free guest users")) + expect(rendered).to have_text(s_("TrialDiscoverPage|Dependency scanning")) + expect(rendered).to have_text(s_("TrialDiscoverPage|Dynamic application security testing (DAST)")) + expect(rendered).to have_text(s_("TrialDiscoverPage|Collaboration made easy")) + expect(rendered).to have_text(s_("TrialDiscoverPage|Lower cost of development")) + expect(rendered).to have_text(s_("TrialDiscoverPage|Your software, deployed your way")) + end + + context 'when trial is active' do + before do + allow(group).to receive(:trial_active?).and_return(true) + end + + it 'has tracking items set as expected' do + render + + tracking_labels.each do |label| + expect_to_have_tracking(action: 'click_video_link_trial_active', label: label) + expect_to_have_tracking(action: 'click_documentation_link_trial_active', label: label) + end + end + + it 'has tracking for Free guest users' do + render + + expect_to_have_tracking(action: 'click_calculate_seats_trial_active', label: :free_guests_feature) + expect_to_have_tracking(action: 'click_documentation_link_trial_active', label: :free_guests_feature) + end + + it 'has tracking for page actions' do + render + + expect_to_have_tracking(action: 'click_compare_plans', label: :trial_active) + expect_to_have_tracking(action: 'click_contact_sales', label: :trial_active) + end + end + + context 'when trial is expired' do + before do + allow(group).to receive(:trial_active?).and_return(false) + end + + it 'has tracking items set as expected' do + render + + tracking_labels.each do |label| + expect_to_have_tracking(action: 'click_video_link_trial_expired', label: label) + expect_to_have_tracking(action: 'click_documentation_link_trial_expired', label: label) + end + end + + it 'has tracking for Free guest users' do + render + + expect_to_have_tracking(action: 'click_calculate_seats_trial_expired', label: :free_guests_feature) + expect_to_have_tracking(action: 'click_documentation_link_trial_expired', label: :free_guests_feature) + end + + it 'has tracking for page actions' do + render + + expect_to_have_tracking(action: 'click_compare_plans', label: :trial_expired) + expect_to_have_tracking(action: 'click_contact_sales', label: :trial_expired) + end + end + + def expect_to_have_tracking(action:, label: nil) + css = "[data-track-action='#{action}']" + css += "[data-track-label='#{label}']" if label + + expect(rendered).to have_css(css) + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2c617b32eb9ec..453830c711a10 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -52431,15 +52431,114 @@ msgstr "" msgid "Trending" msgstr "" +msgid "TrialDiscoverPage|A single application eliminates complex integrations, data checkpoints, and toolchain maintenance, resulting in greater productivity and lower cost." +msgstr "" + +msgid "TrialDiscoverPage|Access advanced features" +msgstr "" + msgid "TrialDiscoverPage|Access advanced features, build more efficiently, strengthen security and compliance." msgstr "" +msgid "TrialDiscoverPage|Break down silos to coordinate seamlessly across development, operations, and security with a consistent experience across the development lifecycle." +msgstr "" + +msgid "TrialDiscoverPage|Burn down charts" +msgstr "" + +msgid "TrialDiscoverPage|Calculate seats" +msgstr "" + +msgid "TrialDiscoverPage|Code owners" +msgstr "" + +msgid "TrialDiscoverPage|Code review analytics" +msgstr "" + +msgid "TrialDiscoverPage|Collaborate on high-level ideas that share a common theme. Use epics to group issues that cross milestones and projects." +msgstr "" + +msgid "TrialDiscoverPage|Collaboration made easy" +msgstr "" + +msgid "TrialDiscoverPage|Compare all plans" +msgstr "" + +msgid "TrialDiscoverPage|Create a more advanced workflow for issues, merge requests, and epics by using scoped, mutually exclusive labels." +msgstr "" + +msgid "TrialDiscoverPage|Dependency scanning" +msgstr "" + msgid "TrialDiscoverPage|Discover" msgstr "" msgid "TrialDiscoverPage|Discover Premium & Ultimate" msgstr "" +msgid "TrialDiscoverPage|Documentation" +msgstr "" + +msgid "TrialDiscoverPage|Dynamic application security testing (DAST)" +msgstr "" + +msgid "TrialDiscoverPage|Epics" +msgstr "" + +msgid "TrialDiscoverPage|Find and fix bottlenecks in your code review process by understanding how long open merge requests have been in review." +msgstr "" + +msgid "TrialDiscoverPage|Free guest users" +msgstr "" + +msgid "TrialDiscoverPage|GitLab is infrastructure agnostic. GitLab supports GCP, AWS, OpenShift, VMware, on-premises, bare metal, and more." +msgstr "" + +msgid "TrialDiscoverPage|Keep your application secure by checking your deployed environments for vulnerabilities." +msgstr "" + +msgid "TrialDiscoverPage|Keep your application secure by checking your libraries for vulnerabilities." +msgstr "" + +msgid "TrialDiscoverPage|Let users view what GitLab has to offer without using a subscription seat." +msgstr "" + +msgid "TrialDiscoverPage|Lower cost of development" +msgstr "" + +msgid "TrialDiscoverPage|Maintain high quality code by requiring approval from specific users on your merge requests." +msgstr "" + +msgid "TrialDiscoverPage|Merge request approval rule" +msgstr "" + +msgid "TrialDiscoverPage|Premium" +msgstr "" + +msgid "TrialDiscoverPage|Roadmaps" +msgstr "" + +msgid "TrialDiscoverPage|Scoped Labels" +msgstr "" + +msgid "TrialDiscoverPage|Speed. Efficiency. Trust." +msgstr "" + +msgid "TrialDiscoverPage|Target the right approvers for your merge request by assigning owners to specific files." +msgstr "" + +msgid "TrialDiscoverPage|Track your development progress by viewing issues in a burndown chart." +msgstr "" + +msgid "TrialDiscoverPage|Ultimate" +msgstr "" + +msgid "TrialDiscoverPage|Visualize your epics and milestones in a timeline." +msgstr "" + +msgid "TrialDiscoverPage|Your software, deployed your way" +msgstr "" + msgid "TrialRegistration|Start GitLab Ultimate free trial" msgstr "" -- GitLab