GA4 Ecommerce Tracking on Episerver Commerce | OpsBlu Docs

GA4 Ecommerce Tracking on Episerver Commerce

Complete guide to implementing GA4 ecommerce tracking for Episerver Commerce (Optimizely)

Implement comprehensive GA4 ecommerce tracking for Episerver Commerce (now Optimizely Commerce) including products, cart, checkout, and purchases.

Prerequisites

  • Episerver Commerce 13+ or Commerce 14+
  • GA4 Setup completed
  • Access to product templates, cart, and checkout pages
  • Understanding of Episerver Commerce catalog structure

Ecommerce Data Layer Structure

Base Helper Class

Create a helper class to convert Episerver Commerce objects to GA4 format.

using EPiServer.Commerce.Catalog.ContentTypes;
using EPiServer.Commerce.Order;
using Mediachase.Commerce;
using Mediachase.Commerce.Catalog;

public class GA4EcommerceHelper
{
    private readonly IContentLoader _contentLoader;
    private readonly ReferenceConverter _referenceConverter;
    private readonly ICurrentMarket _currentMarket;

    public GA4EcommerceHelper(
        IContentLoader contentLoader,
        ReferenceConverter referenceConverter,
        ICurrentMarket currentMarket)
    {
        _contentLoader = contentLoader;
        _referenceConverter = referenceConverter;
        _currentMarket = currentMarket;
    }

    public object GetGA4Item(EntryContentBase entry, decimal quantity = 1, int index = 0)
    {
        var variant = entry as VariationContent;
        var product = variant != null
            ? _contentLoader.Get<ProductContent>(variant.GetParentProducts().FirstOrDefault())
            : entry as ProductContent;

        var price = entry.GetDefaultPrice()?.UnitPrice.Amount ?? 0;

        return new
        {
            item_id = entry.Code,
            item_name = entry.DisplayName,
            affiliation = _currentMarket.GetCurrentMarket().MarketName,
            item_brand = product?.Brand,
            item_category = GetCategoryPath(entry),
            item_variant = variant?.VariantName,
            price = price,
            quantity = quantity,
            index = index
        };
    }

    public object GetGA4Item(ILineItem lineItem, int index = 0)
    {
        var entry = _contentLoader.Get<EntryContentBase>(
            _referenceConverter.GetContentLink(lineItem.Code));

        var variant = entry as VariationContent;
        var product = variant != null
            ? _contentLoader.Get<ProductContent>(variant.GetParentProducts().FirstOrDefault())
            : entry as ProductContent;

        return new
        {
            item_id = lineItem.Code,
            item_name = lineItem.DisplayName,
            affiliation = lineItem.OrderGroupId > 0 ? "Store" : "Online",
            item_brand = product?.Brand,
            item_category = GetCategoryPath(entry),
            item_variant = variant?.VariantName,
            price = lineItem.PlacedPrice,
            quantity = lineItem.Quantity,
            discount = lineItem.GetDiscountTotal().Amount,
            index = index
        };
    }

    private string GetCategoryPath(EntryContentBase entry)
    {
        var category = entry.GetCategories()
            .Select(c => _contentLoader.Get<NodeContent>(c))
            .FirstOrDefault();

        if (category == null) return string.Empty;

        var path = new List<string>();
        var current = category;

        while (current != null && !(current is CatalogContent))
        {
            path.Insert(0, current.DisplayName);
            var parent = _contentLoader.Get<IContent>(current.ParentLink);
            current = parent as NodeContent;
        }

        return string.Join("/", path);
    }
}

View Model Extension

Create view model extensions for easy template usage.

public static class GA4ViewModelExtensions
{
    public static string ToGA4Json(this object obj)
    {
        return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            NullValueHandling = NullValueHandling.Ignore
        });
    }

    public static string GetGA4ItemsJson(this IEnumerable<ILineItem> lineItems, GA4EcommerceHelper helper)
    {
        var items = lineItems.Select((item, index) => helper.GetGA4Item(item, index)).ToList();
        return items.ToGA4Json();
    }
}

