Source: app/util/tracking/asyncTrackingMiddleware.js

import debugLib from 'debug';
import { isAsyncAction } from '../asyncActionMiddleware';
import { dataLayerCustomEvent, dataLayerTimingEvent } from './dataLayerUtils';

const debug = debugLib('SlimmingWorld:asyncTrackingMiddleware');

/** @module */

/**
 * Time in ms until an event is considered as having timed out
 * @type {number}
 */
const ASYNC_FULFILLED_TIMEOUT = 5000;
/**
 * Minimum duration in ms of an async action before it will be tracked. Is to prevent
 * tracking promise actions that resolve synchronously.
 * @type {number}
 */
const ASYNC_THRESHOLD = 100;
/**
 * Event category for the timing events pushed to the dataLayer
 * @type {string}
 */
const TRACKING_EVENT_CATEGORY = 'asyncAction';
/**
 * Event category for the timeout events pushed to the dataLayer
 * @type {string}
 */
const TIMEOUT_EVENT_CATEGORY = 'asyncTracking';
/**
 * Event action for the timeout events pushed to the dataLayer
 * @type {string}
 */
const TIMEOUT_EVENT_ACTION = 'timeout';

/**
 * Map that will contain all currently pending actions
 * @type {Object}
 */
const pendingActions = {};

/**
 * Helper class to track timing of a single async action
 * @hidden
 */
class AsyncActionTracking {
  constructor(actionType, getState) {
    this.startTime = Date.now();
    this.actionType = actionType;
    this.getReduxState = getState;
    this.timeout = setTimeout(this.trackTimeout, ASYNC_FULFILLED_TIMEOUT);
  }

  /**
   * To be called when the action has timed out. Pushes a timeout event to the dataLayer
   */
  trackTimeout = () => {
    this.timeout = null;
    dataLayerCustomEvent(
      {
        category: TIMEOUT_EVENT_CATEGORY,
        action: TIMEOUT_EVENT_ACTION,
        label: this.actionType,
        value: ASYNC_FULFILLED_TIMEOUT,
      },
      this.getReduxState(),
    );
  };

  /**
   * To be called when a fulfilled action is dispatched. Pushes a timing event to the dataLayer
   */
  fulfill = () => {
    if (this.timeout) {
      clearTimeout(this.timeout);
      this.timeout = null;
    }
    const endTime = Date.now();
    const duration = endTime - this.startTime;
    if (duration > ASYNC_THRESHOLD) {
      dataLayerTimingEvent(
        TRACKING_EVENT_CATEGORY,
        this.actionType,
        duration,
        '',
        this.getReduxState(),
      );
    }
  };
}

/**
 * Middleware function that tracks the timing of all dispatched async actions. Should be added after
 * the asyncActionMiddleware that dispatches these actions.
 * @function asyncTrackingMiddleware
 * @category tracking
 */
export default ({ getState }) => next => action => {
  if (isAsyncAction(action) && action.type) {
    if (action.meta.isFulfilled) {
      if (pendingActions[action.meta.asyncId]) {
        pendingActions[action.meta.asyncId].fulfill();
        delete pendingActions[action.meta.asyncId];
      }
    } else {
      if (pendingActions[action.meta.asyncId]) {
        debug(`Unexpected duplicate async action id "${action.meta.asyncId}"`);
        return next(action);
      }

      pendingActions[action.meta.asyncId] = new AsyncActionTracking(action.type, getState);
    }
  }
  return next(action);
};