Build a complete data layer for Google Tag Manager on NopCommerce to enable advanced ecommerce tracking, custom events, and enhanced analytics capabilities.
Before You Begin
Prerequisites:
- GTM container installed (see GTM Setup)
- Understanding of data layer structure
- Access to NopCommerce theme files or plugin development
- Basic knowledge of C# and Razor syntax
Data Layer Benefits:
- Consistent data structure across tags
- Enhanced ecommerce tracking
- Custom dimensions and metrics
- Better debugging and testing
- Reduced direct code dependencies
Data Layer Architecture
Basic Structure
dataLayer = [{
// Page information
'pageType': 'product',
'pageCategory': 'Electronics',
// User information
'userId': '12345',
'userType': 'registered',
// Store information
'storeId': '1',
'storeName': 'Main Store',
'currency': 'USD',
// Ecommerce data
'ecommerce': {
'items': [...]
}
}];
Base Data Layer Implementation
Global Data Layer (All Pages)
Create /Themes/YourTheme/Views/Shared/_DataLayer.cshtml:
@inject Nop.Core.IWorkContext workContext
@inject Nop.Core.IStoreContext storeContext
@inject Nop.Services.Customers.ICustomerService customerService
@inject Nop.Services.Directory.ICurrencyService currencyService
@{
var customer = await workContext.GetCurrentCustomerAsync();
var store = await storeContext.GetCurrentStoreAsync();
var currency = await workContext.GetWorkingCurrencyAsync();
var isAdmin = await customerService.IsAdminAsync(customer);
var isRegistered = await customerService.IsRegisteredAsync(customer);
}
<script>
window.dataLayer = window.dataLayer || [];
dataLayer.push({
// Page Context
'event': 'pageview',
'pageType': '@ViewBag.PageType',
'pageTitle': '@ViewBag.Title',
'pageUrl': '@Context.Request.Path',
// User Information
'userId': '@(isRegistered ? customer.Id.ToString() : "")',
'userType': '@(isAdmin ? "admin" : isRegistered ? "registered" : "guest")',
'customerEmail': '@(isRegistered ? customer.Email : "")',
'customerGroup': '@(customer.CustomerRoles.FirstOrDefault()?.Name ?? "Guest")',
// Store Information
'storeId': '@store.Id',
'storeName': '@store.Name',
'storeUrl': '@store.Url',
'currency': '@currency.CurrencyCode',
'language': '@(await workContext.GetWorkingLanguageAsync()).UniqueSeoCode',
// Environment
'environment': '@(System.Diagnostics.Debugger.IsAttached ? "development" : "production")'
});
</script>
Include in Layout
Add to /Themes/YourTheme/Views/Shared/_Root.cshtml:
<!DOCTYPE html>
<html>
<head>
@* GTM Head Code *@
@await Component.InvokeAsync("GoogleTagManager", new { widgetZone = "head_html_tag" })
</head>
<body>
@* GTM NoScript *@
@await Component.InvokeAsync("GoogleTagManager", new { widgetZone = "body_start_html_tag_after" })
@* Data Layer - Load before content *@
@await Html.PartialAsync("_DataLayer")
@* Page Content *@
@RenderBody()
</body>
</html>
Product Page Data Layer
Product Details View
@model ProductDetailsModel
@section DataLayer {
<script>
@{
var category = Model.Breadcrumb.CategoryBreadcrumb.LastOrDefault();
var manufacturer = Model.ProductManufacturers.FirstOrDefault();
}
dataLayer.push({
'event': 'productView',
'pageType': 'product',
'ecommerce': {
'detail': {
'actionField': {
'list': '@(category?.Name ?? "")'
},
'products': [{
'id': '@Model.Id',
'name': '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Name))',
'price': @Model.ProductPrice.PriceValue,
'brand': '@(manufacturer?.Name ?? "")',
'category': '@(category?.Name ?? "")',
'variant': '',
'quantity': 1,
'dimension1': '@Model.Sku',
'dimension2': '@(Model.StockAvailability)',
'metric1': @Model.ReviewCount,
'metric2': @(Model.ProductReviewOverview.RatingSum / (Model.ReviewCount > 0 ? Model.ReviewCount : 1))
}]
}
}
});
</script>
}
Category Page Data Layer
Category/Product List View
@model CategoryModel
@section DataLayer {
<script>
dataLayer.push({
'event': 'productImpression',
'pageType': 'category',
'categoryId': '@Model.Id',
'categoryName': '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Name))',
'ecommerce': {
'currencyCode': '@Model.Products.FirstOrDefault()?.ProductPrice.CurrencyCode',
'impressions': [
@for (int i = 0; i < Model.Products.Count; i++)
{
var product = Model.Products[i];
<text>
{
'id': '@product.Id',
'name': '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(product.Name))',
'price': @product.ProductPrice.PriceValue,
'brand': '@(product.ProductManufacturers.FirstOrDefault()?.Name ?? "")',
'category': '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Name))',
'list': 'Category Page',
'position': @(i + 1)
}@(i < Model.Products.Count - 1 ? "," : "")
</text>
}
]
}
});
</script>
}
Shopping Cart Data Layer
Cart Page View
@model ShoppingCartModel
@section DataLayer {
<script>
dataLayer.push({
'event': 'cartView',
'pageType': 'cart',
'ecommerce': {
'currencyCode': '@Model.OrderTotals.Currency',
'actionField': {
'step': 1,
'option': 'cart_view'
},
'products': [
@for (int i = 0; i < Model.Items.Count; i++)
{
var item = Model.Items[i];
<text>
{
'id': '@item.ProductId',
'name': '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(item.ProductName))',
'price': @item.UnitPrice,
'brand': '@(item.BrandName ?? "")',
'category': '',
'variant': '@item.AttributeInfo',
'quantity': @item.Quantity,
'dimension1': '@item.Sku'
}@(i < Model.Items.Count - 1 ? "," : "")
</text>
}
],
'cartTotal': @Model.OrderTotals.SubTotalValue,
'cartQuantity': @Model.Items.Sum(i => i.Quantity)
}
});
</script>
}
Add to Cart Event
// Add to cart data layer push
function pushAddToCart(productData) {
dataLayer.push({
'event': 'addToCart',
'ecommerce': {
'currencyCode': productData.currency,
'add': {
'products': [{
'id': productData.id,
'name': productData.name,
'price': productData.price,
'brand': productData.brand,
'category': productData.category,
'quantity': productData.quantity
}]
}
}
});
}
// Override NopCommerce add to cart function
var originalAddToCart = window.addProductToCart_details;
window.addProductToCart_details = function(url) {
var form = $('#product-details-form');
var quantity = parseInt(form.find('input[name*="EnteredQuantity"]').val()) || 1;
$.ajax({
url: url,
type: 'POST',
data: form.serialize(),
success: function(data) {
if (data.success) {
pushAddToCart({
id: '@Model.Id',
name: '@Html.Raw(Model.Name)',
price: @Model.ProductPrice.PriceValue,
brand: '@(Model.ProductManufacturers.FirstOrDefault()?.Name ?? "")',
category: '@(Model.Breadcrumb.CategoryBreadcrumb.LastOrDefault()?.Name ?? "")',
currency: '@Model.ProductPrice.CurrencyCode',
quantity: quantity
});
}
// Original NopCommerce handler
if (originalAddToCart) {
originalAddToCart.call(this, url);
}
}
});
return false;
};
Checkout Data Layer
Checkout Steps
@model CheckoutModel
@section DataLayer {
<script>
dataLayer.push({
'event': 'checkout',
'pageType': 'checkout',
'ecommerce': {
'checkout': {
'actionField': {
'step': @ViewBag.CheckoutStep,
'option': '@ViewBag.CheckoutStepName'
},
'products': [
@for (int i = 0; i < Model.Cart.Items.Count; i++)
{
var item = Model.Cart.Items[i];
<text>
{
'id': '@item.ProductId',
'name': '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(item.ProductName))',
'price': @item.UnitPrice,
'brand': '',
'category': '',
'variant': '@item.AttributeInfo',
'quantity': @item.Quantity
}@(i < Model.Cart.Items.Count - 1 ? "," : "")
</text>
}
]
}
}
});
</script>
}
Checkout Step Tracking
// In checkout controller or view
ViewBag.CheckoutStep = 1; // Billing
ViewBag.CheckoutStepName = "Billing Address";
// Step 2
ViewBag.CheckoutStep = 2; // Shipping
ViewBag.CheckoutStepName = "Shipping Method";
// Step 3
ViewBag.CheckoutStep = 3; // Payment
ViewBag.CheckoutStepName = "Payment Method";
// Step 4
ViewBag.CheckoutStep = 4; // Confirm
ViewBag.CheckoutStepName = "Order Confirmation";
Purchase Data Layer
Order Confirmation Page
@model CheckoutCompletedModel
@section DataLayer {
<script>
@{
var order = await orderService.GetOrderByIdAsync(Model.OrderId);
var orderItems = await orderService.GetOrderItemsAsync(order.Id);
}
dataLayer.push({
'event': 'purchase',
'pageType': 'orderconfirmation',
'ecommerce': {
'purchase': {
'actionField': {
'id': '@order.CustomOrderNumber',
'affiliation': '@storeName',
'revenue': @order.OrderTotal,
'tax': @order.OrderTax,
'shipping': @order.OrderShippingInclTax,
'coupon': '@(order.DiscountUsageHistory.FirstOrDefault()?.Discount?.CouponCode ?? "")'
},
'products': [
@for (int i = 0; i < orderItems.Count; i++)
{
var item = orderItems[i];
var product = await _productService.GetProductByIdAsync(item.ProductId);
<text>
{
'id': '@item.ProductId',
'name': '@Html.Raw(System.Text.Json.JsonSerializer.Serialize(product.Name))',
'price': @item.UnitPriceInclTax,
'brand': '',
'category': '',
'variant': '@item.AttributeDescription',
'quantity': @item.Quantity,
'coupon': ''
}@(i < orderItems.Count - 1 ? "," : "")
</text>
}
]
},
'orderStatus': '@order.OrderStatus.ToString()',
'paymentMethod': '@order.PaymentMethodSystemName',
'shippingMethod': '@order.ShippingMethod'
}
});
</script>
}
C# Data Layer Service
Creating Data Layer Service
// Services/IDataLayerService.cs
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Orders;
namespace YourCompany.Plugin.Widgets.GoogleTagManager.Services
{
public interface IDataLayerService
{
Task<object> GetProductDataAsync(Product product);
Task<object> GetCartDataAsync(ShoppingCart cart);
Task<object> GetOrderDataAsync(Order order);
Task<object> GetCheckoutDataAsync(int step, IList<ShoppingCartItem> items);
}
}
Implementation
// Services/DataLayerService.cs
public class DataLayerService : IDataLayerService
{
private readonly IProductService _productService;
private readonly IPriceCalculationService _priceCalculationService;
private readonly IWorkContext _workContext;
private readonly ICategoryService _categoryService;
private readonly IManufacturerService _manufacturerService;
public DataLayerService(
IProductService productService,
IPriceCalculationService priceCalculationService,
IWorkContext workContext,
ICategoryService categoryService,
IManufacturerService manufacturerService)
{
_productService = productService;
_priceCalculationService = priceCalculationService;
_workContext = workContext;
_categoryService = categoryService;
_manufacturerService = manufacturerService;
}
public async Task<object> GetProductDataAsync(Product product)
{
var priceModel = await _priceCalculationService.GetFinalPriceAsync(product);
var currency = await _workContext.GetWorkingCurrencyAsync();
var productCategories = await _categoryService.GetProductCategoriesByProductIdAsync(product.Id);
var category = productCategories.FirstOrDefault();
var categoryName = category != null
? (await _categoryService.GetCategoryByIdAsync(category.CategoryId))?.Name
: "";
var productManufacturers = await _manufacturerService.GetProductManufacturersByProductIdAsync(product.Id);
var manufacturer = productManufacturers.FirstOrDefault();
var manufacturerName = manufacturer != null
? (await _manufacturerService.GetManufacturerByIdAsync(manufacturer.ManufacturerId))?.Name
: "";
return new
{
id = product.Id.ToString(),
name = product.Name,
price = priceModel.finalPrice,
brand = manufacturerName,
category = categoryName,
variant = "",
sku = product.Sku,
currency = currency.CurrencyCode
};
}
public async Task<object> GetCartDataAsync(ShoppingCart cart)
{
var currency = await _workContext.GetWorkingCurrencyAsync();
var items = new List<object>();
foreach (var item in cart)
{
var product = await _productService.GetProductByIdAsync(item.ProductId);
var priceModel = await _priceCalculationService.GetFinalPriceAsync(product, item.Quantity);
items.Add(new
{
id = product.Id.ToString(),
name = product.Name,
price = priceModel.finalPrice,
quantity = item.Quantity
});
}
return new
{
currencyCode = currency.CurrencyCode,
products = items.ToArray()
};
}
public async Task<object> GetOrderDataAsync(Order 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
{
id = product.Id.ToString(),
name = product.Name,
price = item.UnitPriceInclTax,
quantity = item.Quantity
});
}
return new
{
transaction_id = order.CustomOrderNumber,
affiliation = "Online Store",
value = order.OrderTotal,
tax = order.OrderTax,
shipping = order.OrderShippingInclTax,
currency = order.CustomerCurrencyCode,
items = items.ToArray()
};
}
public async Task<object> GetCheckoutDataAsync(int step, IList<ShoppingCartItem> items)
{
var products = new List<object>();
foreach (var item in items)
{
var product = await _productService.GetProductByIdAsync(item.ProductId);
var priceModel = await _priceCalculationService.GetFinalPriceAsync(product, item.Quantity);
products.Add(new
{
id = product.Id.ToString(),
name = product.Name,
price = priceModel.finalPrice,
quantity = item.Quantity
});
}
return new
{
step = step,
products = products.ToArray()
};
}
}
Custom Events
User Actions
// Newsletter subscription
dataLayer.push({
'event': 'newsletterSubscribe',
'subscriptionType': 'newsletter',
'subscriptionEmail': email
});
// Product review
dataLayer.push({
'event': 'productReview',
'productId': productId,
'productName': productName,
'rating': rating
});
// Wishlist add
dataLayer.push({
'event': 'addToWishlist',
'productId': productId,
'productName': productName,
'productPrice': price
});
// Product comparison
dataLayer.push({
'event': 'compareProducts',
'productIds': [id1, id2, id3],
'comparisonType': 'features'
});
Testing Data Layer
Console Debugging
// View entire data layer
console.table(dataLayer);
// View specific push
dataLayer.forEach((item, index) => {
console.log(`Item ${index}:`, item);
});
// Monitor new pushes
var originalPush = dataLayer.push;
dataLayer.push = function() {
console.log('Data Layer Push:', arguments);
return originalPush.apply(dataLayer, arguments);
};
GTM Preview Mode
- Open GTM Preview mode
- Navigate to page
- Click Data Layer tab
- Verify all variables present
- Check ecommerce object structure
Chrome Extension
Use Google Tag Assistant or dataLayer Inspector extensions to view data layer in real-time.
Best Practices
Data Layer Structure
// Good: Consistent naming
dataLayer.push({
'event': 'addToCart',
'ecommerce': {
'currencyCode': 'USD',
'add': {
'products': [...]
}
}
});
// Bad: Inconsistent structure
dataLayer.push({
'addtocart': true,
'product_id': '123',
'prod_price': 99.99
});
Clear Before Push
// Clear ecommerce object before new push
dataLayer.push({
'ecommerce': null
});
dataLayer.push({
'event': 'purchase',
'ecommerce': {
'purchase': {...}
}
});
Event Naming
Use consistent, descriptive event names:
pageView,productView,categoryViewaddToCart,removeFromCart,viewCartcheckout,purchasenewsletterSubscribe,productReview
Next Steps
- GTM Setup - Configure Google Tag Manager
- GA4 Ecommerce - GA4 ecommerce tracking
- Events Not Firing - Debug data layer issues