Track the complete customer journey from product browsing to purchase completion using Google Analytics 4 enhanced ecommerce on NopCommerce.
Before You Begin
Prerequisites:
- GA4 setup complete (see GA4 Setup Guide)
- GA4 Measurement ID configured
- Ecommerce tracking enabled in plugin/implementation
- Understanding of GA4 ecommerce event structure
GA4 Ecommerce Events:
view_item- Product detail page viewview_item_list- Category/search results viewselect_item- Product clicked from listadd_to_cart- Product added to cartremove_from_cart- Product removed from cartview_cart- Shopping cart viewedbegin_checkout- Checkout process startedadd_shipping_info- Shipping method selectedadd_payment_info- Payment method selectedpurchase- Order completed
Product View Tracking (view_item)
Razor View Implementation
Add to /Themes/YourTheme/Views/Product/ProductTemplate.Simple.cshtml:
@model ProductDetailsModel
@section scripts {
<script>
gtag('event', 'view_item', {
'currency': '@Model.ProductPrice.CurrencyCode',
'value': @Model.ProductPrice.PriceValue,
'items': [{
'item_id': '@Model.Id',
'item_name': '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Name))',
'item_brand': '@(Model.ProductManufacturers.FirstOrDefault()?.Name ?? "")',
'item_category': '@(Model.Breadcrumb.CategoryBreadcrumb.LastOrDefault()?.Name ?? "")',
'price': @Model.ProductPrice.PriceValue,
'quantity': 1
}]
});
</script>
}
C# Plugin Implementation
// In custom plugin ViewComponent or controller
public async Task<IViewComponentResult> InvokeAsync()
{
var product = await _productService.GetProductByIdAsync(productId);
var priceModel = await _priceCalculationService.GetFinalPriceAsync(product);
var price = (await _taxService.GetProductPriceAsync(product, priceModel.finalPrice)).price;
var ecommerceData = new
{
currency = (await _workContext.GetWorkingCurrencyAsync()).CurrencyCode,
value = price,
items = new[]
{
new
{
item_id = product.Id.ToString(),
item_name = product.Name,
item_brand = (await _manufacturerService.GetProductManufacturersByProductIdAsync(product.Id))
.FirstOrDefault()?.Manufacturer?.Name ?? "",
item_category = await GetProductCategoryAsync(product),
price = price,
quantity = 1
}
}
};
ViewBag.GA4ViewItem = System.Text.Json.JsonSerializer.Serialize(ecommerceData);
return View();
}
Category/List View Tracking (view_item_list)
Category Page Implementation
@model CategoryModel
@section scripts {
<script>
gtag('event', 'view_item_list', {
'item_list_id': '@Model.Id',
'item_list_name': '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Name))',
'items': [
@for (int i = 0; i < Model.Products.Count; i++)
{
var product = Model.Products[i];
<text>
{
'item_id': '@product.Id',
'item_name': '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(product.Name))',
'item_brand': '@(product.ProductManufacturers.FirstOrDefault()?.Name ?? "")',
'item_category': '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Name))',
'price': @product.ProductPrice.PriceValue,
'index': @i
}@(i < Model.Products.Count - 1 ? "," : "")
</text>
}
]
});
</script>
}
Product Click Tracking (select_item)
@* Add to product grid items *@
<div class="product-item" data-product='@System.Text.Json.JsonSerializer.Serialize(new {
item_id = product.Id,
item_name = product.Name,
price = product.ProductPrice.PriceValue,
item_list_name = Model.Name
})'>
<a href="@Url.RouteUrl("Product", new { SeName = product.SeName })"
class="product-link">
@product.Name
</a>
</div>
<script>
document.querySelectorAll('.product-link').forEach(function(link) {
link.addEventListener('click', function(e) {
var productData = JSON.parse(this.closest('.product-item').getAttribute('data-product'));
gtag('event', 'select_item', {
'item_list_name': productData.item_list_name,
'items': [{
'item_id': productData.item_id,
'item_name': productData.item_name,
'price': productData.price
}]
});
});
});
</script>
Add to Cart Tracking (add_to_cart)
JavaScript Implementation
@model ProductDetailsModel
<script>
// Override default add to cart handler
function addProductToCart_details(url) {
var form = $('#product-details-form');
var quantity = form.find('input[name="addtocart_@Model.Id.EnteredQuantity"]').val();
$.ajax({
cache: false,
url: url,
type: 'POST',
data: form.serialize(),
success: function(data) {
if (data.success) {
// Track add to cart event
gtag('event', 'add_to_cart', {
'currency': '@Model.ProductPrice.CurrencyCode',
'value': @Model.ProductPrice.PriceValue * parseFloat(quantity),
'items': [{
'item_id': '@Model.Id',
'item_name': '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Name))',
'item_brand': '@(Model.ProductManufacturers.FirstOrDefault()?.Name ?? "")',
'item_category': '@(Model.Breadcrumb.CategoryBreadcrumb.LastOrDefault()?.Name ?? "")',
'price': @Model.ProductPrice.PriceValue,
'quantity': parseFloat(quantity)
}]
});
}
// Original NopCommerce handler
displayAjaxLoading(false);
if (data.updatetopcartsectionhtml) {
$('.header-links').html(data.updatetopcartsectionhtml);
}
if (data.message) {
displayBarNotification(data.message, data.success ? 'success' : 'error', 3500);
}
}
});
}
</script>
C# Event Handler Approach
// In custom plugin - subscribe to shopping cart events
using Nop.Core.Events;
using Nop.Services.Events;
using Nop.Services.Orders;
public class ShoppingCartEventConsumer : IConsumer<EntityInsertedEvent<ShoppingCartItem>>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IProductService _productService;
private readonly IPriceCalculationService _priceCalculationService;
private readonly IWorkContext _workContext;
public ShoppingCartEventConsumer(
IHttpContextAccessor httpContextAccessor,
IProductService productService,
IPriceCalculationService priceCalculationService,
IWorkContext workContext)
{
_httpContextAccessor = httpContextAccessor;
_productService = productService;
_priceCalculationService = priceCalculationService;
_workContext = workContext;
}
public async Task HandleEventAsync(EntityInsertedEvent<ShoppingCartItem> eventMessage)
{
var cartItem = eventMessage.Entity;
// Only track shopping cart items, not wishlist
if (cartItem.ShoppingCartType != ShoppingCartType.ShoppingCart)
return;
var product = await _productService.GetProductByIdAsync(cartItem.ProductId);
var priceModel = await _priceCalculationService.GetFinalPriceAsync(product, cartItem.Quantity);
var currency = await _workContext.GetWorkingCurrencyAsync();
var eventData = new
{
event_name = "add_to_cart",
currency = currency.CurrencyCode,
value = priceModel.finalPrice * cartItem.Quantity,
items = new[]
{
new
{
item_id = product.Id.ToString(),
item_name = product.Name,
price = priceModel.finalPrice,
quantity = cartItem.Quantity
}
}
};
// Store in HttpContext for rendering in view
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext?.Items != null)
{
if (!httpContext.Items.ContainsKey("GA4EcommerceEvents"))
httpContext.Items["GA4EcommerceEvents"] = new List<object>();
((List<object>)httpContext.Items["GA4EcommerceEvents"]).Add(eventData);
}
}
}
Remove from Cart Tracking (remove_from_cart)
@* Shopping cart page *@
@model ShoppingCartModel
<script>
function removeFromCart(productId, productName, price, quantity) {
gtag('event', 'remove_from_cart', {
'currency': '@Model.OrderTotals.Currency',
'value': price * quantity,
'items': [{
'item_id': productId,
'item_name': productName,
'price': price,
'quantity': quantity
}]
});
}
// Attach to remove buttons
document.querySelectorAll('.remove-from-cart').forEach(function(btn) {
btn.addEventListener('click', function() {
var row = this.closest('.cart-item-row');
removeFromCart(
row.dataset.productId,
row.dataset.productName,
parseFloat(row.dataset.price),
parseInt(row.dataset.quantity)
);
});
});
</script>
View Cart Tracking (view_cart)
@model ShoppingCartModel
@section scripts {
<script>
gtag('event', 'view_cart', {
'currency': '@Model.OrderTotals.Currency',
'value': @Model.OrderTotals.SubTotalValue,
'items': [
@for (int i = 0; i < Model.Items.Count; i++)
{
var item = Model.Items[i];
<text>
{
'item_id': '@item.ProductId',
'item_name': '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(item.ProductName))',
'price': @item.UnitPrice,
'quantity': @item.Quantity
}@(i < Model.Items.Count - 1 ? "," : "")
</text>
}
]
});
</script>
}
Begin Checkout Tracking (begin_checkout)
@model CheckoutModel
@section scripts {
<script>
// Fire on checkout page load
gtag('event', 'begin_checkout', {
'currency': '@Model.OrderTotals.Currency',
'value': @Model.OrderTotals.SubTotalValue,
'items': [
@for (int i = 0; i < Model.Cart.Items.Count; i++)
{
var item = Model.Cart.Items[i];
<text>
{
'item_id': '@item.ProductId',
'item_name': '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(item.ProductName))',
'price': @item.UnitPrice,
'quantity': @item.Quantity
}@(i < Model.Cart.Items.Count - 1 ? "," : "")
</text>
}
]
});
</script>
}
Shipping Info Tracking (add_shipping_info)
@* Checkout shipping method page *@
<script>
function trackShippingSelection(shippingMethod, shippingCost) {
gtag('event', 'add_shipping_info', {
'currency': '@Model.OrderTotals.Currency',
'value': @Model.OrderTotals.SubTotalValue,
'shipping_tier': shippingMethod,
'items': @Html.Raw(GetCartItemsJson())
});
}
// Track when shipping method selected
document.querySelectorAll('input[name="shippingoption"]').forEach(function(radio) {
radio.addEventListener('change', function() {
if (this.checked) {
var method = this.closest('.shipping-option').dataset.methodName;
var cost = parseFloat(this.closest('.shipping-option').dataset.cost);
trackShippingSelection(method, cost);
}
});
});
</script>
Payment Info Tracking (add_payment_info)
@* Checkout payment method page *@
<script>
function trackPaymentSelection(paymentMethod) {
gtag('event', 'add_payment_info', {
'currency': '@Model.OrderTotals.Currency',
'value': @Model.OrderTotals.OrderTotalValue,
'payment_type': paymentMethod,
'items': @Html.Raw(GetCartItemsJson())
});
}
// Track when payment method selected
document.querySelectorAll('input[name="paymentmethod"]').forEach(function(radio) {
radio.addEventListener('change', function() {
if (this.checked) {
var method = this.closest('.payment-method').dataset.methodName;
trackPaymentSelection(method);
}
});
});
</script>
Purchase Tracking (purchase)
Order Confirmation Page
@model CheckoutCompletedModel
@section scripts {
<script>
@{
var order = await orderService.GetOrderByIdAsync(Model.OrderId);
var orderItems = await orderService.GetOrderItemsAsync(order.Id);
}
gtag('event', 'purchase', {
'transaction_id': '@order.CustomOrderNumber',
'value': @order.OrderTotal,
'tax': @order.OrderTax,
'shipping': @order.OrderShippingInclTax,
'currency': '@order.CustomerCurrencyCode',
'coupon': '@(order.DiscountUsageHistory.FirstOrDefault()?.Discount?.CouponCode ?? "")',
'items': [
@for (int i = 0; i < orderItems.Count; i++)
{
var item = orderItems[i];
var product = await _productService.GetProductByIdAsync(item.ProductId);
<text>
{
'item_id': '@item.ProductId',
'item_name': '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(product.Name))',
'price': @item.UnitPriceInclTax,
'quantity': @item.Quantity
}@(i < orderItems.Count - 1 ? "," : "")
</text>
}
]
});
</script>
}
C# Plugin Approach
// Event consumer for order placement
public class OrderPlacedEventConsumer : IConsumer<OrderPlacedEvent>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IOrderService _orderService;
private readonly IProductService _productService;
public async Task HandleEventAsync(OrderPlacedEvent eventMessage)
{
var order = eventMessage.Order;
var orderItems = await _orderService.GetOrderItemsAsync(order.Id);
var items = new List<object>();
foreach (var item in orderItems)
{
var product = await _productService.GetProductByIdAsync(item.ProductId);
items.Add(new
{
item_id = product.Id.ToString(),
item_name = product.Name,
price = item.UnitPriceInclTax,
quantity = item.Quantity
});
}
var purchaseData = new
{
event_name = "purchase",
transaction_id = order.CustomOrderNumber,
value = order.OrderTotal,
tax = order.OrderTax,
shipping = order.OrderShippingInclTax,
currency = order.CustomerCurrencyCode,
coupon = order.DiscountUsageHistory.FirstOrDefault()?.Discount?.CouponCode ?? "",
items = items.ToArray()
};
// Store for rendering
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext?.Items != null)
{
httpContext.Items["GA4PurchaseEvent"] = System.Text.Json.JsonSerializer.Serialize(purchaseData);
}
}
}
Refund Tracking
C# Implementation for Refunds
// Event consumer for order refunds
public class OrderRefundedEventConsumer : IConsumer<OrderRefundedEvent>
{
private readonly IOrderService _orderService;
private readonly IProductService _productService;
private readonly ILogger _logger;
public async Task HandleEventAsync(OrderRefundedEvent eventMessage)
{
var order = eventMessage.Order;
var orderItems = await _orderService.GetOrderItemsAsync(order.Id);
// Use Measurement Protocol for server-side tracking
var items = new List<object>();
foreach (var item in orderItems)
{
var product = await _productService.GetProductByIdAsync(item.ProductId);
items.Add(new
{
item_id = product.Id.ToString(),
item_name = product.Name,
price = item.UnitPriceInclTax,
quantity = item.Quantity
});
}
var refundData = new
{
transaction_id = order.CustomOrderNumber,
value = eventMessage.Amount,
currency = order.CustomerCurrencyCode,
items = items.ToArray()
};
// Send to GA4 via Measurement Protocol
await SendRefundToGA4Async(refundData);
}
private async Task SendRefundToGA4Async(object refundData)
{
// Implementation using GA4 Measurement Protocol
// See Google's documentation for server-side tracking
}
}
Product Variants and Customization
Tracking Product Variants
// Track product variant selection
function trackVariantChange(productId, variantName, attributeName, attributeValue) {
gtag('event', 'view_item_variant', {
'item_id': productId,
'variant_name': variantName,
'attribute_name': attributeName,
'attribute_value': attributeValue
});
}
// Example: Color selection
document.querySelectorAll('.product-attribute select, .product-attribute input[type="radio"]').forEach(function(input) {
input.addEventListener('change', function() {
var attributeName = this.closest('.product-attribute').dataset.attributeName;
var attributeValue = this.value || this.nextElementSibling?.textContent;
trackVariantChange(
'@Model.Id',
'@Model.Name',
attributeName,
attributeValue
);
});
});
Enhanced Ecommerce Reporting
Custom Dimensions for NopCommerce
gtag('config', 'G-XXXXXXXXXX', {
'custom_map': {
'dimension1': 'customer_group',
'dimension2': 'vendor_id',
'dimension3': 'store_id',
'dimension4': 'product_availability',
'dimension5': 'discount_applied'
}
});
// Set dimensions on purchase
gtag('event', 'purchase', {
'transaction_id': '@order.CustomOrderNumber',
'value': @order.OrderTotal,
'currency': '@order.CustomerCurrencyCode',
'customer_group': '@customerRole.Name',
'vendor_id': '@vendorId',
'store_id': '@storeId',
'discount_applied': '@(order.OrderDiscount > 0 ? "yes" : "no")',
'items': [...] // order items
});
Testing Ecommerce Tracking
Use GA4 DebugView
Enable debug mode:
gtag('config', 'G-XXXXXXXXXX', { 'debug_mode': true });Complete test purchase:
- Add product to cart
- Proceed to checkout
- Select shipping/payment
- Complete order
Verify in DebugView:
- All events firing in correct sequence
- All parameters present
- Item arrays properly formatted
- Transaction ID unique
Console Testing
// Test event structure in console
var testPurchase = {
transaction_id: 'TEST-12345',
value: 99.99,
currency: 'USD',
items: [{
item_id: '123',
item_name: 'Test Product',
price: 99.99,
quantity: 1
}]
};
gtag('event', 'purchase', testPurchase);
Common Issues and Solutions
Issue: Purchase Events Not Firing
Check:
- Order confirmation page loads completely
- No JavaScript errors blocking execution
- Correct transaction ID format
- Customer ID available in session
Issue: Duplicate Purchase Events
Solution:
// Prevent duplicate purchase tracking
// Store transaction ID in session
var sessionKey = $"GA4_Purchase_Tracked_{order.Id}";
if (HttpContext.Session.GetString(sessionKey) == null)
{
// Track purchase
HttpContext.Session.SetString(sessionKey, "true");
}
Issue: Items Not Showing in Reports
Check:
- Item ID is string, not integer
- All required parameters present: item_id, item_name
- Items array properly formatted
- Currency code is valid ISO format
Next Steps
- GA4 Event Tracking - Track custom events
- GTM Data Layer - Advanced ecommerce with GTM
- Events Not Firing - Troubleshoot tracking issues