Google Tag Manager Integration

1. Product Click Events.

📘

Components:

In order to send the Product Click events to GA, you would need to override the default Findify onClick function in components/Cards/Product/index.tsx component.
In order to get the different types of the widgets (isCollection, isAutocomplete, isRecommendation etc.), please utilize the commonly used HOC's.

/**
 * @module components/Cards/Product
 */

import React from 'react';
import {
  compose,
  defaultProps,
  mapProps,
  setDisplayName,
  withHandlers,
  withProps,
  withPropsOnChange,
  withHandlers,
} from 'recompose';
import pure from 'helpers/pure';

import styles from 'components/Cards/Product/styles.css';
import view from 'components/Cards/Product/view';
import withTheme from 'helpers/withTheme';

//actual GA event
const sendFindifyClickEvent = (list, item, config, index) => new Promise(resolve =>
  window.dataLayer.push({
    eventCallback: resolve,
    event: "gaEvent",
    eventCategory: "Ecommerce",
    eventAction: "Product Click",
    ecommerce: {
      click: {
        actionField: { list },
          products: [{
            currencyCode: config.getIn(['currency', 'code']),
            name: item.get('title'), // Name or ID is required.
            id: item.getIn(['sku', 0]),
            price: item.getIn(['price', 0]),
            brand: item.get('brand'),
            category: item.getIn(['category', 0, 'category1']),
            list: list,
            position: index + 1
         }]
       }
     }
  })
);

//nagigate helper for custom onCLick event
const navigate = (openInNewWindow, url) => {
  if (!window) return;
  if (openInNewWindow) return window.open(url, '_blank');
  return window.location.href = url;
}

const ProductCard: any = compose(
  pure,
  setDisplayName('ProductCard'),
  withTheme(styles),
  withHandlers({
    onClick: ({ item, isCollection, isAutocomplete, isRecommendation, config, index }) => (event) => {
      event.preventDefault();
      const openInNewWindow = event && (event.ctrlKey || event.metaKey);
      //select the action type
      let list = '';
      switch(true){
        case isAutocomplete:
          list = 'Autocomplete Results'
          break;
        case isCollection:
          list = 'Collection Results'
          break;
        case isRecommendation:
          list = 'Recommendation'
          break;
        default: list = 'Search Results'; 
      }
      //send the GA event
      sendFindifyClickEvent(list, item, config, index);
      //send Findify analytics
      item.analytics.sendEvent(
        'click-item',
        { rid: item.meta.get('rid'), item_id: item.get('id') },
        !openInNewWindow
      );
      navigate(openInNewWindow, item.get('product_url'));
    }
  })
)(view);

export default ProductCard;

Then in the View component, you can grab a new custom onClick function and put use it for the ProductCard component:

// ...
const ProductCardView: React.SFC<IProductCardProps> = ({
  item,
  config,
  theme,
  onClick // <-- Get the handler from props
}: any) => (
  <a
    onClick={onClick} // <-- Change handler to what we created above
    href={item.get('product_url')}
    className={classNames(
      theme.root,
      config.get('simple') && theme.simple,
      theme.productCard,
    )}
  >
)

// ...

2. Product Impressions.

📘

Components:

If you need to send the Product Impressions you would need to adjust 2 components depending on the current way of loading new products, either with Pagination (StaticResults/index.ts component) or Lazy Loading (LazyResults/index.ts component).

/**
 * @module components/search/LazyResults
 */
import React, { Component, createElement, lifecycle } from 'react'; //add lifecycle
import { is, List, Map } from 'immutable';
import { connectItems } from '@findify/react-connect';
import { compose, withPropsOnChange, setDisplayName } from 'recompose';
import withTheme from 'helpers/withTheme';
import withLazy from 'helpers/withLazy';
import withColumns from 'helpers/withColumns';
import view from 'components/search/LazyResults/view';
import styles from 'components/search/LazyResults/styles.css';

/* GA events and helper functions */
const isSmartCollection = (config) => config.get('type') === 'smart-collection'; 

const pushDataLayers = (data, config) => window.dataLayer && data && window.dataLayer.push({
    event: "data.pageView",
    ecommerce: {
        currencyCode: config.getIn(['currency_config', 'code']),
        impressions: data.toJS()
    }
});

