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 Jan 10, 2024
1 parent 0bc8aa2 commit 799f59b
Show file tree
Hide file tree
Showing 11 changed files with 741 additions and 7 deletions.
40 changes: 35 additions & 5 deletions packages/react/src/analytics/integrations/GA4/GA4.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
OPTION_DATA_LAYER_NAME,
OPTION_DEBUG_MODE,
OPTION_ENABLE_AUTOMATIC_PAGE_VIEWS,
OPTION_GOOGLE_CONSENT_CONFIG,
OPTION_LOAD_SCRIPT_FUNCTION,
OPTION_MEASUREMENT_ID,
OPTION_NON_INTERACTION_EVENTS,
Expand All @@ -39,6 +40,7 @@ import {
OPTION_SCOPE_COMMANDS,
OPTION_SET_CUSTOM_USER_ID_PROPERTY,
} from './constants';
import { GoogleConsentMode } from '../shared';
import { validateFields } from './validation/optionsValidator';
import defaultSchemaEventsMap from '../shared/validation/eventSchemas';
import each from 'lodash/each';
Expand Down Expand Up @@ -76,7 +78,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 +89,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 +126,38 @@ class GA4 extends integrations.Integration {
true,
);

this.googleConsentMode = new GoogleConsentMode(
this.getDataLayerName(),
loadData?.consent,
get(options, OPTION_GOOGLE_CONSENT_CONFIG),
);

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) {
this.googleConsentMode.updateConsent(consent);

return this;
}

/**
* Returns the data layer name.
*
* @returns {string} - Data Layer Name.
*/
getDataLayerName() {
return get(this.options, OPTION_DATA_LAYER_NAME, DEFAULT_DATA_LAYER_NAME);
}

