Skip to content

Commit

Permalink
Merge pull request #398 from woocommerce/update/separate-site-tag-set…
Browse files Browse the repository at this point in the history
…up-and-event-tracking

Separate the global site tag setup and event tracking
  • Loading branch information
martynmjones authored Apr 4, 2024
2 parents 3d4e4b4 + 5d587a3 commit 7137073
Show file tree
Hide file tree
Showing 13 changed files with 314 additions and 180 deletions.
2 changes: 0 additions & 2 deletions assets/js/src/config.js

This file was deleted.

36 changes: 31 additions & 5 deletions assets/js/src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
// Initialize tracking for classic WooCommerce pages
import { trackClassicPages } from './integrations/classic';
window.wcgai.trackClassicPages = trackClassicPages;
import { setupEventHandlers } from './tracker';
import { classicTracking } from './integrations/classic';
import { blocksTracking } from './integrations/blocks';

// Initialize tracking for Block based WooCommerce pages
import './integrations/blocks';
// Wait for 'ga4w:ready' event if `window.ga4w` is not there yet.
if ( window.ga4w ) {
initializeTracking();
} else {
document.addEventListener( 'ga4w:ready', initializeTracking );

// Warn if there is still nothing after the document is fully loded.
if ( document.readyState === 'complete' ) {
warnIfDataMissing();
} else {
window.addEventListener( 'load', warnIfDataMissing );
}
}
function initializeTracking() {
const getEventHandler = setupEventHandlers( window.ga4w.settings );

classicTracking( getEventHandler, window.ga4w.data );
blocksTracking( getEventHandler );
}

function warnIfDataMissing() {
if ( ! window.ga4w ) {
// eslint-disable-next-line no-console -- It's not an error, as one may load the script later, but we'd like to warn developers if it's about to be missing.
console.warn(
'Google Analytics for WooCommerce: Configuration and tracking data not found after the page was fully loaded. Make sure the `woocommerce-google-analytics-integration-data` script gets eventually loaded.'
);
}
}
72 changes: 37 additions & 35 deletions assets/js/src/integrations/blocks.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,50 @@
import { removeAction } from '@wordpress/hooks';
import { addUniqueAction } from '../utils';
import { tracker } from '../tracker';
import { ACTION_PREFIX, NAMESPACE } from '../constants';

addUniqueAction(
`${ ACTION_PREFIX }-product-render`,
NAMESPACE,
tracker.eventHandler( 'view_item' )
);
// We add actions asynchronosly, to make sure handlers will have the config available.
export const blocksTracking = ( getEventHandler ) => {
addUniqueAction(
`${ ACTION_PREFIX }-product-render`,
NAMESPACE,
getEventHandler( 'view_item' )
);

addUniqueAction(
`${ ACTION_PREFIX }-cart-remove-item`,
NAMESPACE,
tracker.eventHandler( 'remove_from_cart' )
);
addUniqueAction(
`${ ACTION_PREFIX }-cart-remove-item`,
NAMESPACE,
getEventHandler( 'remove_from_cart' )
);

addUniqueAction(
`${ ACTION_PREFIX }-checkout-render-checkout-form`,
NAMESPACE,
tracker.eventHandler( 'begin_checkout' )
);
addUniqueAction(
`${ ACTION_PREFIX }-checkout-render-checkout-form`,
NAMESPACE,
getEventHandler( 'begin_checkout' )
);

// These actions only works for All Products Block
addUniqueAction(
`${ ACTION_PREFIX }-cart-add-item`,
NAMESPACE,
( { product } ) => {
tracker.eventHandler( 'add_to_cart' )( { product } );
}
);
// These actions only works for All Products Block
addUniqueAction(
`${ ACTION_PREFIX }-cart-add-item`,
NAMESPACE,
( { product } ) => {
getEventHandler( 'add_to_cart' )( { product } );
}
);

addUniqueAction(
`${ ACTION_PREFIX }-product-list-render`,
NAMESPACE,
tracker.eventHandler( 'view_item_list' )
);
addUniqueAction(
`${ ACTION_PREFIX }-product-list-render`,
NAMESPACE,
getEventHandler( 'view_item_list' )
);

addUniqueAction(
`${ ACTION_PREFIX }-product-view-link`,
NAMESPACE,
tracker.eventHandler( 'select_content' )
);
addUniqueAction(
`${ ACTION_PREFIX }-product-view-link`,
NAMESPACE,
getEventHandler( 'select_content' )
);
};