const pushImpressions = (item, index, listType) => ({
  name: item.get('title'), // Name or ID is required.
  id: item.getIn(['sku',0]),
  price: item.getIn(['price', 0]),
  brand: item.get('brand'),
  category: item.getIn(['category', 0, 'category1']),
  list: listType,
  position: index + 1
});

const sendGAEvents = (items, config, meta) => {
  const offset = meta.get('offset');
  const limit = meta.get('limit');
  const listType = isSmartCollection(config) ? 'Collection Results' : 'Search Results';
  const itemsCutLimit = items.slice(offset, offset + limit);
  const itemsImpressions = itemsCutLimit.map((item, index) => pushImpressions(item, offset + index, listType));
  if(itemsImpressions.size > 0){
    pushDataLayers(itemsImpressions, config);
  }
}

const checkIfTheSameProducts = (currItems) => {
  const lastDataLayer = dataLayer.filter(i => i.event  === 'data.pageView').pop();
  const check = lastDataLayer && lastDataLayer.ecommerce.impressions.length === currItems.size && lastDataLayer.ecommerce.impressions.filter(i => currItems.find(currItem => currItem.get('title') === i.name));
  return check && check.length === currItems.size;
}
/* END GA events and helper functions */

export default compose(
  setDisplayName('LazyResults'),
  withTheme(styles),

  /**
   * Connect columns count
   * To customize column count pass mapper to withColumns
   * @param {Function} [columnsMapper] - maps search layout width to columns count
  */
  withColumns(),
  
  connectItems,
  withLazy(),
  
  // add the lifecycle below
  lifecycle({
    componentDidMount(){
      const { config, items, meta } = this.props;
      if(!checkIfTheSameProducts(items)){
        sendGAEvents(items, config, meta);
      }
    },
    componentDidUpdate(prevProps){
      const { config, items, meta } = this.props;
      if(!checkIfTheSameProducts(items)){
        sendGAEvents(items, config, meta);
      }
    }
  })
)(view);
/**
 * @module components/search/StaticResults
 */
import React from 'react';
import { compose, setDisplayName, withPropsOnChange, lifecycle } from 'recompose'; //add lifecycle
import { connectConfig } from '@findify/react-connect';
import withTheme from 'helpers/withTheme';
import withColumns from 'helpers/withColumns';

import view from 'components/search/StaticResults/view';
import styles from 'components/search/StaticResults/styles.css';

/* GA events and helper functions */
const isSmartCollection = (config) => config.get('type') === 'smart-collection'; 

const pushDataLayers = (data, config) => window.dataLayer && data && window.dataLayer.push({
    event: "data.pageView",
    ecommerce: {
        currencyCode: config.getIn(['currency_config', 'code']),
        impressions: data.toJS()
    }
});

const pushImpressions = (item, index, listType) => ({
  name: item.get('title'), // Name or ID is required.
  id: item.getIn(['sku',0]),
  price: item.getIn(['price', 0]),
  brand: item.get('brand'),
  category: item.getIn(['category', 0, 'category1']),
  list: listType,
  position: index + 1
});

const sendGAEvents = (items, config, meta) => {
  const offset = meta.get('offset');
  const limit = meta.get('limit');
  const listType = isSmartCollection(config) ? 'Collection Results' : 'Search Results';
  const itemsCutLimit = items.slice(offset, offset + limit);
  const itemsImpressions = itemsCutLimit.map((item, index) => pushImpressions(item, offset + index, listType));
  if(itemsImpressions.size > 0){
    pushDataLayers(itemsImpressions, config);
  }
}

const checkIfTheSameProducts = (currItems) => {
  const lastDataLayer = dataLayer.filter(i => i.event  === 'data.pageView').pop();
  const check = lastDataLayer && lastDataLayer.ecommerce.impressions.length === currItems.size && lastDataLayer.ecommerce.impressions.filter(i => currItems.find(currItem => currItem.get('title') === i.name));
  return check && check.length === currItems.size;
}
/* END GA events and helper functions */

export default compose(
  setDisplayName('StaticResults'),

  withTheme(styles),

  connectConfig,

  withColumns()

  // add the lifecycle below
  lifecycle({
    componentDidMount(){
      const { config, items, meta } = this.props;
      if(!checkIfTheSameProducts(items)){
        sendGAEvents(items, config, meta);
      }
    },
    componentDidUpdate(prevProps){
      const { config, items, meta } = this.props;
      if(!checkIfTheSameProducts(items)){
        sendGAEvents(items, config, meta);
      }
    }
  })
  
)(view);