Commit a22a84f6 authored by Enrique Alcántara's avatar Enrique Alcántara

Merge branch 'feature/add-transaction-event-for-enhanced-ecommerce' into 'master'

Add GTM enhanced ecommerce transaction event to checkout

See merge request gitlab-org/gitlab!79355
parents b5380180 c788eef9
import { v4 as uuidv4 } from 'uuid';
import { logError } from '~/lib/logger'; import { logError } from '~/lib/logger';
const SKU_PREMIUM = '2c92a00d76f0d5060176f2fb0a5029ff'; const SKU_PREMIUM = '2c92a00d76f0d5060176f2fb0a5029ff';
...@@ -19,6 +20,24 @@ const PRODUCT_INFO = { ...@@ -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 isSupported = () => Boolean(window.dataLayer) && gon.features?.gitlabGtmDatalayer;
const pushEvent = (event, args = {}) => { const pushEvent = (event, args = {}) => {
...@@ -162,25 +181,17 @@ export const trackCheckout = (selectedPlan, quantity) => { ...@@ -162,25 +181,17 @@ export const trackCheckout = (selectedPlan, quantity) => {
return; return;
} }
const product = PRODUCT_INFO[selectedPlan]; const product = generateProductInfo(selectedPlan, quantity);
if (!product) { if (Object.keys(product).length === 0) {
logError('Unexpected product sku provided to trackCheckout');
return; return;
} }
const selectedProductData = {
...product,
brand: 'GitLab',
category: 'DevOps',
quantity,
};
const eventData = { const eventData = {
ecommerce: { ecommerce: {
checkout: { checkout: {
actionField: { step: 1 }, actionField: { step: 1 },
products: [selectedProductData], products: [product],
}, },
}, },
}; };
...@@ -188,3 +199,34 @@ export const trackCheckout = (selectedPlan, quantity) => { ...@@ -188,3 +199,34 @@ export const trackCheckout = (selectedPlan, quantity) => {
// eslint-disable-next-line @gitlab/require-i18n-strings // eslint-disable-next-line @gitlab/require-i18n-strings
pushEnhancedEcommerceEvent('EECCheckout', 'USD', eventData); 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 ...@@ -5,7 +5,7 @@ import activateNextStepMutation from 'ee/vue_shared/purchase_flow/graphql/mutati
import createFlash from '~/flash'; import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import { sprintf, s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
import { trackCheckout } from '~/google_tag_manager'; import { trackCheckout, trackTransaction } from '~/google_tag_manager';
import defaultClient from '../graphql'; import defaultClient from '../graphql';
import * as types from './mutation_types'; import * as types from './mutation_types';
...@@ -195,6 +195,16 @@ export const confirmOrder = ({ getters, dispatch, commit }) => { ...@@ -195,6 +195,16 @@ export const confirmOrder = ({ getters, dispatch, commit }) => {
Api.confirmOrder(getters.confirmOrderParams) Api.confirmOrder(getters.confirmOrderParams)
.then(({ data }) => { .then(({ data }) => {
if (data.location) { 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', { dispatch('confirmOrderSuccess', {
location: data.location, location: data.location,
}); });
......
...@@ -9,6 +9,7 @@ import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; ...@@ -9,6 +9,7 @@ import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash'; import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import * as googleTagManager from '~/google_tag_manager';
const { const {
countriesPath, countriesPath,
...@@ -553,6 +554,22 @@ describe('Subscriptions Actions', () => { ...@@ -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 () => { it('calls confirmOrderError with the errors on error', async () => {
mock.onPost(confirmOrderPath).replyOnce(200, { errors: 'errors' }); mock.onPost(confirmOrderPath).replyOnce(200, { errors: 'errors' });
......
import { merge } from 'lodash'; import { merge } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { import {
trackFreeTrialAccountSubmissions, trackFreeTrialAccountSubmissions,
trackNewRegistrations, trackNewRegistrations,
...@@ -9,11 +10,13 @@ import { ...@@ -9,11 +10,13 @@ import {
trackSaasTrialProjectImport, trackSaasTrialProjectImport,
trackSaasTrialGetStarted, trackSaasTrialGetStarted,
trackCheckout, trackCheckout,
trackTransaction,
} from '~/google_tag_manager'; } from '~/google_tag_manager';
import { setHTMLFixture } from 'helpers/fixtures'; import { setHTMLFixture } from 'helpers/fixtures';
import { logError } from '~/lib/logger'; import { logError } from '~/lib/logger';
jest.mock('~/lib/logger'); jest.mock('~/lib/logger');
jest.mock('uuid');
describe('~/google_tag_manager/index', () => { describe('~/google_tag_manager/index', () => {
let spy; let spy;
...@@ -217,28 +220,29 @@ describe('~/google_tag_manager/index', () => { ...@@ -217,28 +220,29 @@ describe('~/google_tag_manager/index', () => {
trackCheckout('2c92a00d76f0d5060176f2fb0a5029ff', 1); trackCheckout('2c92a00d76f0d5060176f2fb0a5029ff', 1);
expect(spy).toHaveBeenCalledTimes(2); expect(spy.mock.calls.flatMap((x) => x)).toEqual([
expect(spy).toHaveBeenCalledWith({ ecommerce: null }); { ecommerce: null },
expect(spy).toHaveBeenCalledWith({ {
event: 'EECCheckout', event: 'EECCheckout',
currencyCode: 'USD', currencyCode: 'USD',
ecommerce: { ecommerce: {
checkout: { checkout: {
actionField: { step: 1 }, actionField: { step: 1 },
products: [ products: [
{ {
brand: 'GitLab', brand: 'GitLab',
category: 'DevOps', category: 'DevOps',
id: '0002', id: '0002',
name: 'Premium', name: 'Premium',
price: 228, price: 228,
quantity: 1, quantity: 1,
variant: 'SaaS', variant: 'SaaS',
}, },
], ],
},
}, },
}, },
}); ]);
}); });
it('with selectedPlan: 2c92a0ff76f0d5250176f2f8c86f305a', () => { it('with selectedPlan: 2c92a0ff76f0d5250176f2f8c86f305a', () => {
...@@ -307,6 +311,82 @@ describe('~/google_tag_manager/index', () => { ...@@ -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([ 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