This guide covers full ecommerce funnel tracking for GA4 on Salesforce Commerce Cloud, from product impressions to purchase confirmation.
Ecommerce Funnel Overview
Product List → Product View → Add to Cart → Checkout → Purchase
↓ ↓ ↓ ↓ ↓
view_item_list view_item add_to_cart begin_checkout purchase
Product List Tracking
Track product impressions on category and search pages.
Category Page Implementation
Controller Extension:
// cartridges/app_custom/cartridge/controllers/Search.js
'use strict';
var server = require('server');
var Search = module.superModule;
server.extend(Search);
server.append('Show', function (req, res, next) {
var viewData = res.getViewData();
var productSearch = viewData.productSearch;
if (productSearch && productSearch.productIds) {
var impressions = [];
productSearch.productIds.forEach(function(productId, index) {
var product = productSearch.getProduct(productId.productSearchHit);
impressions.push({
item_id: productId.productID,
item_name: product.productName,
item_category: viewData.productSearch.category.displayName || '',
item_list_name: 'Category: ' + (viewData.productSearch.category.displayName || 'Search'),
index: index,
price: product.price.sales ? product.price.sales.value : 0
});
});
viewData.ga4Impressions = {
item_list_name: 'Category: ' + (viewData.productSearch.category.displayName || 'Search'),
items: impressions
};
}
res.setViewData(viewData);
return next();
});
module.exports = server.exports();
Client-side Tracking:
// Track product impressions on page load
(function() {
var impressions = window.ga4Impressions;
if (impressions && impressions.items.length > 0) {
gtag('event', 'view_item_list', {
item_list_name: impressions.item_list_name,
items: impressions.items
});
}
})();
// Track product click
$(document).on('click', '.product-tile a', function() {
var $tile = $(this).closest('.product-tile');
var productData = $tile.data('product');
gtag('event', 'select_item', {
item_list_name: window.ga4Impressions.item_list_name,
items: [{
item_id: productData.id,
item_name: productData.name,
item_category: productData.category,
index: $tile.index(),
price: productData.price
}]
});
});
Purchase Tracking
The purchase event is critical for revenue tracking.
Order Confirmation Page
Controller Extension:
// cartridges/app_custom/cartridge/controllers/Order.js
'use strict';
var server = require('server');
var Order = module.superModule;
server.extend(Order);
server.append('Confirm', function (req, res, next) {
var viewData = res.getViewData();
var order = viewData.order;
if (order) {
var items = [];
order.items.items.forEach(function(item) {
items.push({
item_id: item.id,
item_name: item.productName,
item_category: item.categoryName || '',
item_brand: item.brand || '',
price: item.priceTotal.price.sales.value / item.quantity,
quantity: item.quantity,
discount: item.priceTotal.adjustedPrice ?
(item.priceTotal.price.sales.value - item.priceTotal.adjustedPrice.value) : 0
});
});
viewData.ga4Purchase = {
transaction_id: order.orderNumber,
value: order.totals.grandTotal.replace(/[^0-9.]/g, ''),
currency: order.totals.currencyCode,
tax: order.totals.totalTax.replace(/[^0-9.]/g, ''),
shipping: order.totals.totalShippingCost.replace(/[^0-9.]/g, ''),
coupon: order.totals.discounts.length > 0 ?
order.totals.discounts[0].couponCode : '',
items: items
};
}
res.setViewData(viewData);
return next();
});
module.exports = server.exports();
ISML Template Integration:
<isif condition="${pdict.ga4Purchase}">
<script>
gtag('event', 'purchase', {
transaction_id: '${pdict.ga4Purchase.transaction_id}',
value: ${pdict.ga4Purchase.value},
currency: '${pdict.ga4Purchase.currency}',
tax: ${pdict.ga4Purchase.tax},
shipping: ${pdict.ga4Purchase.shipping},
<isif condition="${pdict.ga4Purchase.coupon}">
coupon: '${pdict.ga4Purchase.coupon}',
</isif>
items: <isprint value="${JSON.stringify(pdict.ga4Purchase.items)}" encoding="off"/>
});
</script>
</isif>
Checkout Step Tracking
Track each step of the checkout process.
Checkout Flow Events
// Shipping step
function trackShippingStep(checkoutData) {
gtag('event', 'add_shipping_info', {
currency: checkoutData.currency,
value: checkoutData.subtotal,
coupon: checkoutData.couponCode || '',
shipping_tier: checkoutData.shippingMethodName,
items: checkoutData.items
});
}
// Payment step
function trackPaymentStep(checkoutData) {
gtag('event', 'add_payment_info', {
currency: checkoutData.currency,
value: checkoutData.subtotal,
coupon: checkoutData.couponCode || '',
payment_type: checkoutData.paymentMethodType,
items: checkoutData.items
});
}
Promotion Tracking
Track promotional interactions.
// View promotion
function trackViewPromotion(promoData) {
gtag('event', 'view_promotion', {
creative_name: promoData.creativeName,
creative_slot: promoData.slotName,
promotion_id: promoData.id,
promotion_name: promoData.name
});
}
// Click promotion
function trackSelectPromotion(promoData) {
gtag('event', 'select_promotion', {
creative_name: promoData.creativeName,
creative_slot: promoData.slotName,
promotion_id: promoData.id,
promotion_name: promoData.name
});
}
Refund Tracking
Track order refunds server-side.
// Server-side refund tracking via Measurement Protocol
var https = require('https');
function trackRefund(orderNumber, refundAmount, currency) {
var measurementId = 'G-XXXXXXXXXX';
var apiSecret = 'YOUR_API_SECRET';
var payload = {
client_id: 'SFCC-Backend',
events: [{
name: 'refund',
params: {
transaction_id: orderNumber,
value: refundAmount,
currency: currency
}
}]
};
var postData = JSON.stringify(payload);
var options = {
hostname: 'www.google-analytics.com',
port: 443,
path: '/mp/collect?measurement_id=' + measurementId + '&api_secret=' + apiSecret,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
};
var req = https.request(options);
req.write(postData);
req.end();
}
Data Quality Checklist
Required Validations
- Transaction ID is unique per order
- Revenue matches order total (excluding tax and shipping)
- Product prices are unit prices, not totals
- Currency code is ISO 4217 format
- No PII in event parameters
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Duplicate purchases | Page refresh | Use dataLayer.push with event callback |
| Missing revenue | Formatted currency strings | Strip currency symbols |
| Wrong item count | Including bundled items | Count parent products only |
Testing Recommendations
- Use GA4 DebugView for real-time validation
- Test complete funnel from product view to purchase
- Verify refund tracking with test orders
- Check cross-device tracking consistency