Commit 59506ba3 authored by Phil Hughes's avatar Phil Hughes

Merge branch '342853-track-saas-signup-events' into 'master'

Instrument GitLab.com New Subscription Purchase flow (SaaS)

See merge request gitlab-org/gitlab!74828
parents 42bcb6a1 88222f8b
...@@ -16,12 +16,18 @@ export const ERROR_LOADING_PAYMENT_FORM = s__( ...@@ -16,12 +16,18 @@ export const ERROR_LOADING_PAYMENT_FORM = s__(
'Checkout|Failed to load the payment form. Please try again.', 'Checkout|Failed to load the payment form. Please try again.',
); );
// The order of the steps in this array determines the flow of the application
/* eslint-disable @gitlab/require-i18n-strings */ /* eslint-disable @gitlab/require-i18n-strings */
export const STEP_SUBSCRIPTION_DETAILS = 'subscriptionDetails';
export const STEP_BILLING_ADDRESS = 'billingAddress';
export const STEP_PAYMENT_METHOD = 'paymentMethod';
export const STEP_CONFIRM_ORDER = 'confirmOrder';
// The order of the steps in this array determines the flow of the application
export const STEPS = [ export const STEPS = [
{ id: 'subscriptionDetails', __typename: 'Step' }, { id: STEP_SUBSCRIPTION_DETAILS, __typename: 'Step' },
{ id: 'billingAddress', __typename: 'Step' }, { id: STEP_BILLING_ADDRESS, __typename: 'Step' },
{ id: 'paymentMethod', __typename: 'Step' }, { id: STEP_PAYMENT_METHOD, __typename: 'Step' },
{ id: 'confirmOrder', __typename: 'Step' }, { id: STEP_CONFIRM_ORDER, __typename: 'Step' },
]; ];
export const TRACK_SUCCESS_MESSAGE = 'Success';
/* eslint-enable @gitlab/require-i18n-strings */ /* eslint-enable @gitlab/require-i18n-strings */
...@@ -3,6 +3,7 @@ import { mapState } from 'vuex'; ...@@ -3,6 +3,7 @@ import { mapState } from 'vuex';
import ProgressBar from 'ee/registrations/components/progress_bar.vue'; import ProgressBar from 'ee/registrations/components/progress_bar.vue';
import { STEPS, SUBSCRIPTON_FLOW_STEPS } from 'ee/registrations/constants'; import { STEPS, SUBSCRIPTON_FLOW_STEPS } from 'ee/registrations/constants';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import Tracking from '~/tracking';
import BillingAddress from 'jh_else_ee/subscriptions/new/components/checkout/billing_address.vue'; import BillingAddress from 'jh_else_ee/subscriptions/new/components/checkout/billing_address.vue';
import ConfirmOrder from './checkout/confirm_order.vue'; import ConfirmOrder from './checkout/confirm_order.vue';
import PaymentMethod from './checkout/payment_method.vue'; import PaymentMethod from './checkout/payment_method.vue';
...@@ -10,11 +11,15 @@ import SubscriptionDetails from './checkout/subscription_details.vue'; ...@@ -10,11 +11,15 @@ import SubscriptionDetails from './checkout/subscription_details.vue';
export default { export default {
components: { ProgressBar, SubscriptionDetails, BillingAddress, PaymentMethod, ConfirmOrder }, components: { ProgressBar, SubscriptionDetails, BillingAddress, PaymentMethod, ConfirmOrder },
mixins: [Tracking.mixin()],
currentStep: STEPS.checkout, currentStep: STEPS.checkout,
steps: SUBSCRIPTON_FLOW_STEPS, steps: SUBSCRIPTON_FLOW_STEPS,
computed: { computed: {
...mapState(['isNewUser']), ...mapState(['isNewUser']),
}, },
mounted() {
this.track('render', { label: 'saas_checkout' });
},
i18n: { i18n: {
checkout: s__('Checkout|Checkout'), checkout: s__('Checkout|Checkout'),
}, },
......
...@@ -2,10 +2,11 @@ ...@@ -2,10 +2,11 @@
import { GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui'; import { GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { STEPS } from 'ee/subscriptions/constants'; import { STEP_BILLING_ADDRESS } from 'ee/subscriptions/constants';
import Step from 'ee/vue_shared/purchase_flow/components/step.vue'; import Step from 'ee/vue_shared/purchase_flow/components/step.vue';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import Tracking from '~/tracking';
export default { export default {
components: { components: {
...@@ -17,6 +18,7 @@ export default { ...@@ -17,6 +18,7 @@ export default {
directives: { directives: {
autofocusonshow, autofocusonshow,
}, },
mixins: [Tracking.mixin()],
computed: { computed: {
...mapState([ ...mapState([
'country', 'country',
...@@ -117,6 +119,21 @@ export default { ...@@ -117,6 +119,21 @@ export default {
'updateCountryState', 'updateCountryState',
'updateZipCode', 'updateZipCode',
]), ]),
trackStepTransition() {
this.track('click_button', { label: 'select_country', property: this.country });
this.track('click_button', { label: 'state', property: this.countryState });
this.track('click_button', {
label: 'saas_checkout_postal_code',
property: this.zipCode,
});
this.track('click_button', { label: 'continue_payment' });
},
trackStepEdit() {
this.track('click_button', {
label: 'edit',
property: STEP_BILLING_ADDRESS,
});
},
}, },
i18n: { i18n: {
stepTitle: s__('Checkout|Billing address'), stepTitle: s__('Checkout|Billing address'),
...@@ -129,7 +146,7 @@ export default { ...@@ -129,7 +146,7 @@ export default {
stateSelectPrompt: s__('Checkout|Please select a state'), stateSelectPrompt: s__('Checkout|Please select a state'),
zipCodeLabel: s__('Checkout|Zip code'), zipCodeLabel: s__('Checkout|Zip code'),
}, },
stepId: STEPS[1].id, stepId: STEP_BILLING_ADDRESS,
}; };
</script> </script>
<template> <template>
...@@ -138,6 +155,8 @@ export default { ...@@ -138,6 +155,8 @@ export default {
:title="$options.i18n.stepTitle" :title="$options.i18n.stepTitle"
:is-valid="isValid" :is-valid="isValid"
:next-step-button-text="$options.i18n.nextStepButtonText" :next-step-button-text="$options.i18n.nextStepButtonText"
@nextStep="trackStepTransition"
@stepEdit="trackStepEdit"
> >
<template #body> <template #body>
<gl-form-group :label="$options.i18n.countryLabel" label-size="sm" class="mb-3"> <gl-form-group :label="$options.i18n.countryLabel" label-size="sm" class="mb-3">
......
<script> <script>
import { GlSprintf } from '@gitlab/ui'; import { GlSprintf } from '@gitlab/ui';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { STEPS } from 'ee/subscriptions/constants'; import { STEP_PAYMENT_METHOD, TRACK_SUCCESS_MESSAGE } from 'ee/subscriptions/constants';
import Step from 'ee/vue_shared/purchase_flow/components/step.vue'; import Step from 'ee/vue_shared/purchase_flow/components/step.vue';
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
import Tracking from '~/tracking';
import Zuora from './zuora.vue'; import Zuora from './zuora.vue';
export default { export default {
...@@ -12,6 +13,7 @@ export default { ...@@ -12,6 +13,7 @@ export default {
Step, Step,
Zuora, Zuora,
}, },
mixins: [Tracking.mixin()],
computed: { computed: {
...mapState(['paymentMethodId', 'creditCardDetails']), ...mapState(['paymentMethodId', 'creditCardDetails']),
isValid() { isValid() {
...@@ -24,18 +26,43 @@ export default { ...@@ -24,18 +26,43 @@ export default {
}); });
}, },
}, },
methods: {
trackStepSuccess() {
this.track('click_button', {
label: 'review_order',
property: TRACK_SUCCESS_MESSAGE,
});
},
trackStepError(errorMessage) {
this.track('click_button', {
label: 'review_order',
property: errorMessage,
});
},
trackStepEdit() {
this.track('click_button', {
label: 'edit',
property: STEP_PAYMENT_METHOD,
});
},
},
i18n: { i18n: {
stepTitle: s__('Checkout|Payment method'), stepTitle: s__('Checkout|Payment method'),
creditCardDetails: s__('Checkout|%{cardType} ending in %{lastFourDigits}'), creditCardDetails: s__('Checkout|%{cardType} ending in %{lastFourDigits}'),
expirationDate: s__('Checkout|Exp %{expirationMonth}/%{expirationYear}'), expirationDate: s__('Checkout|Exp %{expirationMonth}/%{expirationYear}'),
}, },
stepId: STEPS[2].id, stepId: STEP_PAYMENT_METHOD,
}; };
</script> </script>
<template> <template>
<step :step-id="$options.stepId" :title="$options.i18n.stepTitle" :is-valid="isValid"> <step
:step-id="$options.stepId"
:title="$options.i18n.stepTitle"
:is-valid="isValid"
@stepEdit="trackStepEdit"
>
<template #body="props"> <template #body="props">
<zuora :active="props.active" /> <zuora :active="props.active" @success="trackStepSuccess" @error="trackStepError" />
</template> </template>
<template #summary> <template #summary>
<div class="js-summary-line-1"> <div class="js-summary-line-1">
......
...@@ -2,11 +2,12 @@ ...@@ -2,11 +2,12 @@
import { GlFormGroup, GlFormSelect, GlFormInput, GlSprintf, GlLink } from '@gitlab/ui'; import { GlFormGroup, GlFormSelect, GlFormInput, GlSprintf, GlLink } from '@gitlab/ui';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import { STEPS } from 'ee/subscriptions/constants'; import { STEP_SUBSCRIPTION_DETAILS } from 'ee/subscriptions/constants';
import { NEW_GROUP } from 'ee/subscriptions/new/constants'; import { NEW_GROUP } from 'ee/subscriptions/new/constants';
import Step from 'ee/vue_shared/purchase_flow/components/step.vue'; import Step from 'ee/vue_shared/purchase_flow/components/step.vue';
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import Tracking from '~/tracking';
export default { export default {
components: { components: {
...@@ -20,6 +21,7 @@ export default { ...@@ -20,6 +21,7 @@ export default {
directives: { directives: {
autofocusonshow, autofocusonshow,
}, },
mixins: [Tracking.mixin()],
computed: { computed: {
...mapState([ ...mapState([
'availablePlans', 'availablePlans',
...@@ -33,6 +35,8 @@ export default { ...@@ -33,6 +35,8 @@ export default {
]), ]),
...mapGetters([ ...mapGetters([
'selectedPlanText', 'selectedPlanText',
'selectedPlanDetails',
'selectedGroupId',
'isGroupSelected', 'isGroupSelected',
'selectedGroupUsers', 'selectedGroupUsers',
'selectedGroupName', 'selectedGroupName',
...@@ -134,6 +138,21 @@ export default { ...@@ -134,6 +138,21 @@ export default {
'updateNumberOfUsers', 'updateNumberOfUsers',
'updateOrganizationName', 'updateOrganizationName',
]), ]),
trackStepTransition() {
this.track('click_button', {
label: 'update_plan_type',
property: this.selectedPlanDetails.code,
});
this.track('click_button', { label: 'update_group', property: this.selectedGroupId });
this.track('click_button', { label: 'update_seat_count', property: this.numberOfUsers });
this.track('click_button', { label: 'continue_billing' });
},
trackStepEdit() {
this.track('click_button', {
label: 'edit',
property: STEP_SUBSCRIPTION_DETAILS,
});
},
}, },
i18n: { i18n: {
stepTitle: s__('Checkout|Subscription details'), stepTitle: s__('Checkout|Subscription details'),
...@@ -152,7 +171,7 @@ export default { ...@@ -152,7 +171,7 @@ export default {
group: s__('Checkout|Group'), group: s__('Checkout|Group'),
users: s__('Checkout|Users'), users: s__('Checkout|Users'),
}, },
stepId: STEPS[0].id, stepId: STEP_SUBSCRIPTION_DETAILS,
}; };
</script> </script>
<template> <template>
...@@ -161,6 +180,8 @@ export default { ...@@ -161,6 +180,8 @@ export default {
:title="$options.i18n.stepTitle" :title="$options.i18n.stepTitle"
:is-valid="isValid" :is-valid="isValid"
:next-step-button-text="$options.i18n.nextStepButtonText" :next-step-button-text="$options.i18n.nextStepButtonText"
@nextStep="trackStepTransition"
@stepEdit="trackStepEdit"
> >
<template #body> <template #body>
<gl-form-group :label="$options.i18n.selectedPlanLabel" label-size="sm" class="mb-3"> <gl-form-group :label="$options.i18n.selectedPlanLabel" label-size="sm" class="mb-3">
......
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import Tracking from '~/tracking';
import { ZUORA_SCRIPT_URL, ZUORA_IFRAME_OVERRIDE_PARAMS } from 'ee/subscriptions/constants'; import { ZUORA_SCRIPT_URL, ZUORA_IFRAME_OVERRIDE_PARAMS } from 'ee/subscriptions/constants';
export default { export default {
components: { components: {
GlLoadingIcon, GlLoadingIcon,
}, },
mixins: [Tracking.mixin()],
props: { props: {
active: { active: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
}, },
emits: ['success', 'error'],
computed: { computed: {
...mapState([ ...mapState([
'paymentFormParams', 'paymentFormParams',
...@@ -51,10 +54,18 @@ export default { ...@@ -51,10 +54,18 @@ export default {
this.fetchPaymentFormParams(); this.fetchPaymentFormParams();
} }
}, },
handleZuoraCallback(response) {
this.paymentFormSubmitted(response);
if (response?.success === 'true') {
this.$emit('success');
} else {
this.$emit('error', response?.errorMessage);
}
},
renderZuoraIframe() { renderZuoraIframe() {
const params = { ...this.paymentFormParams, ...ZUORA_IFRAME_OVERRIDE_PARAMS }; const params = { ...this.paymentFormParams, ...ZUORA_IFRAME_OVERRIDE_PARAMS };
window.Z.runAfterRender(this.zuoraIframeRendered); window.Z.runAfterRender(this.zuoraIframeRendered);
window.Z.render(params, {}, this.paymentFormSubmitted); window.Z.render(params, {}, this.handleZuoraCallback);
}, },
}, },
}; };
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { GlLink, GlSprintf } from '@gitlab/ui'; import { GlLink, GlSprintf } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import Tracking from '~/tracking';
import formattingMixins from '../../formatting_mixins'; import formattingMixins from '../../formatting_mixins';
export default { export default {
...@@ -9,7 +10,7 @@ export default { ...@@ -9,7 +10,7 @@ export default {
GlLink, GlLink,
GlSprintf, GlSprintf,
}, },
mixins: [formattingMixins], mixins: [formattingMixins, Tracking.mixin()],
computed: { computed: {
...mapState(['startDate', 'taxRate', 'numberOfUsers']), ...mapState(['startDate', 'taxRate', 'numberOfUsers']),
...mapGetters([ ...mapGetters([
...@@ -81,6 +82,7 @@ export default { ...@@ -81,6 +82,7 @@ export default {
href="https://about.gitlab.com/handbook/tax/#indirect-taxes-management" href="https://about.gitlab.com/handbook/tax/#indirect-taxes-management"
target="_blank" target="_blank"
data-testid="tax-help-link" data-testid="tax-help-link"
@click="track('click_button', { label: 'tax_link' })"
>{{ content }}</gl-link >{{ content }}</gl-link
> >
</template> </template>
......
...@@ -72,9 +72,9 @@ export default { ...@@ -72,9 +72,9 @@ export default {
throw new Error(JSON.stringify(data.errors)); throw new Error(JSON.stringify(data.errors));
} }
}) })
.catch((error) => .catch((error) => {
createFlash({ message: GENERAL_ERROR_MESSAGE, error, captureError: true }), createFlash({ message: GENERAL_ERROR_MESSAGE, error, captureError: true });
) })
.finally(() => { .finally(() => {
this.isLoading = false; this.isLoading = false;
}); });
......
...@@ -41,6 +41,7 @@ export default { ...@@ -41,6 +41,7 @@ export default {
default: '', default: '',
}, },
}, },
emits: ['nextStep', 'stepEdit'],
data() { data() {
return { return {
activeStep: {}, activeStep: {},
...@@ -71,7 +72,7 @@ export default { ...@@ -71,7 +72,7 @@ export default {
const activeIndex = this.stepList.findIndex(({ id }) => id === this.activeStep.id); const activeIndex = this.stepList.findIndex(({ id }) => id === this.activeStep.id);
return this.isFinished && index < activeIndex; return this.isFinished && index < activeIndex;
}, },
snakeCasedStep() { dasherizedStep() {
return dasherize(convertToSnakeCase(this.stepId)); return dasherize(convertToSnakeCase(this.stepId));
}, },
}, },
...@@ -93,10 +94,12 @@ export default { ...@@ -93,10 +94,12 @@ export default {
}) })
.finally(() => { .finally(() => {
this.loading = false; this.loading = false;
this.$emit('nextStep');
}); });
}, },
async edit() { async edit() {
this.loading = true; this.loading = true;
this.$emit('stepEdit', this.stepId);
await this.$apollo await this.$apollo
.mutate({ .mutate({
mutation: updateStepMutation, mutation: updateStepMutation,
...@@ -115,7 +118,7 @@ export default { ...@@ -115,7 +118,7 @@ export default {
<template> <template>
<div class="mb-3 mb-lg-5 gl-w-full"> <div class="mb-3 mb-lg-5 gl-w-full">
<step-header :title="title" :is-finished="isFinished" /> <step-header :title="title" :is-finished="isFinished" />
<div :class="['card', snakeCasedStep]"> <div class="card" :class="dasherizedStep">
<div v-show="isActive" @keyup.enter="nextStep"> <div v-show="isActive" @keyup.enter="nextStep">
<slot name="body" :active="isActive"></slot> <slot name="body" :active="isActive"></slot>
<gl-form-group <gl-form-group
......
...@@ -83,7 +83,11 @@ class SubscriptionsController < ApplicationController ...@@ -83,7 +83,11 @@ class SubscriptionsController < ApplicationController
group = params[:selected_group] ? current_group : create_group group = params[:selected_group] ? current_group : create_group
return not_found if group.nil? return not_found if group.nil?
return render json: group.errors.to_json unless group.persisted?
unless group.persisted?
track_purchase message: group.errors.full_messages.to_s
return render json: group.errors.to_json
end
response = Subscriptions::CreateService.new( response = Subscriptions::CreateService.new(
current_user, current_user,
...@@ -93,7 +97,10 @@ class SubscriptionsController < ApplicationController ...@@ -93,7 +97,10 @@ class SubscriptionsController < ApplicationController
).execute ).execute
if response[:success] if response[:success]
track_purchase message: 'Success', namespace: group
response[:data] = { location: redirect_location(group) } response[:data] = { location: redirect_location(group) }
else
track_purchase message: response.dig(:data, :errors), namespace: group
end end
render json: response[:data] render json: response[:data]
...@@ -101,6 +108,15 @@ class SubscriptionsController < ApplicationController ...@@ -101,6 +108,15 @@ class SubscriptionsController < ApplicationController
private private
def track_purchase(message:, namespace: nil)
Gitlab::Tracking.event(self.class.name, 'click_button',
label: 'confirm_purchase',
property: message,
user: current_user,
namespace: namespace
)
end
def redirect_location(group) def redirect_location(group)
return safe_redirect_path(params[:redirect_after_success]) if params[:redirect_after_success] return safe_redirect_path(params[:redirect_after_success]) if params[:redirect_after_success]
......
!!! 5 !!! 5
%html.subscriptions-layout-html{ lang: 'en' } %html.subscriptions-layout-html{ lang: 'en' }
= render 'layouts/head' = render 'layouts/head'
%body.ui-indigo.d-flex.vh-100 %body.ui-indigo.d-flex.vh-100{ data: body_data }
= render "layouts/header/logo_with_title" = render "layouts/header/logo_with_title"
= render "layouts/broadcast" = render "layouts/broadcast"
.container.d-flex.gl-flex-direction-column.m-0 .container.d-flex.gl-flex-direction-column.m-0
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
- if show_plans?(namespace) - if show_plans?(namespace)
- plans = billing_available_plans(plans_data, current_plan) - plans = billing_available_plans(plans_data, current_plan)
.billing-plans.gl-mt-7 .billing-plans.gl-mt-7{ data: { track: { action: 'render', label: 'billing' } } }
- plans.each do |plan| - plans.each do |plan|
- next if plan.hide_card - next if plan.hide_card
- is_default_plan = current_plan.nil? && plan.default? - is_default_plan = current_plan.nil? && plan.default?
......
...@@ -264,7 +264,7 @@ RSpec.describe SubscriptionsController do ...@@ -264,7 +264,7 @@ RSpec.describe SubscriptionsController do
end end
end end
describe 'POST #create' do describe 'POST #create', :snowplow do
subject do subject do
post :create, post :create,
params: params, params: params,
...@@ -350,6 +350,20 @@ RSpec.describe SubscriptionsController do ...@@ -350,6 +350,20 @@ RSpec.describe SubscriptionsController do
expect(Gitlab::Json.parse(response.body)['name']).to match_array([Gitlab::Regex.group_name_regex_message, HtmlSafetyValidator.error_message]) expect(Gitlab::Json.parse(response.body)['name']).to match_array([Gitlab::Regex.group_name_regex_message, HtmlSafetyValidator.error_message])
end end
it 'tracks errors' do
group.valid?
subject
expect_snowplow_event(
category: 'SubscriptionsController',
label: 'confirm_purchase',
action: 'click_button',
property: group.errors.full_messages.to_s,
user: user,
namespace: nil
)
end
end end
end end
...@@ -372,6 +386,14 @@ RSpec.describe SubscriptionsController do ...@@ -372,6 +386,14 @@ RSpec.describe SubscriptionsController do
subject subject
expect(response.body).to eq('{"errors":"error message"}') expect(response.body).to eq('{"errors":"error message"}')
expect_snowplow_event(
category: 'SubscriptionsController',
label: 'confirm_purchase',
action: 'click_button',
property: 'error message',
user: user,
namespace: group
)
end end
end end
...@@ -430,6 +452,19 @@ RSpec.describe SubscriptionsController do ...@@ -430,6 +452,19 @@ RSpec.describe SubscriptionsController do
expect(response.body).to eq({ location: redirect_after_success }.to_json) expect(response.body).to eq({ location: redirect_after_success }.to_json)
end end
it 'tracks the creation of the subscriptions' do
subject
expect_snowplow_event(
category: 'SubscriptionsController',
label: 'confirm_purchase',
action: 'click_button',
property: 'Success',
namespace: selected_group,
user: user
)
end
end end
end end
......
...@@ -3,6 +3,7 @@ import Vue, { nextTick } from 'vue'; ...@@ -3,6 +3,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
import { STEPS } from 'ee/subscriptions/constants'; import { STEPS } from 'ee/subscriptions/constants';
import BillingAddress from 'ee/subscriptions/new/components/checkout/billing_address.vue'; import BillingAddress from 'ee/subscriptions/new/components/checkout/billing_address.vue';
import { getStoreConfig } from 'ee/subscriptions/new/store'; import { getStoreConfig } from 'ee/subscriptions/new/store';
...@@ -83,6 +84,48 @@ describe('Billing Address', () => { ...@@ -83,6 +84,48 @@ describe('Billing Address', () => {
}); });
}); });
describe('tracking', () => {
beforeEach(() => {
store.commit(types.UPDATE_COUNTRY, 'US');
store.commit(types.UPDATE_ZIP_CODE, '10467');
store.commit(types.UPDATE_COUNTRY_STATE, 'NY');
});
it('tracks completion details', () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.findComponent(Step).vm.$emit('nextStep');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'select_country',
property: 'US',
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'state',
property: 'NY',
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'saas_checkout_postal_code',
property: '10467',
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'continue_payment',
});
});
it('tracks step edits', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.findComponent(Step).vm.$emit('stepEdit', 'stepID');
await nextTick();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'edit',
property: 'billingAddress',
});
});
});
describe('validations', () => { describe('validations', () => {
const isStepValid = () => wrapper.findComponent(Step).props('isValid'); const isStepValid = () => wrapper.findComponent(Step).props('isValid');
......
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { triggerEvent, mockTracking, unmockTracking } from 'helpers/tracking_helper';
import Component from 'ee/subscriptions/new/components/order_summary.vue'; import Component from 'ee/subscriptions/new/components/order_summary.vue';
import createStore from 'ee/subscriptions/new/store'; import createStore from 'ee/subscriptions/new/store';
import * as types from 'ee/subscriptions/new/store/mutation_types'; import * as types from 'ee/subscriptions/new/store/mutation_types';
...@@ -9,6 +10,7 @@ describe('Order Summary', () => { ...@@ -9,6 +10,7 @@ describe('Order Summary', () => {
Vue.use(Vuex); Vue.use(Vuex);
let wrapper; let wrapper;
let trackingSpy;
const availablePlans = [ const availablePlans = [
{ id: 'firstPlanId', code: 'bronze', price_per_year: 48, name: 'bronze plan' }, { id: 'firstPlanId', code: 'bronze', price_per_year: 48, name: 'bronze plan' },
...@@ -36,9 +38,11 @@ describe('Order Summary', () => { ...@@ -36,9 +38,11 @@ describe('Order Summary', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
}); });
afterEach(() => { afterEach(() => {
unmockTracking();
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -188,6 +192,17 @@ describe('Order Summary', () => { ...@@ -188,6 +192,17 @@ describe('Order Summary', () => {
}); });
describe('tax rate', () => { describe('tax rate', () => {
describe('tracking', () => {
it('track click on tax_link', () => {
trackingSpy = mockTracking(undefined, findTaxHelpLink().element, jest.spyOn);
triggerEvent(findTaxHelpLink().element);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'tax_link',
});
});
});
describe('with a tax rate of 0', () => { describe('with a tax rate of 0', () => {
it('displays the total amount excluding vat', () => { it('displays the total amount excluding vat', () => {
expect(wrapper.find('.js-total-ex-vat').exists()).toBe(true); expect(wrapper.find('.js-total-ex-vat').exists()).toBe(true);
......
...@@ -2,6 +2,8 @@ import { mount } from '@vue/test-utils'; ...@@ -2,6 +2,8 @@ import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import Vuex from 'vuex'; import Vuex from 'vuex';
import Zuora from 'ee/subscriptions/new/components/checkout/zuora.vue';
import { mockTracking } from 'helpers/tracking_helper';
import { STEPS } from 'ee/subscriptions/constants'; import { STEPS } from 'ee/subscriptions/constants';
import PaymentMethod from 'ee/subscriptions/new/components/checkout/payment_method.vue'; import PaymentMethod from 'ee/subscriptions/new/components/checkout/payment_method.vue';
import createStore from 'ee/subscriptions/new/store'; import createStore from 'ee/subscriptions/new/store';
...@@ -68,4 +70,42 @@ describe('Payment Method', () => { ...@@ -68,4 +70,42 @@ describe('Payment Method', () => {
expect(wrapper.find('.js-summary-line-2').text()).toEqual('Exp 12/09'); expect(wrapper.find('.js-summary-line-2').text()).toEqual('Exp 12/09');
}); });
}); });
describe('tracking', () => {
it('tracks step completion details', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.findComponent(Zuora).vm.$emit('success');
await nextTick();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'review_order',
property: 'Success',
});
});
it('tracks zuora errors on step transition', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.findComponent(Zuora).vm.$emit('error', 'This was a mistake.');
await nextTick();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'review_order',
property: 'This was a mistake.',
});
});
it('tracks step edits', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.findComponent(Step).vm.$emit('stepEdit', 'stepID');
await nextTick();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'edit',
property: 'paymentMethod',
});
});
});
}); });
...@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'; ...@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
import { STEPS } from 'ee/subscriptions/constants'; import { STEPS } from 'ee/subscriptions/constants';
import Component from 'ee/subscriptions/new/components/checkout/subscription_details.vue'; import Component from 'ee/subscriptions/new/components/checkout/subscription_details.vue';
import { NEW_GROUP } from 'ee/subscriptions/new/constants'; import { NEW_GROUP } from 'ee/subscriptions/new/constants';
...@@ -340,6 +341,58 @@ describe('Subscription Details', () => { ...@@ -340,6 +341,58 @@ describe('Subscription Details', () => {
}); });
}); });
describe('tracking', () => {
let store;
beforeEach(() => {
const mockApollo = createMockApolloProvider(STEPS);
store = createStore(
createDefaultInitialStoreData({
isNewUser: 'false',
namespaceId: '132',
setupForCompany: 'false',
}),
);
store.commit(types.UPDATE_NUMBER_OF_USERS, 13);
wrapper = createComponent({ apolloProvider: mockApollo, store });
});
it('tracks completion details', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.findComponent(Step).vm.$emit('nextStep');
await nextTick();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'update_plan_type',
property: 'silver',
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'update_seat_count',
property: 13,
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'update_group',
property: 132,
});
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'continue_billing',
});
});
it('tracks step edits', async () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.findComponent(Step).vm.$emit('stepEdit', 'stepID');
await nextTick();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'edit',
property: 'subscriptionDetails',
});
});
});
describe('validations', () => { describe('validations', () => {
const isStepValid = () => wrapper.findComponent(Step).props('isValid'); const isStepValid = () => wrapper.findComponent(Step).props('isValid');
let store; let store;
......
...@@ -117,7 +117,25 @@ describe('Zuora', () => { ...@@ -117,7 +117,25 @@ describe('Zuora', () => {
return nextTick().then(() => { return nextTick().then(() => {
expect(actionMocks.zuoraIframeRendered).toHaveBeenCalled(); expect(actionMocks.zuoraIframeRendered).toHaveBeenCalled();
wrapper.vm.handleZuoraCallback();
expect(actionMocks.paymentFormSubmitted).toHaveBeenCalled();
}); });
}); });
}); });
describe('tracking', () => {
it('emits success event on correct response', async () => {
wrapper.vm.handleZuoraCallback({ success: 'true' });
await nextTick();
expect(wrapper.emitted().success.length).toEqual(1);
});
it('emits error with message', async () => {
createComponent();
wrapper.vm.handleZuoraCallback({ errorMessage: '1337' });
await nextTick();
expect(wrapper.emitted().error.length).toEqual(1);
expect(wrapper.emitted().error[0]).toEqual(['1337']);
});
});
}); });
...@@ -4,6 +4,7 @@ import Vuex from 'vuex'; ...@@ -4,6 +4,7 @@ import Vuex from 'vuex';
import ProgressBar from 'ee/registrations/components/progress_bar.vue'; import ProgressBar from 'ee/registrations/components/progress_bar.vue';
import Component from 'ee/subscriptions/new/components/checkout.vue'; import Component from 'ee/subscriptions/new/components/checkout.vue';
import createStore from 'ee/subscriptions/new/store'; import createStore from 'ee/subscriptions/new/store';
import { mockTracking } from 'helpers/tracking_helper';
describe('Checkout', () => { describe('Checkout', () => {
Vue.use(Vuex); Vue.use(Vuex);
...@@ -58,4 +59,16 @@ describe('Checkout', () => { ...@@ -58,4 +59,16 @@ describe('Checkout', () => {
expect(findProgressBar().props('currentStep')).toEqual('Checkout'); expect(findProgressBar().props('currentStep')).toEqual('Checkout');
}); });
}); });
describe('tracking', () => {
it('tracks render on mount', () => {
const trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
shallowMount(Component, { store: createStore() });
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'render', {
label: 'saas_checkout',
});
});
});
}); });
...@@ -17,7 +17,6 @@ jest.mock('~/flash'); ...@@ -17,7 +17,6 @@ jest.mock('~/flash');
describe('Step', () => { describe('Step', () => {
let wrapper; let wrapper;
const initialProps = { const initialProps = {
stepId: STEPS[1].id, stepId: STEPS[1].id,
isValid: true, isValid: true,
...@@ -33,6 +32,7 @@ describe('Step', () => { ...@@ -33,6 +32,7 @@ describe('Step', () => {
} }
function createComponent(options = {}) { function createComponent(options = {}) {
const { apolloProvider, propsData } = options; const { apolloProvider, propsData } = options;
return shallowMount(Step, { return shallowMount(Step, {
propsData: { ...initialProps, ...propsData }, propsData: { ...initialProps, ...propsData },
apolloProvider, apolloProvider,
...@@ -198,4 +198,28 @@ describe('Step', () => { ...@@ -198,4 +198,28 @@ describe('Step', () => {
}); });
}); });
}); });
describe('tracking', () => {
it('emits stepEdit', async () => {
const mockApollo = createMockApolloProvider(STEPS, 1);
wrapper = createComponent({ propsData: { stepId: STEPS[1].id }, apolloProvider: mockApollo });
// button in step-summary is not rendered b/c of shallowMount
wrapper.vm.edit();
await waitForPromises();
expect(wrapper.emitted().stepEdit[0]).toEqual(['secondStep']);
});
it('emits nextStep on step transition', async () => {
const mockApollo = createMockApolloProvider(STEPS, 1);
wrapper = createComponent({ propsData: { stepId: STEPS[1].id }, apolloProvider: mockApollo });
await activateFirstStep(mockApollo);
wrapper.findComponent(GlButton).vm.$emit('click');
await waitForPromises();
expect(wrapper.emitted().nextStep).toBeTruthy();
});
});
}); });
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