Source: app/util/tracking/dataLayerUtils.js

/* 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);