Google Tag Manager Integration

#1. Background

#2. Time Estimates

  • Set up in Platform: n/a hours
  • Integration: 1.5 hours
  • Styling: n/a

#3. Functional Overview

📘

Components:

#4. Integration Steps

1. Product Click Events.

In order to send Product Click events to Google Analytics, you need to override the default Findify onClick function in 'components/Cards/Product/index.tsx' and create a 'sendFindifyClickEvent' function:

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

import cx from 'classnames';
import trackProductPosition from 'helpers/trackProductPosition';

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: [{
            base_product: item.get('item_group_id'),
            variant: item.get('selected_variant_id'),
            name: item.get('title'), // Name or ID is required.
            id: item.get('id'),
            price: item.getIn(['price', 0]),
            brand: item.get('brand'),
            category: item.getIn(['category', 0, 'category1']),
            list: list,
            position: index + 1
         }]
       }
     }
  })
);

export default ({
  index,
  item,
  theme = styles,
  className,
  config,
  highlighted,
  isAutocomplete,
  isSearch,
  isCollection,
  isRecommendation
}: IProductCardProps) => {
  const container = trackProductPosition(item);
  const [variant, setVariant] = useVariants(item);

  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'; 
  }

  const itemOnclick = (e) => {
    const openInNewWindow = e && (e.ctrlKey || e.metaKey);
    const productUrl = item.get('product_url');

    item.analytics.sendEvent(
      'click-item',
      { rid: item.meta.get('rid'), item_id: item.get('id'), variant_item_id:  item.get('selected_variant_id')},
      !openInNewWindow
    );
    
    if (isSearch && !openInNewWindow && typeof window !== 'undefined') {  
      document.location.hash = item.get('id');
    }

    sendFindifyClickEvent(list, item, config, index);

  }

  return (
    <a
      ref={container}
      data-element="card"
      className={cx(
        theme.root,
        theme[config.get('template')],
        highlighted && theme.highlighted,
        isAutocomplete && theme.autocomplete,
        className
      )}
      href={url}
      onClick={itemOnclick}
    >
      // default code
    </a>
  );
};

2. Product Impressions.

If you need to send Product Impressions to StaticResults (or LazyResults), Recommendations and Autocomplete, this is what the build looks like:

/**
 * @module components/search/LazyResults
 */
import {useEffect} from 'react';
import { useConfig } from '@findify/react-connect';
import styles from 'components/search/LazyResults/styles.css';
import useLazy from 'helpers/useLazy';

// default code

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

const pushImpressions = (item, index, listType) => ({
  base_product: item.get('item_group_id'),
  variant: item.get('selected_variant_id'),
  name: item.get('title'), // Name or ID is required.
  id: item.get('id'),
  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, isCollection) => {
  const offset = meta.get('offset');
  const limit = meta.get('limit');
  const listType = isCollection ? '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, isCollection);
  }
}

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;
}

const LazyResults = ({ theme = styles, card = ProductCard, itemConfig, isMobile, isCollection, meta }: ILazyResultsProps) => {
  const {
    // default code
    items
  } = useLazy();
  const { config } = useConfig<Immutable.SearchConfig>();

// default code
  
  useEffect(
    () => {
      if(!checkIfTheSameProducts(items)){
        sendGAEvents(items, config, meta, isCollection);
      }
    }, [items]
  );

  return (
    // default code
  );
};

export default LazyResults;
/**
 * @module components/Dropdown
 */
import {useEffect} from 'react';
import { useItems, useQuery, useConfig } from '@findify/react-connect';
import { Immutable } from '@findify/store-configuration';
import styles from 'layouts/Recommendation/Slider/styles.css';

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

const pushImpressions = (item, index, listType) => ({
  base_product: item.get('item_group_id'),
  variant: item.get('selected_variant_id'),
  name: item.get('title'), // Name or ID is required.
  id: item.get('id'),
  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 = 'Recommendation 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;
}

export default ({ theme = styles }) => {
  const { items, config } = useItems<Immutable.RecommendationConfig>();
  
  const { meta } = useQuery();
  const { config: baseConfig } = useConfig<Immutable.AutocompleteConfig>();

  useEffect(
    () => {
      if(!checkIfTheSameProducts(items)){
        sendGAEvents(items, baseConfig, meta);
      }
    },[]
  )

  if (!items?.size) return null;
  return (
    <>
      // default code
    </>
  );
};
/**
 * @module components/autocomplete/Products
 */
import { useCallback, useEffect } from 'react';
import styles from 'components/autocomplete/Products/styles.css';
import { useItems, useQuery } from '@findify/react-connect';
import { Immutable } from '@findify/store-configuration';

// default code

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

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

const sendGAEvents = (items, config) => {
  const listType = 'Autocomplete Results';
  const itemsImpressions = items.map((item, index) => pushImpressions(item, 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;
}

export default ({
  theme = styles,
  config,
  isTrendingSearches,
  highlightedItem,
}) => {
  const {
    items,
    config: baseConfig,
  } = useItems<Immutable.AutocompleteConfig>();
  const { query } = useQuery();
  
  // default code

  useEffect(
    () => {
      if(!checkIfTheSameProducts(items)){
        sendGAEvents(items, config);
      }
    }, []
  )

  return (
   // default code
  );
};

#5. MJS Version

This module has been optimized for MJS version 7.1.27