Skip to content

Commit

Permalink
feat: add support to google consent mode for v1
Browse files Browse the repository at this point in the history
  • Loading branch information
danielbento92 committed Dec 29, 2023
1 parent 7a59906 commit c4e226b
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 4 deletions.
32 changes: 30 additions & 2 deletions packages/react/src/analytics/integrations/GA4/GA4.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
integrations,
utils,
} from '@farfetch/blackout-core/analytics';
import { consentManagementProtocol } from '..';
import {
DATA_TEST_SELECTOR,
DEFAULT_DATA_LAYER_NAME,
Expand Down Expand Up @@ -76,7 +77,7 @@ class GA4 extends integrations.Integration {
*/
constructor(options, loadData, analytics) {
super(options, loadData, analytics);
this.initialize(options);
this.initialize(options, loadData);
this.onSetUser(loadData);
}

Expand All @@ -87,8 +88,9 @@ class GA4 extends integrations.Integration {
* Initializes member variables from options and tries to initialize Google Analytics 4.
*
* @param {object} options - Options passed for the GA4 integration.
* @param {object} loadData - Analytics's load event data.
*/
initialize(options) {
initialize(options, loadData) {
this.optionsValidationResultsMap = validateFields(options);

this.measurementId = options[OPTION_MEASUREMENT_ID];
Expand Down Expand Up @@ -123,9 +125,35 @@ class GA4 extends integrations.Integration {
true,
);

const dataLayer = get(
this.options,
OPTION_DATA_LAYER_NAME,
DEFAULT_DATA_LAYER_NAME,
);
consentManagementProtocol.addIntegration(loadData?.consent, dataLayer);
this.loadGtagScript(options);
}

/**
* Sets the consent object.
* This method is called by analytics whenever the consent changes, so there's no need to validate if it has changed or not.
*
* @param {object} consent - Object to be written on the dataLayer.
*
* @returns {GA4} This allows chaining of class methods.
*/
setConsent(consent) {
const dataLayer = get(
this.options,
OPTION_DATA_LAYER_NAME,
DEFAULT_DATA_LAYER_NAME,
);

consentManagementProtocol.setConsents(consent, dataLayer);

return this;
}

/**
* Send page events to GA4.
*
Expand Down
5 changes: 5 additions & 0 deletions packages/react/src/analytics/integrations/GTM/GTM.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
DATA_LAYER_CONSENT_EVENT,
DATA_LAYER_CONTEXT_EVENT,
DATA_LAYER_SET_USER_EVENT,
GTM_DATA_LAYER,
GTM_LABEL_PREFIX,
GTM_TYPE_ERROR_PREFIX,
INVALID_FUNCTION_ERROR_SUFFIX,
Expand All @@ -32,6 +33,7 @@ import {
SET_USER_KEY,
SET_USER_TYPE,
} from './constants';
import { consentManagementProtocol } from '..';
import { utils as coreUtils } from '@farfetch/blackout-core/analytics';
import { getContextParameters, getUserParameters } from './utils';
import { Integration } from '@farfetch/blackout-core/analytics/integrations';
Expand Down Expand Up @@ -107,6 +109,8 @@ class GTM extends Integration {
* @param {object} loadData - Analytics's load event data.
*/
initialize(options, loadData) {
consentManagementProtocol.addIntegration(loadData?.consent, GTM_DATA_LAYER);

this.runGTMScript(options);
this.setConsent(loadData.consent);
this.setContext(loadData.context);
Expand Down Expand Up @@ -150,6 +154,7 @@ class GTM extends Integration {
* @returns {GTM} This allows chaining of class methods.
*/
setConsent(consent) {
consentManagementProtocol.setConsents(consent, GTM_DATA_LAYER);
this.write({
consent,
event: this.consentKey,
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/analytics/integrations/GTM/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ export const INVALID_FUNCTION_ERROR_SUFFIX =
export const CONSENT_TYPE = 'consent';
export const CONTEXT_TYPE = 'context';
export const SET_USER_TYPE = 'user';

export const GTM_DATA_LAYER = 'dataLayer';
4 changes: 3 additions & 1 deletion packages/react/src/analytics/integrations/GTM/gtmTag.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable */

import { GTM_DATA_LAYER } from './constants';

/**
* Script from google tag manager.
*
Expand All @@ -18,4 +20,4 @@ export default containerId =>
j.async = true;
j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl;
f.parentNode.insertBefore(j, f);
})(window, document, 'script', 'dataLayer', containerId);
})(window, document, 'script', GTM_DATA_LAYER, containerId);
7 changes: 6 additions & 1 deletion packages/react/src/analytics/integrations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { integrations } from '@farfetch/blackout-core/analytics';
const { Integration } = integrations;

export { Integration };
export { AnalyticsConstants } from './shared/constants';

export {
AnalyticsConstants,
ConsentManagementProtocol,
consentManagementProtocol,
} from './shared';

export { default as AnalyticsApi } from './AnalyticsApi';
export { default as AnalyticsService } from './AnalyticsService';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* CMP - Consent Management Protocol */

export class ConsentManagementProtocol {
constructor() {
this.dataLayers = [];
this.enabled = true;
this.mappingConsentCategories = this.defaultMappings();
}

addIntegration(consent, dataLayer) {
if (this.enabled) {
this.dataLayers.push(dataLayer);
this.gtag(dataLayer, consent);
}
}

disable() {
this.enabled = false;
}

defaultMappings() {
return {
ad_storage: [],
ad_user_data: [],
ad_personalization: [],
analytics_storage: [],
};
}

setCustomCustomCategoriesMapping(customMapping) {
this.mappingConsentCategories = customMapping;
}

getConsentValues(consents) {
// Dealing with null or undefined consent values
const consentList = consents || {};

return Object.keys(this.mappingConsentCategories).reduce(
(result, consentKey) => ({
...result,
[consentKey]: this.mappingConsentCategories[consentKey]?.every(
consent => consentList[consent],
)
? 'granted'
: 'deny',
}),
{},
);
}

setConsents(consents, dataLayer) {
if (this.enabled) {
const dataLayers = dataLayer ? [dataLayer] : this.dataLayers;
dataLayers.forEach(layer => this.gtag(layer, consents, 'update'));
}
}

gtag(dataLayer, consent, command = 'default') {
if (this.enabled && typeof window !== 'undefined') {
// @ts-ignore
window[dataLayer] = window[dataLayer] || [];

/**
*
*/
function internalGtag() {
// @ts-ignore
// eslint-disable-next-line
window[dataLayer].push(arguments);
}

// @ts-ignore
internalGtag('consent', command, this.getConsentValues(consent));
}
}
}

const instance = new ConsentManagementProtocol();

export default instance;
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { consentManagementProtocol } from '../index.js';

describe('ConsentManagementProtocol', () => {
let instance;
let spyGtag;
const mockConsent = {};

beforeEach(() => {
instance = consentManagementProtocol;
spyGtag = jest.spyOn(instance, 'gtag');
spyGtag.mockClear();
window['dataLayer'] = [];
});

it('should add integrations and initialize data layers', () => {
instance.addIntegration(mockConsent, 'dataLayer');
instance.addIntegration(mockConsent, 'ga4DataLayer');

expect(instance['dataLayers']).toEqual(['dataLayer', 'ga4DataLayer']);

expect(window['dataLayer']).toMatchSnapshot();
});

it('should set custom consent categories mapping and initialize a data layer; only 1 consent should be granted', () => {
const customMappings = {
ad_storage: ['consent1'],
ad_user_data: ['consent1', 'consent2'],
ad_personalization: [],
analytics_storage: ['consent3'],
};

instance.setCustomCustomCategoriesMapping(customMappings);

instance.addIntegration(mockConsent, 'dataLayer');

expect(instance['mappingConsentCategories']).toEqual(customMappings);

expect(window['dataLayer']).toMatchSnapshot();
});

it('should correctly process consent values', () => {
instance.setCustomCustomCategoriesMapping({
ad_storage: ['consent1'],
ad_user_data: ['consent2'],
ad_personalization: [],
analytics_storage: ['consent3'],
});

const mockConsentWithValues = {
consent1: true,
consent2: true,
consent3: false,
};

const consentValues = instance['getConsentValues'](mockConsentWithValues);

expect(consentValues).toEqual({
ad_storage: 'granted',
ad_user_data: 'granted',
ad_personalization: 'granted',
analytics_storage: 'deny',
});
});

it('should set consents for specific or all data layers', () => {
const spyGtag = jest.spyOn(instance, 'gtag');

instance['dataLayers'] = ['dataLayer1', 'dataLayer2'];

instance.setConsents(mockConsent, 'dataLayer');
expect(spyGtag).toHaveBeenCalledWith('dataLayer', mockConsent, 'update');

expect(spyGtag).not.toHaveBeenCalledWith(
'dataLayer1',
mockConsent,
'update',
);

instance.setConsents(mockConsent);

expect(spyGtag).toHaveBeenCalledWith('dataLayer1', mockConsent, 'update');
});

it('should not be fill the datalayer when consent mode is disabled', () => {
instance['dataLayers'] = ['dataLayer1', 'dataLayer2'];
instance.setConsents(mockConsent, 'dataLayer');
expect(spyGtag).toHaveBeenCalled();

spyGtag.mockClear();
instance.disable();

instance.setConsents(mockConsent, 'dataLayer');
expect(spyGtag).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ConsentManagementProtocol should add integrations and initialize data layers 1`] = `
Array [
Arguments [
"consent",
"default",
Object {
"ad_personalization": "granted",
"ad_storage": "granted",
"ad_user_data": "granted",
"analytics_storage": "granted",
},
],
]
`;

exports[`ConsentManagementProtocol should set custom consent categories mapping and initialize a data layer; only 1 consent should be granted 1`] = `
Array [
Arguments [
"consent",
"default",
Object {
"ad_personalization": "granted",
"ad_storage": "deny",
"ad_user_data": "deny",
"analytics_storage": "deny",
},
],
]
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
ConsentManagementProtocol,
default as consentManagementProtocol,
} from './ConsentManagementProtocol';
6 changes: 6 additions & 0 deletions packages/react/src/analytics/integrations/shared/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export {
ConsentManagementProtocol,
consentManagementProtocol,
} from './ConsentManagementProtocol';

export { AnalyticsConstants } from './constants';

0 comments on commit c4e226b

Please sign in to comment.