Commit c788eef9 authored by Tyler Williams's avatar Tyler Williams Committed by Enrique Alcántara

Add GTM enhanced ecommerce transaction event to checkout

Related to:
https://gitlab.com/gitlab-com/marketing/digital-experience/buyer-experience/-/issues/271

Add the transaction enhanced ecommerce event to new subscription page,
in the confirmOrder action with successful response.

trackTransaction will only fire when the ops feature flag for
gitlab_gtm_datalayer is enabled, and GTM is running on the page.

This commit also adds/adjusts Jest specs for the GTM tracking functions.
parent d967565a
import { v4 as uuidv4 } from 'uuid';
import { logError } from '~/lib/logger';
const SKU_PREMIUM = '2c92a00d76f0d5060176f2fb0a5029ff';
......@@ -19,6 +20,24 @@ const PRODUCT_INFO = {
},
};
const generateProductInfo = (sku, quantity) => {
const product = PRODUCT_INFO[sku];
if (!product) {
logError('Unexpected product sku provided to generateProductInfo');
return {};
}
const productInfo = {
...product,
brand: 'GitLab',
category: 'DevOps',
quantity,
};
return productInfo;
};
const isSupported = () => Boolean(window.dataLayer) && gon.features?.gitlabGtmDatalayer;
const pushEvent = (event, args = {}) => {
......@@ -162,25 +181,17 @@ export const trackCheckout = (selectedPlan, quantity) => {
return;
}
const product = PRODUCT_INFO[selectedPlan];
const product = generateProductInfo(selectedPlan, quantity);
if (!product) {
logError('Unexpected product sku provided to trackCheckout');
if (Object.keys(product).length === 0) {
return;
}
const selectedProductData = {
...product,
brand: 'GitLab',
category: 'DevOps',
quantity,
};
const eventData = {
ecommerce: {
checkout: {
actionField: { step: 1 },
products: [selectedProductData],
products: [product],
},
},
};
......@@ -188,3 +199,34 @@ export const trackCheckout = (selectedPlan, quantity) => {
// eslint-disable-next-line @gitlab/require-i18n-strings
pushEnhancedEcommerceEvent('EECCheckout', 'USD', eventData);
};
export const trackTransaction = (transactionDetails) => {
if (!isSupported()) {
return;
}
const transactionId = uuidv4();
const { paymentOption, revenue, tax, selectedPlan, quantity } = transactionDetails;
const product = generateProductInfo(selectedPlan, quantity);
if (Object.keys(product).length === 0) {
return;
}
const eventData = {
ecommerce: {
purchase: {
actionField: {
id: transactionId,
affiliation: 'GitLab',
option: paymentOption,
revenue,
tax,
},
products: [product],
},
},
};
pushEnhancedEcommerceEvent('EECtransactionSuccess', 'USD', eventData);
};
......@@ -5,7 +5,7 @@ import activateNextStepMutation from 'ee/vue_shared/purchase_flow/graphql/mutati
import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { sprintf, s__ } from '~/locale';
import { trackCheckout } from '~/google_tag_manager';
import { trackCheckout, trackTransaction } from '~/google_tag_manager';
import defaultClient from '../graphql';
import * as types from './mutation_types';
......@@ -195,6 +195,16 @@ export const confirmOrder = ({ getters, dispatch, commit }) => {
Api.confirmOrder(getters.confirmOrderParams)
.then(({ data }) => {
if (data.location) {
const transactionDetails = {
paymentOption: getters.confirmOrderParams?.subscription?.payment_method_id,
revenue: getters.totalExVat,
tax: getters.vat,
selectedPlan: getters.selectedPlanDetails?.value,
quantity: getters.selectedGroupUsers,
};
trackTransaction(transactionDetails);
dispatch('confirmOrderSuccess', {
location: data.location,
});
......
......@@ -9,6 +9,7 @@ import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as googleTagManager from '~/google_tag_manager';
const {
countriesPath,
......@@ -553,6 +554,22 @@ describe('Subscriptions Actions', () => {
);
});
it('calls trackTransaction on success', async () => {
const spy = jest.spyOn(googleTagManager, 'trackTransaction');
const response = { location: 'x' };
mock.onPost(confirmOrderPath).replyOnce(200, response);
await testAction(
actions.confirmOrder,
null,
{},
[{ type: 'UPDATE_IS_CONFIRMING_ORDER', payload: true }],
[{ type: 'confirmOrderSuccess', payload: response }],
);
expect(spy).toHaveBeenCalled();
});
it('calls confirmOrderError with the errors on error', async () => {
mock.onPost(confirmOrderPath).replyOnce(200, { errors: 'errors' });
......
import { merge } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import {
trackFreeTrialAccountSubmissions,
trackNewRegistrations,
......@@ -9,11 +10,13 @@ import {
trackSaasTrialProjectImport,
trackSaasTrialGetStarted,
trackCheckout,
trackTransaction,
} from '~/google_tag_manager';
import { setHTMLFixture } from 'helpers/fixtures';
import { logError } from '~/lib/logger';
jest.mock('~/lib/logger');
jest.mock('uuid');
describe('~/google_tag_manager/index', () => {
let spy;
......@@ -217,28 +220,29 @@ describe('~/google_tag_manager/index', () => {
trackCheckout('2c92a00d76f0d5060176f2fb0a5029ff', 1);
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenCalledWith({ ecommerce: null });
expect(spy).toHaveBeenCalledWith({
event: 'EECCheckout',
currencyCode: 'USD',
ecommerce: {
checkout: {
actionField: { step: 1 },
products: [
{
brand: 'GitLab',
category: 'DevOps',
id: '0002',
name: 'Premium',
price: 228,
quantity: 1,
variant: 'SaaS',
},
],
expect(spy.mock.calls.flatMap((x) => x)).toEqual([
{ ecommerce: null },
{
event: 'EECCheckout',
currencyCode: 'USD',
ecommerce: {
checkout: {
actionField: { step: 1 },
products: [
{
brand: 'GitLab',
category: 'DevOps',
id: '0002',
name: 'Premium',
price: 228,
quantity: 1,
variant: 'SaaS',
},
],
},
},
},
});
]);
});
it('with selectedPlan: 2c92a0ff76f0d5250176f2f8c86f305a', () => {
......@@ -307,6 +311,82 @@ describe('~/google_tag_manager/index', () => {
});
});
});
describe('when trackTransactions is invoked', () => {
describe.each([
{
selectedPlan: '2c92a00d76f0d5060176f2fb0a5029ff',
revenue: 228,
name: 'Premium',
id: '0002',
},
{
selectedPlan: '2c92a0ff76f0d5250176f2f8c86f305a',
revenue: 1188,
name: 'Ultimate',
id: '0001',
},
])('with %o', (planObject) => {
it('invokes pushes a new event that references the selected plan', () => {
const { selectedPlan, revenue, name, id } = planObject;
expect(spy).not.toHaveBeenCalled();
uuidv4.mockImplementationOnce(() => '123');
const transactionDetails = {
paymentOption: 'visa',
revenue,
tax: 10,
selectedPlan,
quantity: 1,
};
trackTransaction(transactionDetails);
expect(spy.mock.calls.flatMap((x) => x)).toEqual([
{ ecommerce: null },
{
event: 'EECtransactionSuccess',
currencyCode: 'USD',
ecommerce: {
purchase: {
actionField: {
id: '123',
affiliation: 'GitLab',
option: 'visa',
revenue,
tax: 10,
},
products: [
{
brand: 'GitLab',
category: 'DevOps',
id,
name,
price: revenue,
quantity: 1,
variant: 'SaaS',
},
],
},
},
},
]);
});
});
});
describe('when trackTransaction is invoked', () => {
describe('with an invalid plan object', () => {
it('does not get called', () => {
expect(spy).not.toHaveBeenCalled();
trackTransaction({ selectedPlan: 'notAplan' });
expect(spy).not.toHaveBeenCalled();
});
});
});
});
describe.each([
......
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