Skip to main content

Example Google Analytics GA4 Implementation

The following is an example implementation that allows sending events through to Google Analytics for reporting. This script can be placed in your Wordpress theme's header.php file inside <script></script> tags. You can edit this file by going to Appearance > Theme Editor > header.php. It does not replace the Google Analytics script which must also be included.

It contains the following eCommerce GA4 events:

  • view_item
  • view_item_list
  • add_to_cart
  • remove_from_cart
  • begin_checkout
  • purchase

The code below is just an example. Events can be customised or added. See the Analytics section for more information on plugin events.

Example: GA4 eCommerce Events
(function withwine_GA4() { //Encapsulate the code in a function to avoid polluting the global scope

//View item event history is used to ensure the user has viewed the product before adding it to the cart
const viewItemEventHistory = {};

/**
* Format a product from WithWine in GA4 format
* @param {Object} product
* @param {number} product.id
* @param {string | null} product.skuCode
* @param {string} product.name
* @param {string | null} product.brandNameOverride
* @param {string | null} product.vintage
* @param {string | null} product.wineType
* @param {string | null} product.productType
* @param {string | null} product.productGroup
* @param {number | null} product.fullPrice
* @param {string | null} product.fullPriceType
* @param {number | null} product.minimumUnitPurchase
* @param {number | null} product.minimumUnitPrice
* @param {number | null} product.calculatedUserPrice
* @param {string | null} product.state
* @param {string | null} product.region
* @param {string | null} product.subRegion
* @param {string | null} product.variety
* @param {Object[] | null} product.customFields
* @param {string | null} product.paymentCurrency
* @param {number} [index]
* @param {number} [quantity]
* @param {number} [itemTotal]
*/
function withwine_GA4_formatProduct(product, index, quantity, itemTotal) {
return {
item_id: product.id,
item_name: product.name,
index: index || 0,
item_brand: product.brandNameOverride || undefined,
item_category: product.productType,
item_category2: product.productGroup,
item_category3: product.wineType,
item_category4: product.variety,
item_category5: product.subRegion || product.region || product.state,
//Format the item total as a single unit price:
price: typeof itemTotal === 'undefined'
? (product.minimumUnitPrice == null || product.minimumUnitPurchase == null ? null : product.minimumUnitPrice / product.minimumUnitPurchase)
: (quantity === 0 ? null : (itemTotal / quantity)),
quantity: typeof quantity === 'undefined' ? product.minimumUnitPurchase : quantity,
};
}

['view_products'].forEach(e => {
window.addEventListener('out:withwine:' + e, function (event) {
if (!event.detail.v1.products || !event.detail.v1.products.length) {
return;
}
//If the event is from a type="product-details" or type="product-view-event" component, then send a view_item event
if (event.detail.v1.component.type.toLowerCase() === 'product-details' ||
event.detail.v1.component.type.toLowerCase() === 'product-view-event') {
//Remember that we've sent a view_item event for this product so we don't send it again if the user adds it to the cart
viewItemEventHistory[event.detail.v1.products[0].id] = true;
gtag('event', 'view_item', {
currency: event.detail.v1.products[0].paymentCurrency,
value: event.detail.v1.products[0].minimumUnitPrice,
items: [withwine_GA4_formatProduct(event.detail.v1.products[0])],
});
} else {
//If the event is from another product component, then we're probably viewing items in list
//Note: You can use the shortcode attributes data_galistid and data_galistname to override the default values sent to Google Analytics
var listId = event.detail.v1.component.data.galistid ||
(event.detail.v1.component.type.toLowerCase() +
(event.detail.v1.component.data.productlist_id ||
(event.detail.v1.component.data.productlist && event.detail.v1.component.data.productlist.id) ||
''));
var listName = event.detail.v1.component.data.galistname || listId;
gtag('event', 'view_item_list', {
item_list_id: listId,
item_list_name: listName,
items: event.detail.v1.products.map(function (p, i) { return withwine_GA4_formatProduct(p, i); }).slice(0, 200),
});
}
});
});

['add_to_cart'].forEach(e => {
window.addEventListener('out:withwine:' + e, function (event) {

if (!event.detail.v1.added.product || !event.detail.v1.cart) {
return;
}

if (!viewItemEventHistory[event.detail.v1.added.product.id]) {
//Workaround to account for GA4's "closed funnel" product journey report
// - If the GA4 hasn't recived a view_item event, then it will not track the product journey in this report
// - So we send a view_item event before the add_to_cart event if one wasn't already sent for this page
// - You can comment out the gtag line below if you don't want to use this workaround
viewItemEventHistory[event.detail.v1.added.product.id] = true;
gtag('event', 'view_item', {
currency: event.detail.v1.added.product.paymentCurrency,
value: event.detail.v1.added.product.minimumUnitPrice,
items: [withwine_GA4_formatProduct(event.detail.v1.added.product)],
});
//End of workaround
}

var addedQuantity = event.detail.v1.added.quantity - event.detail.v1.added.previousQuantity;
if (addedQuantity > 0) {

gtag('event', 'add_to_cart', {
currency: event.detail.v1.added.product.paymentCurrency,
value: (event.detail.v1.added.product.calculatedUserPrice ?? event.detail.v1.added.product.minimumUnitPrice) *
addedQuantity / event.detail.v1.added.product.minimumUnitPurchase,
items: [withwine_GA4_formatProduct(event.detail.v1.added.product)],
});
}
});
});

['remove_from_cart'].forEach(e => {
window.addEventListener('out:withwine:' + e, function (event) {

if (!event.detail.v1.removed.product || !event.detail.v1.cart) {
return;
}

var removedQuantity = event.detail.v1.removed.previousQuantity - event.detail.v1.removed.quantity;
if (removedQuantity > 0) {
gtag('event', 'remove_from_cart', {
currency: event.detail.v1.removed.product.paymentCurrency,
value: (event.detail.v1.removed.product.calculatedUserPrice ?? event.detail.v1.removed.product.minimumUnitPrice) *
removedQuantity / event.detail.v1.removed.product.minimumUnitPurchase,
items: [withwine_GA4_formatProduct(event.detail.v1.removed.product)],
});
}
});
});

['begin_checkout'].forEach(e => {
window.addEventListener('out:withwine:' + e, function (event) {

if (!event.detail.v1.cart || event.detail.v1.cart.length <= 0) {
return;
}

//Stop the checkout from redirecting automatically. This will give us time to send the event to GA4
event.preventDefault();

//If sending the event takes more than 1 second, just redirect instead of making the user wait too long
var redirectTimeout = setTimeout(function () {
location.href = event.detail.v1.url;
}, 1000);

gtag('event', 'begin_checkout', {
currency: event.detail.v1.cart[0].product.paymentCurrency,
value: event.detail.v1.total,
shipping: event.detail.v1.shipping,
tax: event.detail.v1.totalTaxAdded,
items: event.detail.v1.cart
.filter(function (cartItem) {
return cartItem.quantity > 0;
})
.map(function (cartItem, i) {
var ga4Item = withwine_GA4_formatProduct(cartItem.product, i, cartItem.quantity, cartItem.total)
if (cartItem.discount) {
ga4Item.discount = cartItem.discount;
}
}),
event_callback: function () {
//When the event is complete, redirect the user to the checkout page
location.href = event.detail.v1.url;
clearTimeout(redirectTimeout);
},
});

});
});

['checkout_success'].forEach(e => {
window.addEventListener('out:withwine:' + e, function (event) {

if (event.detail.v1.eventType !== 'Order') {
return;
}

event.preventDefault();

var payload = {
currency: event.detail.v1.paymentCurrency || 'AUD',
transaction_id: event.detail.v1.orderNumber,
value: event.detail.v1.orderTotal,
shipping: event.detail.v1.shipping,
items: event.detail.v1.orderItems
.filter(function (item) { return item.productId && item.productId > 0; })
.map(function (item, index) {
//Product should be loaded but just in case it isn't (eg: maybe it's a member-only product and the user isn't logged into the website), we'll send a fallback
return item.product
? withwine_GA4_formatProduct(item.product, index, item.quantity, item.orderItemTotal)
: {
item_id: item.productId,
item_name: 'Product #' + item.productId,
index: index,
price: item.orderItemTotal,
quantity: item.quantity,
};
}),
};

if (event.detail.v1.totalTaxAdded) {
payload.tax = event.detail.v1.totalTaxAdded;
}

gtag('event', 'purchase', payload);

});
});


})();