/**
* Send page events to GA4.
*
Expand Down Expand Up @@ -654,9 +686,7 @@ class GA4 extends integrations.Integration {
*
*/
internalLoadScript(options) {
const customDataLayerAttr = options[OPTION_DATA_LAYER_NAME]
? options[OPTION_DATA_LAYER_NAME]
: DEFAULT_DATA_LAYER_NAME;
const customDataLayerAttr = this.getDataLayerName();
const debugMode = options[OPTION_DEBUG_MODE] || false;
const script = document.createElement('script');
script.setAttribute(
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/analytics/integrations/GA4/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ export const OPTION_EXCLUDE_ARRAY_PARAMETERS_EVENTS =
'excludeArrayParametersEvents';
export const OPTION_SET_CUSTOM_USER_ID_PROPERTY = 'setCustomUserIdProperty';

export const OPTION_GOOGLE_CONSENT_CONFIG = 'googleConsentConfig';

export const GA4_UNIQUE_EVENT_ID = 'blackoutAnalyticsEventId';
10 changes: 10 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,8 @@ import {
DATA_LAYER_CONSENT_EVENT,
DATA_LAYER_CONTEXT_EVENT,
DATA_LAYER_SET_USER_EVENT,
GOOGLE_CONSENT_CONFIG_KEY,
GTM_DATA_LAYER,
GTM_LABEL_PREFIX,
GTM_TYPE_ERROR_PREFIX,
INVALID_FUNCTION_ERROR_SUFFIX,
Expand All @@ -34,6 +36,7 @@ import {
} from './constants';
import { utils as coreUtils } from '@farfetch/blackout-core/analytics';
import { getContextParameters, getUserParameters } from './utils';
import { GoogleConsentMode } from '../shared';
import { Integration } from '@farfetch/blackout-core/analytics/integrations';
import eventSchemas from '../shared/validation/eventSchemas';
import eventsMapper from './eventsMapper';
Expand Down Expand Up @@ -107,6 +110,12 @@ class GTM extends Integration {
* @param {object} loadData - Analytics's load event data.
*/
initialize(options, loadData) {
this.googleConsentMode = new GoogleConsentMode(
GTM_DATA_LAYER,
loadData?.consent,
get(options, GOOGLE_CONSENT_CONFIG_KEY),
);

this.runGTMScript(options);
this.setConsent(loadData.consent);
this.setContext(loadData.context);
Expand Down Expand Up @@ -150,6 +159,7 @@ class GTM extends Integration {
* @returns {GTM} This allows chaining of class methods.
*/
setConsent(consent) {
this.googleConsentMode.updateConsent(consent);
this.write({
consent,
event: this.consentKey,
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/analytics/integrations/GTM/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const SET_CONTEXT_FN_KEY = 'setContext';
export const SET_CONSENT_KEY = 'consentKey';
export const SET_USER_KEY = 'userKey';
export const SET_USER_FN_KEY = 'onSetUser';
export const GOOGLE_CONSENT_CONFIG_KEY = 'googleConsentConfig';

export const GTM_LABEL_PREFIX = 'Google Tag Manager -';
export const GTM_TYPE_ERROR_PREFIX = `${GTM_LABEL_PREFIX} TypeError:`;
Expand All @@ -18,3 +19,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);
3 changes: 2 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,8 @@ import { integrations } from '@farfetch/blackout-core/analytics';
const { Integration } = integrations;

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

export { AnalyticsConstants, googleConsentTypes } 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,144 @@
/**
* GoogleConsentMode handles with Google Consent Mode v2.
*/
import isEqual from 'lodash/isEqual';
import omit from 'lodash/omit';

export const googleConsentTypes = {
GRANTED: 'granted',
DENIED: 'denied',
};
export class GoogleConsentMode {
/**
* Creates a new GoogleConsentMode instance.
*
* @param {string} dataLayer - DataLayer name.
* @param {string} initConsent - The init consent data.
* @param {object} config - The configuration properties of Google Consent Mode.
*/
constructor(dataLayer, initConsent, config) {
this.dataLayer = dataLayer;
this.config = config;

// select only the Google Consent Elements
this.configExcludingRegionsAndWaitForUpdate = omit(this.config || {}, [
'waitForUpdate',
'regions',
]);

this.loadDefaults(initConsent);
}

/**
* Initialize Google Consent Mode instance.
*
* @param {string} initConsent - The init consent data.
*/
loadDefaults(initConsent) {
if (this.config) {
const initialValue = {};

if (this.config.waitForUpdate) {
initialValue['wait_for_update'] = this.config.waitForUpdate;
}

// Obtain default google consent registry
const consentRegistry = Object.keys(
this.configExcludingRegionsAndWaitForUpdate,
).reduce((result, consentKey) => {
return {
...result,
[consentKey]:
this.configExcludingRegionsAndWaitForUpdate[consentKey]?.default ||
googleConsentTypes.DENIED,
};
}, initialValue);

// Write default consent to data layer
this.write('consent', 'default', consentRegistry);

// write regions to data layer if they exists
const regions = this.config.regions;
if (regions) {
regions.forEach(region => {
this.write('consent', 'default', region);
});
}

// after write default consents, then write first update with initial consent data
this.updateConsent(initConsent);
}
}

/**
* Update consent.
*
* @param {object} consentData - The consent data to be set.
*/
updateConsent(consentData) {
if (this.config) {
// Dealing with null or undefined consent values
const safeConsent = consentData || {};

// Fill consent value into consent element, using analytics consent categories
const consentRegistry = Object.keys(
this.configExcludingRegionsAndWaitForUpdate,
).reduce((result, consentKey) => {
let consentValue = googleConsentTypes.DENIED;
const consent = this.configExcludingRegionsAndWaitForUpdate[consentKey];

if (consent) {
// has consent config key

if (consent.getConsentValue) {
// give priority to custom function
consentValue = consent.getConsentValue(safeConsent);
} else if (
consent?.categories !== undefined &&
consent.categories.every(consent => safeConsent[consent])
) {
// The second option to assign value is by categories list
consentValue = googleConsentTypes.GRANTED;
}
}

return {
...result,
[consentKey]: consentValue,
};
}, {});

// Write consent to data layer
this.write('consent', 'update', consentRegistry);
}
}

/**
* Write consent on data layer.
*
* @param {string} consentCommand - The consent command "consent".
* @param {string} command - The command "default" or "update".
* @param {object} consentParams - The consent arguments.
*/
// eslint-disable-next-line no-unused-vars
write(consentCommand, command, consentParams) {
// Without using the arguments reference, google debug mode would not seem to register the consent
// that was written to the datalayer, so the parameters added to the function signature are only to
// avoid mistakes when calling the function.

if (
this.config &&
typeof window !== 'undefined' &&
consentParams &&
!isEqual(this.lastConsent, consentParams)
) {
// @ts-ignore
window[this.dataLayer] = window[this.dataLayer] || [];

window[this.dataLayer].push(arguments);
this.lastConsent = consentParams;
}
}
}

export default GoogleConsentMode;
Loading

0 comments on commit 799f59b

Please sign in to comment.