Product Views

Track when users view product detail pages.

Product Template

@using EPiServer.Commerce.Catalog.ContentTypes
@using Newtonsoft.Json

@model ProductContent

@{
    var variant = Model.GetVariants()
        .Select(v => _contentLoader.Get<VariationContent>(v))
        .FirstOrDefault();

    var price = variant?.GetDefaultPrice()?.UnitPrice.Amount ?? 0;
    var ga4Helper = ServiceLocator.Current.GetInstance<GA4EcommerceHelper>();
    var item = ga4Helper.GetGA4Item(variant ?? Model as EntryContentBase);
}

<script>
  // Track product view
  gtag('event', 'view_item', {
    currency: '@_currentMarket.GetCurrentMarket().DefaultCurrency.CurrencyCode',
    value: @price,
    items: [@Html.Raw(item.ToGA4Json())]
  });
</script>

<!-- Product details markup -->
<div class="product-detail">
    <h1>@Model.DisplayName</h1>
    <p class="price">@price.ToString("C")</p>

    @if (variant != null)
    {
        <button class="add-to-cart"
                data-code="@variant.Code" '@variant.DisplayName', @price)">
            Add to Cart
        </button>
    }
</div>

Product List Views

Track product impressions on category, search, and listing pages.

Category Page Template

@model CategoryPageType

@{
    var products = Model.GetProducts();
    var ga4Helper = ServiceLocator.Current.GetInstance<GA4EcommerceHelper>();
    var items = products.Select((p, index) => ga4Helper.GetGA4Item(p, 1, index + 1)).ToList();
}

<script>
  // Track product list view
  gtag('event', 'view_item_list', {
    item_list_id: '@Model.ContentLink.ID',
    item_list_name: '@Model.DisplayName',
    items: @Html.Raw(items.ToGA4Json())
  });
</script>

<div class="product-grid">
    @foreach (var product in products.Select((p, index) => new { Product = p, Index = index }))
    {
        <div class="product-card" '@Model.DisplayName', @product.Index)">
            <img src="@product.Product.ThumbnailUrl" alt="@product.Product.DisplayName" />
            <h3>@product.Product.DisplayName</h3>
            <p class="price">@product.Product.GetDefaultPrice()?.UnitPrice.Amount.ToString("C")</p>
        </div>
    }
</div>

<script>
  function selectProduct(itemId, listName, index) {
    gtag('event', 'select_item', {
      item_list_id: '@Model.ContentLink.ID',
      item_list_name: listName,
      items: [{
        item_id: itemId,
        index: index
      }]
    });
  }
</script>

Add to Cart

Track when products are added to the cart.

Client-Side Implementation

// /Scripts/ga4-commerce.js
window.EpiserverCommerce = window.EpiserverCommerce || {};

EpiserverCommerce.addToCart = function(code, name, price, quantity) {
  quantity = quantity || 1;

  // Call your cart API
  fetch('/api/cart/add', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      code: code,
      quantity: quantity
    })
  })
  .then(response => response.json())
  .then(data => {
    if (data.success) {
      // Track add to cart
      gtag('event', 'add_to_cart', {
        currency: data.currency || 'USD',
        value: price * quantity,
        items: [{
          item_id: code,
          item_name: name,
          price: price,
          quantity: quantity
        }]
      });

      // Update cart UI
      updateCartCount(data.cartItemCount);
    }
  })
  .catch(error => {
    console.error('Error adding to cart:', error);
  });
};

Server-Side Controller

public class CartApiController : ApiController
{
    private readonly IOrderRepository _orderRepository;
    private readonly IOrderGroupFactory _orderGroupFactory;
    private readonly IContentLoader _contentLoader;
    private readonly ReferenceConverter _referenceConverter;

