GA4 Ecommerce Tracking on NopCommerce | OpsBlu Docs

GA4 Ecommerce Tracking on NopCommerce

Implement complete Google Analytics 4 ecommerce tracking on NopCommerce including product impressions, add to cart, checkout, and purchase events with...

Track the complete customer journey from product browsing to purchase completion using Google Analytics 4 enhanced ecommerce on NopCommerce.

Before You Begin

Prerequisites:

GA4 Ecommerce Events:

  • view_item - Product detail page view
  • view_item_list - Category/search results view
  • select_item - Product clicked from list
  • add_to_cart - Product added to cart
  • remove_from_cart - Product removed from cart
  • view_cart - Shopping cart viewed
  • begin_checkout - Checkout process started
  • add_shipping_info - Shipping method selected
  • add_payment_info - Payment method selected
  • purchase - 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

  1. Enable debug mode:

    gtag('config', 'G-XXXXXXXXXX', {
        'debug_mode': true
    });
    
  2. Complete test purchase:

    • Add product to cart
    • Proceed to checkout
    • Select shipping/payment
    • Complete order
  3. 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