Commit 39fd54b8 authored by nicolasdular's avatar nicolasdular

Add paid signup tracking events

This adds the required tracking for the new paid signup flow.
We're using frontend tracking as well because it gives us a user
session. For users who have not activated frontend tracking, we
can fallback to backend tracking.

Two things to be aware of:

1. Users who go to /-/subscriptions/new who are not signed in, get
redirected to /users/sign_up. After they successfully created an
account or signed in, we redirect them to /-/subscriptions/new again.
However, when they go to -/subscriptions/new, we would track the
experiment_start event a second time. Therefore, we added the param
experiment_started to not call track them twice

2. For the sign_up_view event, we only want to track users who got
redirected from /-/subscriptions/new, therefore we have added the
redirected_from=checkout param.
parent 3c1e19ba
import LengthValidator from '~/pages/sessions/new/length_validator'; import LengthValidator from '~/pages/sessions/new/length_validator';
import UsernameValidator from '~/pages/sessions/new/username_validator'; import UsernameValidator from '~/pages/sessions/new/username_validator';
import NoEmojiValidator from '~/emoji/no_emoji_validator'; import NoEmojiValidator from '~/emoji/no_emoji_validator';
import Tracking from '~/tracking';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new UsernameValidator(); // eslint-disable-line no-new new UsernameValidator(); // eslint-disable-line no-new
new LengthValidator(); // eslint-disable-line no-new new LengthValidator(); // eslint-disable-line no-new
new NoEmojiValidator(); // eslint-disable-line no-new new NoEmojiValidator(); // eslint-disable-line no-new
}); });
document.addEventListener('SnowplowInitialized', () => {
if (gon.tracking_data) {
const { category, action } = gon.tracking_data;
if (category && action) {
Tracking.event(category, action);
}
}
});
...@@ -7,6 +7,7 @@ import SubscriptionDetails from './checkout/subscription_details.vue'; ...@@ -7,6 +7,7 @@ import SubscriptionDetails from './checkout/subscription_details.vue';
import BillingAddress from './checkout/billing_address.vue'; import BillingAddress from './checkout/billing_address.vue';
import PaymentMethod from './checkout/payment_method.vue'; import PaymentMethod from './checkout/payment_method.vue';
import ConfirmOrder from './checkout/confirm_order.vue'; import ConfirmOrder from './checkout/confirm_order.vue';
import Tracking from '~/tracking';
export default { export default {
components: { ProgressBar, SubscriptionDetails, BillingAddress, PaymentMethod, ConfirmOrder }, components: { ProgressBar, SubscriptionDetails, BillingAddress, PaymentMethod, ConfirmOrder },
...@@ -18,6 +19,15 @@ export default { ...@@ -18,6 +19,15 @@ export default {
computed: { computed: {
...mapState(['isNewUser']), ...mapState(['isNewUser']),
}, },
created() {
document.addEventListener('SnowplowInitialized', () => {
Tracking.event('Growth::Acquisition::Experiment::PaidSignUpFlow', 'start', {
label: null,
property: null,
value: null,
});
});
},
i18n: { i18n: {
checkout: s__('Checkout|Checkout'), checkout: s__('Checkout|Checkout'),
}, },
......
...@@ -4,6 +4,7 @@ import createFlash from '~/flash'; ...@@ -4,6 +4,7 @@ import createFlash from '~/flash';
import Api from 'ee/api'; import Api from 'ee/api';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import { STEPS, PAYMENT_FORM_ID } from '../constants'; import { STEPS, PAYMENT_FORM_ID } from '../constants';
import Tracking from '~/tracking';
export const activateStep = ({ commit }, currentStep) => { export const activateStep = ({ commit }, currentStep) => {
if (STEPS.includes(currentStep)) { if (STEPS.includes(currentStep)) {
...@@ -184,13 +185,26 @@ export const confirmOrder = ({ getters, dispatch, commit }) => { ...@@ -184,13 +185,26 @@ export const confirmOrder = ({ getters, dispatch, commit }) => {
Api.confirmOrder(getters.confirmOrderParams) Api.confirmOrder(getters.confirmOrderParams)
.then(({ data }) => { .then(({ data }) => {
if (data.location) dispatch('confirmOrderSuccess', data.location); if (data.location) {
else dispatch('confirmOrderError', JSON.stringify(data.errors)); dispatch('confirmOrderSuccess', {
location: data.location,
plan_id: data.plan_id,
quantity: data.quantity,
});
} else {
dispatch('confirmOrderError', JSON.stringify(data.errors));
}
}) })
.catch(() => dispatch('confirmOrderError')); .catch(() => dispatch('confirmOrderError'));
}; };
export const confirmOrderSuccess = (_, location) => { export const confirmOrderSuccess = (_, { location, plan_id, quantity }) => {
Tracking.event('Growth::Acquisition::Experiment::PaidSignUpFlow', 'end', {
label: plan_id,
property: null,
value: quantity,
});
redirectTo(location); redirectTo(location);
}; };
......
...@@ -5,6 +5,10 @@ module EE ...@@ -5,6 +5,10 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
prepended do
before_action :set_frontend_tracking_data, only: [:new]
end
private private
override :user_created_message override :user_created_message
...@@ -35,5 +39,9 @@ module EE ...@@ -35,5 +39,9 @@ module EE
super super
end end
end end
def set_frontend_tracking_data
frontend_experimentation_tracking_data(:paid_signup_flow, 'sign_up_page_view') if params[:redirect_from] == 'checkout'
end
end end
end end
...@@ -24,11 +24,13 @@ class SubscriptionsController < ApplicationController ...@@ -24,11 +24,13 @@ class SubscriptionsController < ApplicationController
def new def new
if experiment_enabled?(:paid_signup_flow) if experiment_enabled?(:paid_signup_flow)
track_paid_signup_flow_event('start_experiment') unless experiment_already_started?
if current_user if current_user
track_paid_signup_flow_event('start') track_paid_signup_flow_event('start')
else else
store_location_for :user, request.fullpath store_location_for_user
redirect_to new_user_registration_path redirect_to new_user_registration_path(redirect_from: 'checkout')
end end
else else
redirect_to customer_portal_new_subscription_url redirect_to customer_portal_new_subscription_url
...@@ -65,14 +67,14 @@ class SubscriptionsController < ApplicationController ...@@ -65,14 +67,14 @@ class SubscriptionsController < ApplicationController
).execute ).execute
if response[:success] if response[:success]
plan_id, quantity = subscription_params.values_at(:plan_id, :quantity)
redirect_location = if params[:selected_group] redirect_location = if params[:selected_group]
group_path(group) group_path(group)
else else
plan_id, quantity = subscription_params.values_at(:plan_id, :quantity)
edit_subscriptions_group_path(group.path, plan_id: plan_id, quantity: quantity, new_user: params[:new_user]) edit_subscriptions_group_path(group.path, plan_id: plan_id, quantity: quantity, new_user: params[:new_user])
end end
response[:data] = { location: redirect_location } response[:data] = { location: redirect_location, plan_id: plan_id, quantity: quantity }
track_paid_signup_flow_event('end', label: plan_id, value: quantity) track_paid_signup_flow_event('end', label: plan_id, value: quantity)
end end
...@@ -103,6 +105,15 @@ class SubscriptionsController < ApplicationController ...@@ -103,6 +105,15 @@ class SubscriptionsController < ApplicationController
Gitlab::SubscriptionPortal::Client Gitlab::SubscriptionPortal::Client
end end
def store_location_for_user
redirect_url = url_for(safe_params.merge(experiment_started: true))
store_location_for :user, redirect_url
end
def experiment_already_started?
params[:experiment_started].present?
end
def customer_portal_new_subscription_url def customer_portal_new_subscription_url
"#{EE::SUBSCRIPTIONS_URL}/subscriptions/new?plan_id=#{params[:plan_id]}&transaction=create_subscription" "#{EE::SUBSCRIPTIONS_URL}/subscriptions/new?plan_id=#{params[:plan_id]}&transaction=create_subscription"
end end
......
...@@ -42,6 +42,35 @@ describe RegistrationsController do ...@@ -42,6 +42,35 @@ describe RegistrationsController do
end end
end end
describe '#new' do
before do
stub_experiment(signup_flow: true, paid_signup_flow: true)
stub_experiment_for_user(signup_flow: true, paid_signup_flow: true)
end
context 'when not redirected from checkout page' do
it 'does not push tracking data to gon' do
get :new
expect(Gon.tracking_data).to eq(nil)
end
end
context 'when redirect from checkout page' do
it 'pushes tracking data to gon' do
get :new, params: { redirect_from: 'checkout' }
expect(Gon.tracking_data).to include(
{
category: 'Growth::Acquisition::Experiment::PaidSignUpFlow',
action: 'sign_up_page_view',
property: 'experimental_group'
}
)
end
end
end
describe '#welcome' do describe '#welcome' do
subject { get :welcome } subject { get :welcome }
......
...@@ -14,18 +14,32 @@ describe SubscriptionsController do ...@@ -14,18 +14,32 @@ describe SubscriptionsController do
stub_experiment_for_user(paid_signup_flow: true) stub_experiment_for_user(paid_signup_flow: true)
end end
context 'with unauthorized user' do context 'with unauthenticated user' do
it { is_expected.to have_gitlab_http_status(:redirect) } it { is_expected.to have_gitlab_http_status(:redirect) }
it { is_expected.to redirect_to new_user_registration_path } it { is_expected.to redirect_to new_user_registration_path(redirect_from: 'checkout') }
it 'stores subscription URL for later' do it 'stores subscription URL for later' do
subject subject
expect(controller.stored_location_for(:user)).to eq(new_subscriptions_path(plan_id: 'bronze_id')) expected_subscription_path = new_subscriptions_path(experiment_started: true, plan_id: 'bronze_id')
expect(controller.stored_location_for(:user)).to eq(expected_subscription_path)
end end
it 'tracks the event when experiment starts' do
expect(Gitlab::Tracking).to receive(:event).with('Growth::Acquisition::Experiment::PaidSignUpFlow', 'start_experiment', label: nil, value: nil)
subject
end end
context 'with authorized user' do it 'does not track event when user got redirected to the subscription page again' do
get :new, params: { plan_id: 'bronze_id', experiment_started: 'true' }
expect(Gitlab::Tracking).not_to receive(:event).with('Growth::Acquisition::Experiment::PaidSignUpFlow', 'start_experiment', label: nil, value: nil)
end
end
context 'with authenticated user' do
before do before do
sign_in(user) sign_in(user)
end end
...@@ -34,6 +48,7 @@ describe SubscriptionsController do ...@@ -34,6 +48,7 @@ describe SubscriptionsController do
it { is_expected.to render_template :new } it { is_expected.to render_template :new }
it 'tracks the event with the right parameters' do it 'tracks the event with the right parameters' do
expect(Gitlab::Tracking).to receive(:event).with('Growth::Acquisition::Experiment::PaidSignUpFlow', 'start_experiment', label: nil, value: nil)
expect(Gitlab::Tracking).to receive(:event).with('Growth::Acquisition::Experiment::PaidSignUpFlow', 'start', label: nil, value: nil) expect(Gitlab::Tracking).to receive(:event).with('Growth::Acquisition::Experiment::PaidSignUpFlow', 'start', label: nil, value: nil)
subject subject
...@@ -197,7 +212,13 @@ describe SubscriptionsController do ...@@ -197,7 +212,13 @@ describe SubscriptionsController do
it 'returns the group edit location in JSON format' do it 'returns the group edit location in JSON format' do
subject subject
expect(response.body).to eq({ location: "/-/subscriptions/groups/#{group.path}/edit?plan_id=x&quantity=2" }.to_json) expected_response = {
location: "/-/subscriptions/groups/#{group.path}/edit?plan_id=x&quantity=2",
plan_id: 'x',
quantity: 2
}
expect(response.body).to eq(expected_response.to_json)
end end
end end
...@@ -234,7 +255,13 @@ describe SubscriptionsController do ...@@ -234,7 +255,13 @@ describe SubscriptionsController do
it 'returns the selected group location in JSON format' do it 'returns the selected group location in JSON format' do
subject subject
expect(response.body).to eq({ location: "/#{selected_group.path}" }.to_json) expected_response = {
location: "/#{selected_group.path}",
plan_id: 'x',
quantity: 1
}
expect(response.body).to eq(expected_response.to_json)
end end
end end
......
import Vuex from 'vuex'; import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { mockTracking } from 'helpers/tracking_helper';
import createStore from 'ee/subscriptions/new/store'; import createStore from 'ee/subscriptions/new/store';
import Component from 'ee/subscriptions/new/components/checkout.vue'; import Component from 'ee/subscriptions/new/components/checkout.vue';
import ProgressBar from 'ee/subscriptions/new/components/checkout/progress_bar.vue'; import ProgressBar from 'ee/subscriptions/new/components/checkout/progress_bar.vue';
...@@ -10,6 +11,7 @@ describe('Checkout', () => { ...@@ -10,6 +11,7 @@ describe('Checkout', () => {
let store; let store;
let wrapper; let wrapper;
let spy;
const createComponent = () => { const createComponent = () => {
wrapper = shallowMount(Component, { wrapper = shallowMount(Component, {
...@@ -20,6 +22,7 @@ describe('Checkout', () => { ...@@ -20,6 +22,7 @@ describe('Checkout', () => {
const findProgressBar = () => wrapper.find(ProgressBar); const findProgressBar = () => wrapper.find(ProgressBar);
beforeEach(() => { beforeEach(() => {
spy = mockTracking('Growth::Acquisition::Experiment::PaidSignUpFlow', null, jest.spyOn);
store = createStore(); store = createStore();
createComponent(); createComponent();
}); });
...@@ -28,6 +31,16 @@ describe('Checkout', () => { ...@@ -28,6 +31,16 @@ describe('Checkout', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it('sends tracking event when snowplow got initialized', () => {
document.dispatchEvent(new Event('SnowplowInitialized'));
expect(spy).toHaveBeenCalledWith('Growth::Acquisition::Experiment::PaidSignUpFlow', 'start', {
label: null,
property: null,
value: null,
});
});
describe.each([[true, true], [false, false]])('when isNewUser=%s', (isNewUser, visible) => { describe.each([[true, true], [false, false]])('when isNewUser=%s', (isNewUser, visible) => {
beforeEach(() => { beforeEach(() => {
store.state.isNewUser = isNewUser; store.state.isNewUser = isNewUser;
......
import { mockTracking } from 'helpers/tracking_helper';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
...@@ -587,14 +588,15 @@ describe('Subscriptions Actions', () => { ...@@ -587,14 +588,15 @@ describe('Subscriptions Actions', () => {
describe('confirmOrder', () => { describe('confirmOrder', () => {
it('calls confirmOrderSuccess with a redirect location on success', done => { it('calls confirmOrderSuccess with a redirect location on success', done => {
mock.onPost(confirmOrderPath).replyOnce(200, { location: 'x' }); const response = { location: 'x', plan_id: 'id', quantity: 1 };
mock.onPost(confirmOrderPath).replyOnce(200, response);
testAction( testAction(
actions.confirmOrder, actions.confirmOrder,
null, null,
{}, {},
[{ type: 'UPDATE_IS_CONFIRMING_ORDER', payload: true }], [{ type: 'UPDATE_IS_CONFIRMING_ORDER', payload: true }],
[{ type: 'confirmOrderSuccess', payload: 'x' }], [{ type: 'confirmOrderSuccess', payload: response }],
done, done,
); );
}); });
...@@ -627,14 +629,30 @@ describe('Subscriptions Actions', () => { ...@@ -627,14 +629,30 @@ describe('Subscriptions Actions', () => {
}); });
describe('confirmOrderSuccess', () => { describe('confirmOrderSuccess', () => {
const params = { location: 'http://example.com', plan_id: 'x', quantity: 10 };
it('changes the window location', done => { it('changes the window location', done => {
const spy = jest.spyOn(window.location, 'assign').mockImplementation(); const spy = jest.spyOn(window.location, 'assign').mockImplementation();
testAction(actions.confirmOrderSuccess, 'http://example.com', {}, [], [], () => { testAction(actions.confirmOrderSuccess, params, {}, [], [], () => {
expect(spy).toHaveBeenCalledWith('http://example.com'); expect(spy).toHaveBeenCalledWith('http://example.com');
done(); done();
}); });
}); });
it('sends tracking event', done => {
const spy = mockTracking('Growth::Acquisition::Experiment::PaidSignUpFlow', null, jest.spyOn);
jest.spyOn(window.location, 'assign').mockImplementation();
testAction(actions.confirmOrderSuccess, params, {}, [], [], () => {
expect(spy).toHaveBeenCalledWith('Growth::Acquisition::Experiment::PaidSignUpFlow', 'end', {
label: 'x',
property: null,
value: 10,
});
done();
});
});
}); });
describe('confirmOrderError', () => { describe('confirmOrderError', () => {
......
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