GTM Data Layer Implementation on NopCommerce | OpsBlu Docs

GTM Data Layer Implementation on NopCommerce

Implement comprehensive Google Tag Manager data layer on NopCommerce for enhanced ecommerce tracking, using C# and Razor to populate product, cart, and...

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

  1. Open GTM Preview mode
  2. Navigate to page
  3. Click Data Layer tab
  4. Verify all variables present
  5. 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, categoryView
  • addToCart, removeFromCart, viewCart
  • checkout, purchase
  • newsletterSubscribe, productReview

Next Steps