    [HttpPost]
    [Route("api/cart/add")]
    public IHttpActionResult AddToCart([FromBody] AddToCartRequest request)
    {
        var cart = _orderRepository.LoadOrCreateCart<ICart>(
            CustomerContext.Current.CurrentContactId,
            "Default");

        var contentLink = _referenceConverter.GetContentLink(request.Code);
        var entry = _contentLoader.Get<EntryContentBase>(contentLink);

        if (entry == null)
        {
            return BadRequest("Product not found");
        }

        var lineItem = cart.CreateLineItem(request.Code, _orderGroupFactory);
        lineItem.Quantity = request.Quantity;

        cart.AddLineItem(lineItem);

        _orderRepository.Save(cart);

        return Ok(new
        {
            success = true,
            cartItemCount = cart.GetAllLineItems().Sum(li => li.Quantity),
            currency = cart.Currency.CurrencyCode
        });
    }
}

public class AddToCartRequest
{
    public string Code { get; set; }
    public decimal Quantity { get; set; }
}

Remove from Cart

Track cart removals.

EpiserverCommerce.removeFromCart = function(code, name, price, quantity) {
  fetch('/api/cart/remove', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ code: code })
  })
  .then(response => response.json())
  .then(data => {
    if (data.success) {
      gtag('event', 'remove_from_cart', {
        currency: data.currency || 'USD',
        value: price * quantity,
        items: [{
          item_id: code,
          item_name: name,
          price: price,
          quantity: quantity
        }]
      });
    }
  });
};

View Cart

Track when users view their shopping cart.

Cart Page Template

@model CartPageType

@{
    var cart = _orderRepository.LoadCart<ICart>(
        CustomerContext.Current.CurrentContactId,
        "Default");

    var ga4Helper = ServiceLocator.Current.GetInstance<GA4EcommerceHelper>();
    var items = cart.GetAllLineItems().GetGA4ItemsJson(ga4Helper);
}

<script>
  gtag('event', 'view_cart', {
    currency: '@cart.Currency.CurrencyCode',
    value: @cart.GetTotal().Amount,
    items: @Html.Raw(items)
  });
</script>

<div class="cart">
    <h1>Shopping Cart</h1>

    @foreach (var lineItem in cart.GetAllLineItems())
    {
        <div class="cart-item">
            <span>@lineItem.DisplayName</span>
            <span>@lineItem.Quantity</span>
            <span>@lineItem.PlacedPrice.ToString("C")</span>
            <button
                '@lineItem.Code',
                '@lineItem.DisplayName',
                @lineItem.PlacedPrice,
                @lineItem.Quantity)">
                Remove
            </button>
        </div>
    }

    <div class="cart-total">
        <strong>Total: @cart.GetTotal().Amount.ToString("C")</strong>
    </div>

    <a href="@Url.ContentUrl(Model.CheckoutPage)" class="btn-checkout">
        Proceed to Checkout
    </a>
</div>

Begin Checkout

Track when users start the checkout process.

@model CheckoutPageType

@{
    var cart = _orderRepository.LoadCart<ICart>(
        CustomerContext.Current.CurrentContactId,
        "Default");

    var ga4Helper = ServiceLocator.Current.GetInstance<GA4EcommerceHelper>();
    var items = cart.GetAllLineItems().GetGA4ItemsJson(ga4Helper);
}

<script>
  gtag('event', 'begin_checkout', {
    currency: '@cart.Currency.CurrencyCode',
    value: @cart.GetTotal().Amount,
    items: @Html.Raw(items)
  });
</script>

Add Shipping Info

Track shipping information step.

EpiserverCommerce.addShippingInfo = function(cartData) {
  gtag('event', 'add_shipping_info', {
    currency: cartData.currency,
    value: cartData.total,
    shipping_tier: cartData.shippingMethod,
    items: cartData.items
  });
};