/**
/*
* Remove additional actions added by WooCommerce Core which are either
* not supported by Google Analytics for WooCommerce or are redundant
* since Google retired Universal Analytics.
Expand Down
39 changes: 17 additions & 22 deletions assets/js/src/integrations/classic.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { tracker } from '../tracker';
import { getProductFromID } from '../utils';

/**
Expand All @@ -13,6 +12,7 @@ import { getProductFromID } from '../utils';
*
* It also handles some Block events that are not fired reliably for `woocommerce/all-products` block.
*
* @param {Function} getEventHandler
* @param {Object} data - The tracking data from the current page load, containing the following properties:
* @param {Object} data.events - An object containing the events to be instantly tracked.
* @param {Object} data.cart - The cart object.
Expand All @@ -21,26 +21,21 @@ import { getProductFromID } from '../utils';
* @param {Object} data.added_to_cart - The product added to cart.
* @param {Object} data.order - The order object.
*/
export function trackClassicPages( {
events,
cart,
products,
product,
added_to_cart: addedToCart,
order,
} ) {
export function classicTracking(
getEventHandler,
{ events, cart, products, product, added_to_cart: addedToCart, order }
) {
// Instantly track the events listed in the `events` object.
const eventData = {
storeCart: cart,
products,
product,
order,
};
Object.values( events ?? {} ).forEach( ( eventName ) => {
if ( eventName === 'add_to_cart' ) {
tracker.eventHandler( eventName )( { product: addedToCart } );
getEventHandler( eventName )( { product: addedToCart } );
} else {
tracker.eventHandler( eventName )( eventData );
getEventHandler( eventName )( {
storeCart: cart,
products,
product,
order,
} );
}
} );

Expand Down Expand Up @@ -70,7 +65,7 @@ export function trackClassicPages( {
return;
}

tracker.eventHandler( 'add_to_cart' )( { product: productToHandle } );
getEventHandler( 'add_to_cart' )( { product: productToHandle } );
};

/**
Expand All @@ -92,7 +87,7 @@ export function trackClassicPages( {
* @param {HTMLElement|Object} element - The HTML element clicked on to trigger this event
*/
function removeFromCartHandler( element ) {
tracker.eventHandler( 'remove_from_cart' )( {
getEventHandler( 'remove_from_cart' )( {
product: getProductFromID(
parseInt( element.target.dataset.product_id ),
products,
Expand Down Expand Up @@ -159,7 +154,7 @@ export function trackClassicPages( {
return;
}

tracker.eventHandler( 'select_content' )( {
getEventHandler( 'select_content' )( {
product: getProductFromID(
parseInt( productId ),
products,
Expand Down Expand Up @@ -207,7 +202,7 @@ export function trackClassicPages( {

if ( isAddToCartButton ) {
// Add to cart.
tracker.eventHandler( 'add_to_cart' )( {
getEventHandler( 'add_to_cart' )( {
product: getProductFromID(
parseInt( productId ),
products,
Expand All @@ -216,7 +211,7 @@ export function trackClassicPages( {
} );
} else if ( viewLink || button || nameLink ) {
// Product image or add-to-cart-like button.
tracker.eventHandler( 'select_content' )( {
getEventHandler( 'select_content' )( {
product: getProductFromID(
parseInt( productId ),
products,
Expand Down
74 changes: 18 additions & 56 deletions assets/js/src/tracker/index.js
Original file line number Diff line number Diff line change
@@ -1,75 +1,37 @@
import { config } from '../config';
import * as formatters from './data-formatting';

let instance;

/**
* A tracking utility for initializing a GA4 and tracking accepted events.
* Get a new event handler constructing function, based on given settings.
*
* @class
* @param {Object} settings - The settings object.
* @param {Array} settings.events - The list of supported events.
* @param {string} settings.tracker_function_name - The name of the global function to call for tracking.
* @return {function(string): Function} - A function to get event handlers for specific events.
*/
class Tracker {
export function setupEventHandlers( {
events,
tracker_function_name: trackerFunctionName,
} ) {
/**
* Constructs a new instance of the Tracker class.
* Returns an event handler for a specified event name.
*
* @throws {Error} If an instance of the Tracker already exists.
*/
constructor() {
if ( instance ) {
throw new Error( 'Cannot instantiate more than one Tracker' );
}
instance = this;

window.dataLayer = window.dataLayer || [];

function gtag() {
window.dataLayer.push( arguments );
}

window[ config.tracker_function_name ] = gtag;

// Set up default consent state, denying all for EEA visitors.
for ( const mode of config.consent_modes || [] ) {
gtag( 'consent', 'default', mode );
}

gtag( 'js', new Date() );
gtag( 'set', `developer_id.${ config.developer_id }`, true );
gtag( 'config', config.gtag_id, {
allow_google_signals: config.allow_google_signals,
link_attribution: config.link_attribution,
anonymize_ip: config.anonymize_ip,
logged_in: config.logged_in,
linker: config.linker,
custom_map: config.custom_map,
} );
}

/**
* Creates and returns an event handler for a specified event name.
*
* @param {string} name The name of the event.
* @param {string} eventName The name of the event.
* @return {function(*): void} Function for processing and tracking the event.
* @throws {Error} If the event name is not supported.
*/
eventHandler( name ) {
function getEventHandler( eventName ) {
/* eslint import/namespace: [ 'error', { allowComputed: true } ] */
const formatter = formatters[ name ];
const formatter = formatters[ eventName ];
if ( typeof formatter !== 'function' ) {
throw new Error( `Event ${ name } is not supported.` );
throw new Error( `Event ${ eventName } is not supported.` );
}

return function trackerEventHandler( data ) {
return function eventHandler( data ) {
const eventData = formatter( data );
if ( config.events.includes( name ) && eventData ) {
window[ config.tracker_function_name ](
'event',
name,
eventData
);
if ( events.includes( eventName ) && eventData ) {
window[ trackerFunctionName ]( 'event', eventName, eventData );
}
};
}
return getEventHandler;
}

export const tracker = Object.freeze( new Tracker() );
9 changes: 4 additions & 5 deletions assets/js/src/utils/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { addAction, removeAction } from '@wordpress/hooks';
import { config } from '../config.js';

/**
* Formats data into the productFieldObject shape.
Expand Down Expand Up @@ -85,15 +84,15 @@ export const addUniqueAction = ( hookName, namespace, callback ) => {
* @return {string} - The product ID
*/
export const getProductId = ( product ) => {
const identifier =
const productIdentifier =
product.extensions?.woocommerce_google_analytics_integration
?.identifier;

if ( identifier !== undefined ) {
return identifier;
if ( productIdentifier !== undefined ) {
return productIdentifier;
}

if ( config.identifier === 'product_sku' ) {
if ( window.ga4w?.settings?.identifier === 'product_sku' ) {
return product.sku ? product.sku : '#' + product.id;
}

Expand Down
Loading

0 comments on commit 7137073

Please sign in to comment.