Implement GA4 e-commerce tracking for Umbraco using Vendr (Umbraco 8-10), Umbraco Commerce (Umbraco 11+), or custom .NET shopping cart solutions. This guide covers transaction tracking, product impressions, and revenue reporting.
Prerequisites
Before implementing e-commerce tracking:
- GA4 Setup Complete - Follow GA4 Setup Guide
- E-commerce Platform - Vendr, Umbraco Commerce, or custom solution
- Commerce Package Installed - NuGet package configured
- Test Orders - Test environment for validation
Vendr E-commerce Tracking (Umbraco 8-10)
Install Vendr Analytics Package
Install-Package Vendr.Contrib.GoogleAnalytics
Or via .NET CLI:
dotnet add package Vendr.Contrib.GoogleAnalytics
Configure Vendr Analytics
Register in Startup.cs:
using Vendr.Contrib.GoogleAnalytics.Extensions;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddUmbraco(_env, _config)
.AddBackOffice()
.AddWebsite()
.AddVendr()
.AddVendrGoogleAnalytics(options => {
options.MeasurementId = "G-XXXXXXXXXX";
options.TrackProductImpressions = true;
options.TrackProductClicks = true;
options.TrackAddToCart = true;
options.TrackRemoveFromCart = true;
options.TrackCheckout = true;
options.TrackPurchase = true;
})
.AddComposers()
.Build();
}
}
Track Product Impressions
ProductListing.cshtml:
@using Vendr.Core.Models
@using Vendr.Core.Api
@inject IVendrApi VendrApi
@{
var store = VendrApi.GetStore(Model.Value<Guid>("store"));
var products = Model.Children().Where(x => x.ContentType.Alias == "product");
}
<div class="product-listing">
@foreach (var product in products)
{
var productName = product.Name;
var productId = product.GetProperty("sku")?.Value<string>();
var price = product.GetProperty("price")?.Value<decimal>() ?? 0;
<div class="product-item"
data-product-id="@productId"
data-product-name="@productName"
data-product-price="@price">
<h3>@productName</h3>
<p>@price.ToString("C")</p>
<a href="@product.Url()" class="view-product">View Product</a>
</div>
}
</div>
<script>
// Track product impressions
var products = document.querySelectorAll('.product-item');
var items = [];
products.forEach(function(product, index) {
items.push({
item_id: product.getAttribute('data-product-id'),
item_name: product.getAttribute('data-product-name'),
price: parseFloat(product.getAttribute('data-product-price')),
index: index,
item_category: '@Model.Name'
});
});
gtag('event', 'view_item_list', {
item_list_id: '@Model.Id',
item_list_name: '@Model.Name',
items: items
});
// Track product clicks
products.forEach(function(product) {
product.querySelector('.view-product').addEventListener('click', function(e) {
gtag('event', 'select_item', {
item_list_id: '@Model.Id',
item_list_name: '@Model.Name',
items: [{
item_id: product.getAttribute('data-product-id'),
item_name: product.getAttribute('data-product-name'),
price: parseFloat(product.getAttribute('data-product-price'))
}]
});
});
});
</script>
Track Product Detail Views
ProductDetail.cshtml:
@using Vendr.Core.Models
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
@{
var productSku = Model.GetProperty("sku")?.Value<string>();
var productPrice = Model.GetProperty("price")?.Value<decimal>() ?? 0;
var productCategory = Model.Parent?.Name;
}
<script>
gtag('event', 'view_item', {
currency: 'USD',
value: @productPrice,
items: [{
item_id: '@productSku',
item_name: '@Model.Name',
price: @productPrice,
item_category: '@productCategory',
quantity: 1
}]
});
</script>
Track Add to Cart
Add to Cart Razor:
@using Umbraco.Forms.Core
@model Vendr.Core.Models.ProductSnapshot
<form method="post" id="add-to-cart-form">
<input type="hidden" name="productId" value="@Model.ProductReference" />
<input type="number" name="quantity" value="1" min="1" />
<button type="submit">Add to Cart</button>
</form>
<script>
document.getElementById('add-to-cart-form').addEventListener('submit', function(e) {
e.preventDefault();
var quantity = parseInt(this.querySelector('[name="quantity"]').value);
// Track add to cart
gtag('event', 'add_to_cart', {
currency: '@Model.TotalPrice.Value.CurrencyCode',
value: @Model.TotalPrice.Value.Value,
items: [{
item_id: '@Model.Sku',
item_name: '@Model.Name',
price: @Model.TotalPrice.Value.Value,
quantity: quantity
}]
});
// Submit form
this.submit();
});
</script>
Track Checkout Process
Checkout.cshtml:
@using Vendr.Core.Models
@using Vendr.Core.Api
@inject IVendrApi VendrApi
@{
var order = VendrApi.GetCurrentOrder(Model.Value<Guid>("store"));
}
@if (order != null)
{
<script>
var items = [];
@foreach (var orderLine in order.OrderLines)
{
<text>
items.push({
item_id: '@orderLine.Sku',
item_name: '@orderLine.Name',
price: @orderLine.TotalPrice.Value.WithoutAdjustments.Value,
quantity: @orderLine.Quantity
});
</text>
}
// Track begin checkout
gtag('event', 'begin_checkout', {
currency: '@order.CurrencyCode',
value: @order.TotalPrice.Value.WithoutAdjustments.Value,
items: items
});
</script>
}
Checkout Steps:
@{
var currentStep = ViewData["CheckoutStep"]?.ToString() ?? "shipping";
}
<script>
// Track checkout progress
gtag('event', 'checkout_progress', {
checkout_step: '@currentStep',
checkout_option: '@ViewData["CheckoutOption"]'
});
</script>
Track Purchase Completion
OrderConfirmation.cshtml:
@using Vendr.Core.Models
@using Vendr.Core.Api
@inject IVendrApi VendrApi
@{
var orderRef = Request.Query["order"];
var order = VendrApi.GetOrder(Guid.Parse(orderRef));
}
@if (order != null)
{
<script>
var items = [];
@foreach (var orderLine in order.OrderLines)
{
<text>
items.push({
item_id: '@orderLine.Sku',
item_name: '@orderLine.Name',
price: @orderLine.TotalPrice.Value.WithoutAdjustments.Value,
quantity: @orderLine.Quantity,
item_category: '@orderLine.Properties.GetValue("category")'
});
</text>
}
// Track purchase
gtag('event', 'purchase', {
transaction_id: '@order.OrderNumber',
affiliation: '@order.Properties.GetValue("storeName")',
value: @order.TotalPrice.Value.WithoutAdjustments.Value,
tax: @order.TotalPrice.Value.TotalAdjustment.Value,
shipping: @order.ShippingInfo.TotalPrice.Value.WithoutAdjustments.Value,
currency: '@order.CurrencyCode',
coupon: '@order.DiscountCodes.FirstOrDefault()?.Code',
items: items
});
</script>
<h1>Order Confirmed</h1>
<p>Order Number: @order.OrderNumber</p>
<p>Total: @order.TotalPrice.Value.Formatted()</p>
}
Umbraco Commerce Tracking (Umbraco 11+)
Install Umbraco Commerce Analytics
dotnet add package Umbraco.Commerce.GoogleAnalytics
Configure Umbraco Commerce Analytics
appsettings.json:
{
"Umbraco": {
"Commerce": {
"GoogleAnalytics": {
"Enabled": true,
"MeasurementId": "G-XXXXXXXXXX",
"TrackServerSide": false,
"Events": {
"ViewItem": true,
"AddToCart": true,
"RemoveFromCart": true,
"BeginCheckout": true,
"Purchase": true
}
}
}
}
}
Track with Umbraco Commerce Services
Create Commerce Analytics Service:
using Umbraco.Commerce.Core.Api;
using Umbraco.Commerce.Core.Models;
namespace YourProject.Services
{
public class CommerceAnalyticsService
{
private readonly IUmbracoCommerceApi _commerceApi;
private readonly IAnalyticsService _analyticsService;
public CommerceAnalyticsService(
IUmbracoCommerceApi commerceApi,
IAnalyticsService analyticsService)
{
_commerceApi = commerceApi;
_analyticsService = analyticsService;
}
public async Task TrackPurchase(Guid orderId)
{
var order = _commerceApi.GetOrder(orderId);
if (order == null) return;
var items = order.OrderLines.Select(line => new Dictionary<string, object>
{
{ "item_id", line.Sku },
{ "item_name", line.Name },
{ "price", line.UnitPrice.Value.Value },
{ "quantity", line.Quantity }
}).ToList();
await _analyticsService.TrackEvent("purchase", new Dictionary<string, object>
{
{ "transaction_id", order.OrderNumber },
{ "value", order.SubtotalPrice.Value.Value },
{ "tax", order.TotalPrice.Value.TotalAdjustment.Value },
{ "shipping", order.ShippingInfo?.TotalPrice.Value.Value ?? 0 },
{ "currency", order.CurrencyCode },
{ "items", items }
});
}
}
}
Track Order Events
OrderEventHandler.cs:
using Umbraco.Commerce.Core.Events.Notification;
using Umbraco.Cms.Core.Events;
namespace YourProject.EventHandlers
{
public class OrderEventHandler :
INotificationHandler<OrderFinalizedNotification>
{
private readonly CommerceAnalyticsService _analyticsService;
public OrderEventHandler(CommerceAnalyticsService analyticsService)
{
_analyticsService = analyticsService;
}
public async Task Handle(OrderFinalizedNotification notification, CancellationToken cancellationToken)
{
var order = notification.Order;
await _analyticsService.TrackPurchase(order.Id);
}
}
}
Custom .NET Shopping Cart Integration
Create Shopping Cart Model
namespace YourProject.Models
{
public class CartItem
{
public string Sku { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
public string Category { get; set; }
}
public class ShoppingCart
{
public List<CartItem> Items { get; set; } = new();
public decimal Subtotal => Items.Sum(i => i.Price * i.Quantity);
public decimal Tax { get; set; }
public decimal Shipping { get; set; }
public decimal Total => Subtotal + Tax + Shipping;
public string CurrencyCode { get; set; } = "USD";
}
}
Track Cart Events
CartController.cs:
using Microsoft.AspNetCore.Mvc;
using YourProject.Models;
using YourProject.Services;
namespace YourProject.Controllers
{
public class CartController : SurfaceController
{
private readonly IAnalyticsService _analyticsService;
public CartController(IAnalyticsService analyticsService, /* other dependencies */)
{
_analyticsService = analyticsService;
}
[HttpPost]
public async Task<IActionResult> AddToCart(string sku, string name, decimal price, int quantity)
{
// Add to cart logic
// ...
// Track add to cart
await _analyticsService.TrackEvent("add_to_cart", new Dictionary<string, object>
{
{ "currency", "USD" },
{ "value", price * quantity },
{
"items", new[]
{
new Dictionary<string, object>
{
{ "item_id", sku },
{ "item_name", name },
{ "price", price },
{ "quantity", quantity }
}
}
}
});
return Json(new { success = true });
}
[HttpPost]
public async Task<IActionResult> RemoveFromCart(string sku)
{
// Get item before removal
var item = GetCartItem(sku);
// Remove from cart logic
// ...
// Track removal
await _analyticsService.TrackEvent("remove_from_cart", new Dictionary<string, object>
{
{ "currency", "USD" },
{ "value", item.Price * item.Quantity },
{
"items", new[]
{
new Dictionary<string, object>
{
{ "item_id", item.Sku },
{ "item_name", item.Name },
{ "price", item.Price },
{ "quantity", item.Quantity }
}
}
}
});
return Json(new { success = true });
}
}
}
Server-Side E-commerce Tracking
Use GA4 Measurement Protocol
EnhancedAnalyticsService.cs:
using System.Net.Http;
using System.Text;
using System.Text.Json;
namespace YourProject.Services
{
public class EnhancedAnalyticsService : IAnalyticsService
{
private readonly HttpClient _httpClient;
private readonly IConfiguration _configuration;
public EnhancedAnalyticsService(HttpClient httpClient, IConfiguration configuration)
{
_httpClient = httpClient;
_configuration = configuration;
}
public async Task TrackPurchase(Order order, string clientId)
{
var measurementId = _configuration["Analytics:GoogleAnalytics:MeasurementId"];
var apiSecret = _configuration["Analytics:GoogleAnalytics:ApiSecret"];
var items = order.Items.Select(item => new
{
item_id = item.Sku,
item_name = item.Name,
price = item.Price,
quantity = item.Quantity,
item_category = item.Category
}).ToArray();
var payload = new
{
client_id = clientId,
events = new[]
{
new
{
name = "purchase",
@params = new
{
transaction_id = order.OrderNumber,
value = order.Total,
tax = order.Tax,
shipping = order.Shipping,
currency = order.CurrencyCode,
items = items
}
}
}
};
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var url = $"https://www.google-analytics.com/mp/collect?measurement_id={measurementId}&api_secret={apiSecret}";
var response = await _httpClient.PostAsync(url, content);
response.EnsureSuccessStatusCode();
}
}
}
Enhanced E-commerce Tracking
Track Refunds
public async Task TrackRefund(string transactionId, decimal refundAmount, List<RefundItem> items = null)
{
var eventParams = new Dictionary<string, object>
{
{ "transaction_id", transactionId },
{ "value", refundAmount },
{ "currency", "USD" }
};
if (items != null && items.Any())
{
eventParams["items"] = items.Select(item => new Dictionary<string, object>
{
{ "item_id", item.Sku },
{ "quantity", item.Quantity }
}).ToList();
}
await _analyticsService.TrackEvent("refund", eventParams);
}
Track Promotions
@{
var promotion = Model.Value<IPublishedContent>("activePromotion");
}
@if (promotion != null)
{
<div class="promotion-banner"
data-promotion-id="@promotion.GetProperty("code")?.Value<string>()"
data-promotion-name="@promotion.Name">
<p>@promotion.GetProperty("description")?.Value<string>()</p>
</div>
<script>
gtag('event', 'view_promotion', {
creative_name: '@promotion.Name',
creative_slot: 'banner',
promotion_id: '@promotion.GetProperty("code")?.Value<string>()',
promotion_name: '@promotion.Name'
});
document.querySelector('.promotion-banner').addEventListener('click', function() {
gtag('event', 'select_promotion', {
creative_name: '@promotion.Name',
creative_slot: 'banner',
promotion_id: '@promotion.GetProperty("code")?.Value<string>()',
promotion_name: '@promotion.Name'
});
});
</script>
}
Testing E-commerce Tracking
Enable E-commerce Reports in GA4
- Navigate to Google Analytics → Admin
- Select Property → Data Streams → Web
- Click Enhanced Measurement
- Ensure File Downloads and Page Views enabled
- Navigate to Events → Create custom event if needed
Test Purchase Tracking
- Place test order on your Umbraco site
- Complete checkout process
- Navigate to Google Analytics → Realtime → Events
- Verify
purchaseevent appears - Check Monetization → E-commerce Purchases report (24-48 hours)
Validate Event Parameters
In GA4 DebugView:
- Enable Debug Mode in configuration
- Navigate to Google Analytics → Configure → DebugView
- Perform purchase
- Verify all parameters present:
transaction_idvaluecurrencyitemsarraytax,shipping
Common Issues
Duplicate Transactions
Cause: Page refresh on confirmation page Solution:
@{
// Only track once using session
if (Session["OrderTracked_@order.OrderNumber"] == null)
{
Session["OrderTracked_@order.OrderNumber"] = true;
<script>
gtag('event', 'purchase', { /* ... */ });
</script>
}
}
Missing Transaction Data
Cause: Order not finalized when tracking fires Solution: Use server-side tracking in order finalization event handler
Currency Code Issues
Cause: Incorrect or missing currency code Solution: Always specify currency:
await _analyticsService.TrackEvent("purchase", new Dictionary<string, object>
{
{ "currency", order.Currency.IsoCode }, // Use actual currency
{ "value", order.TotalPrice.Value.Value }
});
Next Steps
- Event Tracking - Track custom events
- GTM Setup - Implement Google Tag Manager
- Troubleshooting Tracking - Debug tracking issues