// Call when shipping method is selected
document.querySelector('#shipping-method').addEventListener('change', function(e) {
  var cartData = getCartData(); // Your function to get cart data
  cartData.shippingMethod = e.target.value;
  EpiserverCommerce.addShippingInfo(cartData);
});

Add Payment Info

Track payment information step.

EpiserverCommerce.addPaymentInfo = function(cartData, paymentMethod) {
  gtag('event', 'add_payment_info', {
    currency: cartData.currency,
    value: cartData.total,
    payment_type: paymentMethod,
    items: cartData.items
  });
};

// Call when payment method is selected
document.querySelector('#payment-method').addEventListener('change', function(e) {
  var cartData = getCartData();
  EpiserverCommerce.addPaymentInfo(cartData, e.target.value);
});

Purchase Tracking

Track completed purchases on the order confirmation page.

Order Confirmation Page

@model OrderConfirmationPageType

@{
    var orderId = RouteData.Values["orderNumber"]?.ToString();

    if (!string.IsNullOrEmpty(orderId))
    {
        var order = _orderRepository.Load<IPurchaseOrder>(orderId);

        if (order != null)
        {
            var ga4Helper = ServiceLocator.Current.GetInstance<GA4EcommerceHelper>();
            var items = order.GetAllLineItems().GetGA4ItemsJson(ga4Helper);

            var shipping = order.GetFirstShipment()?.ShippingSubTotal.Amount ?? 0;
            var tax = order.TaxTotal.Amount;
            var discount = order.GetTotal().Amount - order.SubTotal.Amount - shipping - tax;
        }
    }
}

@if (order != null)
{
    <script>
      gtag('event', 'purchase', {
        transaction_id: '@order.OrderNumber',
        affiliation: '@order.MarketName',
        value: @order.GetTotal().Amount,
        tax: @tax,
        shipping: @shipping,
        currency: '@order.Currency.CurrencyCode',
        coupon: '@(order.GetFirstForm()?.CouponCodes.FirstOrDefault())',
        items: @Html.Raw(items)
      });
    </script>

    <div class="order-confirmation">
        <h1>Thank You for Your Order!</h1>
        <p>Order Number: <strong>@order.OrderNumber</strong></p>
        <p>Total: <strong>@order.GetTotal().Amount.ToString("C")</strong></p>

        <div class="order-items">
            @foreach (var lineItem in order.GetAllLineItems())
            {
                <div class="order-item">
                    <span>@lineItem.DisplayName</span>
                    <span>Qty: @lineItem.Quantity</span>
                    <span>@lineItem.PlacedPrice.ToString("C")</span>
                </div>
            }
        </div>
    </div>
}

Prevent Duplicate Purchase Events

Store tracked orders to prevent duplicate events on page refresh.

public class PurchaseTrackingFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var orderId = filterContext.RouteData.Values["orderNumber"]?.ToString();

        if (!string.IsNullOrEmpty(orderId))
        {
            var cookieName = $"ga4_tracked_{orderId}";
            var cookie = filterContext.HttpContext.Request.Cookies[cookieName];

            if (cookie != null)
            {
                // Already tracked, don't track again
                filterContext.HttpContext.Items["OrderAlreadyTracked"] = true;
            }
            else
            {
                // Set cookie to prevent duplicate tracking
                filterContext.HttpContext.Response.Cookies.Add(new HttpCookie(cookieName, "1")
                {
                    Expires = DateTime.Now.AddDays(1)
                });
            }
        }
    }
}

Apply to controller:

[PurchaseTrackingFilter]
public ActionResult OrderConfirmation(string orderNumber)
{
    // Your code
}

Update template:

@if (order != null && HttpContext.Items["OrderAlreadyTracked"] == null)
{
    <script>
      gtag('event', 'purchase', {
        // ... purchase data
      });
    </script>
}

Refund Tracking

Track refunds (typically done server-side or via backend process).

