/* global WP_DEFINE_IS_NODE */
import debugLib from 'debug';
import clone from 'lodash/clone';
const debug = debugLib('SlimmingWorld:tracking:dataLayer');
/**
* Utilities to manage data layer tracking. You probably don't want to use these
* utilities directly. Instead, use the wrapper utilities provided in
* ./index.js that will also take care of other types
* of tracking
* @module
* @category tracking
*/
/**
* Event name for custom page view events
* @type {string}
*/
const GTM_PAGE_VIEW = 'pageView';
/**
* Event name for custom (non page-view) events
* @type {string}
*/
const GTM_CUSTOM_EVENT = 'pageEvent';
/**
* Event name for tracking timings
* @type {string}
*/
const GTM_TIMING_EVENT = 'timingEvent';
/**
* Variable name of the data layer on window
* @type {string}
*/
const DATA_LAYER_WINDOW_VAR = 'dataLayer';
/**
* Stores lastPushed pageEvent variables
* @type {object}
*/
let lastPushed = {};
/**
* Pushes the given items to the dataLayer. Verifies that the dataLayer variable
* exists on window and skips the push on the NodeJS side.
* @param {...object} items The items to push to the array. These arguments are
* the same as if you would call window.dataLayer.push( ... )
* @returns {boolean} True if the items were pushed (in the browser), false if
* it has been skipped (on the NodeJS side).
*/
export function dataLayerPush(...items) {
if (!WP_DEFINE_IS_NODE) {
if (!window[DATA_LAYER_WINDOW_VAR]) {
window[DATA_LAYER_WINDOW_VAR] = [];
}
if (items.length) {
window[DATA_LAYER_WINDOW_VAR].push(...items);
debug(`Pushed ${items.length} event${items.length === 1 ? '' : 's'} to dataLayer`);
}
return true;
}
if (items.length) {
debug(
`Skipped pushing ${items.length} event${
items.length === 1 ? '' : 's'
} to dataLayer on server side`,
);
}
return false;
}
/**
* Push a new event to the data layer. This is a lower-level helper and should not
* be used directly. To track custom events, use the dataLayerCustomEvent() util.
* @param {string} event The name of the event to push
* @param {object} [attributes={}] Attributes to add to the event
* @param {object} [state] The current redux state, used to get tracking meta properties
*/
const dataLayerEvent = (event, attributes = {}, state) => {
if (!state) {
debug('Warning: redux state not passed to dataLayerEvent. Tracking meta will not be included.');
}
const keys = Object.keys(lastPushed);
// set previous values to undefined (force-resets GTM stored variables)
keys.forEach(key => {
if (attributes[key]) {
delete lastPushed[key];
} else {
lastPushed[key] = undefined;
}
});
const trackingMeta = (state && state.tracking && state.tracking.persistentData) || {};
const finalAttributes = { ...trackingMeta, ...attributes };
debug('Pushing %s to dataLayer: %o', event, finalAttributes);
const pushEvent = dataLayerPush({
...lastPushed,
event,
// legacy, wrapped in an attributes key, kept for backwards compatibility
attributes: finalAttributes,
...finalAttributes, // now just on main level
});
// Reset lastPushed object
lastPushed = clone(attributes);
return pushEvent;
};
/**
* Tracks a custom event. All custom events have been given the same name (see GTM_CUSTOM_EVENT
* const above) so we can define a trigger in the GTM admin console to respond to these
* events. For more information about the parameters passed to this util, see
* {@link https://support.google.com/analytics/answer/1033068?hl=en#Anatomy|The Anatomy of Events}
*
* @param {object} eventAttributes
* @param {object} [state] The current redux state, used to get tracking meta properties
*/
export const dataLayerCustomEvent = (eventAttributes, state) => {
const { data } = state?.tracking.pageData.find(
page => page.pathname === state.routeHistory[0].pathname,
) || { data: {} };
return dataLayerEvent(
GTM_CUSTOM_EVENT,
{
...data,
...eventAttributes,
},
state,
);
};
/**
* Tracks a timing event in GTM. This is a custom event trigger, but it has been
* structured to match a timing event in Google Analytics so they can easily be
* mapped to GA in the GTM admin console. See
* {@link https://developers.google.com/analytics/devguides/collection/analyticsjs/user-timings}
* for more info.
* @param {string} timingCategory The category for this event
* @param {string} timingVar The name of the variable to time
* @param {number} timingValue The number of milliseconds tracked
* @param {string} [timingLabel] An optional label to visualize the timings
* @param {object} [state] The current redux state, used to get tracking meta properties
*/
export const dataLayerTimingEvent = (
timingCategory,
timingVar,
timingValue,
timingLabel = '',
state,
) => {
if (typeof timingValue !== 'number' || timingValue % 1 !== 0) {
debug(
`WARNING: invalid timingValue passed to dataLayerTimingEvent. Expected an integer, got ${timingValue}. Ignoring event.`,
);
return false;
}
const eventObject = { timingCategory, timingVar, timingValue, timingLabel };
return dataLayerEvent(GTM_TIMING_EVENT, eventObject, state);
};
/**
* Tracks a custom page view event to the data layer. The attributes of the event
* can be mapped in the GTM admin console. Currently, not all of them are being
* used.
* @param data Data object for tracking
* @param {object} [state] The current redux state, used to get tracking meta properties
*/
export const dataLayerPageView = (data, state) => dataLayerEvent(GTM_PAGE_VIEW, data, state);