public class OrderEventHandler
{
    public void OnOrderRefunded(IPurchaseOrder order, decimal refundAmount)
    {
        // Log refund for manual import to GA4
        // or use Measurement Protocol API

        var refundData = new
        {
            transaction_id = order.OrderNumber,
            value = refundAmount,
            currency = order.Currency.CurrencyCode
        };

        // Option 1: Log to file for batch import
        LogRefund(refundData);

        // Option 2: Send via Measurement Protocol
        SendRefundToGA4(refundData);
    }

    private void SendRefundToGA4(object refundData)
    {
        // Use GA4 Measurement Protocol
        // https://developers.google.com/analytics/devguides/collection/protocol/ga4
    }
}

Product Impressions in Blocks

Track product impressions in recommendation blocks or featured products.

@model ProductRecommendationBlock

@{
    var products = Model.RecommendedProducts;
    var ga4Helper = ServiceLocator.Current.GetInstance<GA4EcommerceHelper>();
    var items = products.Select((p, index) =>
    {
        var entry = _contentLoader.Get<EntryContentBase>(p);
        return ga4Helper.GetGA4Item(entry, 1, index + 1);
    }).ToList();
}

<script>
  gtag('event', 'view_item_list', {
    item_list_id: 'recommendations_@Model.ContentLink.ID',
    item_list_name: '@Model.Name',
    items: @Html.Raw(items.ToGA4Json())
  });
</script>

<div class="product-recommendations">
    @foreach (var productRef in products.Select((p, index) => new { Ref = p, Index = index }))
    {
        var product = _contentLoader.Get<EntryContentBase>(productRef.Ref);

        <div class="product-card" @productRef.Index)">
            <img src="@product.ThumbnailUrl" alt="@product.DisplayName" />
            <h4>@product.DisplayName</h4>
        </div>
    }
</div>

<script>
  function selectRecommendation(itemId, index) {
    gtag('event', 'select_item', {
      item_list_id: 'recommendations_@Model.ContentLink.ID',
      item_list_name: '@Model.Name',
      items: [{
        item_id: itemId,
        index: index
      }]
    });
  }
</script>

Promotions

Track promotion views and selections.

@{
    var promotion = Model.ActivePromotion;
}

<script>
  // View promotion
  gtag('event', 'view_promotion', {
    creative_name: '@promotion.Name',
    creative_slot: 'banner_top',
    promotion_id: '@promotion.Id',
    promotion_name: '@promotion.Name'
  });
</script>

<div class="promotion-banner" '@promotion.Name')">
    @promotion.Content
</div>

<script>
  function selectPromotion(promoId, promoName) {
    gtag('event', 'select_promotion', {
      creative_name: promoName,
      creative_slot: 'banner_top',
      promotion_id: promoId,
      promotion_name: promoName
    });
  }
</script>

Testing Ecommerce Tracking

1. GA4 DebugView

Enable debug mode and test each event:

  • Product views
  • Add to cart
  • Cart view
  • Checkout steps
  • Purchase

2. Browser Console

// Check data layer
console.log(window.dataLayer);

// Manually trigger test purchase
gtag('event', 'purchase', {
  transaction_id: 'TEST_' + Date.now(),
  value: 99.99,
  currency: 'USD',
  items: [{
    item_id: 'TEST_SKU',
    item_name: 'Test Product',
    price: 99.99,
    quantity: 1
  }]
});

3. Network Tab

Monitor collect endpoint calls and verify ecommerce parameters.

Best Practices

  1. Test Mode: Create a separate GA4 property for testing
  2. Currency: Always include currency code
  3. Product IDs: Use consistent product codes/SKUs
  4. Categories: Include full category path when available
  5. Variants: Track product variants separately
  6. Refunds: Implement refund tracking for accurate revenue
  7. Duplicate Prevention: Prevent duplicate purchase events
  8. Privacy: Respect user consent for ecommerce tracking

Next Steps