Track user interactions and custom events in Google Analytics 4 on your NopCommerce store using JavaScript, C# server-side tracking, and Razor view integration.
Before You Begin
Prerequisites:
- GA4 setup complete (see GA4 Setup Guide)
- Active GA4 Measurement ID configured
- Basic understanding of GA4 event structure
- Access to NopCommerce theme files or plugin development
GA4 Event Structure:
gtag('event', 'event_name', {
'parameter_1': 'value_1',
'parameter_2': 'value_2',
// Additional parameters
});
Recommended GA4 Events for NopCommerce
Standard E-commerce Events
These events are covered in detail in the Ecommerce Tracking Guide:
view_item- Product page viewadd_to_cart- Add product to cartremove_from_cart- Remove from cartbegin_checkout- Start checkout processadd_payment_info- Payment method selectedadd_shipping_info- Shipping method selectedpurchase- Order completed
Custom NopCommerce Events
Additional events specific to NopCommerce functionality:
search- Product searchview_item_list- Category/manufacturer page viewselect_item- Product clicked from listingadd_to_wishlist- Add to wishlistshare- Product sharelogin- Customer loginsign_up- New customer registrationgenerate_lead- Newsletter subscriptionview_promotion- View promotional banner
Method 1: JavaScript Event Tracking in Razor Views
Product Search Tracking
Add to /Themes/YourTheme/Views/Catalog/Search.cshtml:
@model SearchModel
@section scripts {
<script>
// Track search event when search results load
@if (!string.IsNullOrEmpty(Model.q))
{
<text>
gtag('event', 'search', {
'search_term': '@Html.Raw(Model.q)',
'number_of_results': @Model.Products.Count,
'search_type': 'product_search'
});
</text>
}
</script>
}
Add to Wishlist Tracking
Add to /Themes/YourTheme/Views/Product/ProductTemplate.Simple.cshtml (or your product template):
@model ProductDetailsModel
<script>
// Track add to wishlist
function trackWishlistAdd(productId, productName, productPrice) {
gtag('event', 'add_to_wishlist', {
'currency': '@Model.ProductPrice.CurrencyCode',
'value': productPrice,
'items': [{
'item_id': productId,
'item_name': productName,
'price': productPrice
}]
});
}
// Attach to wishlist button
document.addEventListener('DOMContentLoaded', function() {
var wishlistBtn = document.querySelector('.add-to-wishlist-button');
if (wishlistBtn) {
wishlistBtn.addEventListener('click', function() {
trackWishlistAdd(
'@Model.Id',
'@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Name))',
@Model.ProductPrice.PriceValue
);
});
}
});
</script>
Product Comparison Tracking
@* Add to comparison tracking *@
<script>
function trackCompareAdd(productId, productName) {
gtag('event', 'add_to_compare', {
'event_category': 'Product Comparison',
'event_label': productName,
'item_id': productId
});
}
document.addEventListener('DOMContentLoaded', function() {
var compareBtn = document.querySelector('.add-to-compare-list-button');
if (compareBtn) {
compareBtn.addEventListener('click', function() {
var productId = this.getAttribute('data-productid');
var productName = this.getAttribute('data-productname');
trackCompareAdd(productId, productName);
});
}
});
</script>
Newsletter Subscription Tracking
Add to /Themes/YourTheme/Views/Newsletter/SubscriptionActivation.cshtml:
@model SubscriptionActivationModel
@if (Model.Result)
{
<script>
gtag('event', 'generate_lead', {
'event_category': 'Newsletter',
'event_label': 'Subscription Confirmed',
'method': 'email'
});
</script>
}
Product Share Tracking
@* Track social sharing *@
<script>
function trackProductShare(method, productId, productName) {
gtag('event', 'share', {
'method': method,
'content_type': 'product',
'item_id': productId,
'content_name': productName
});
}
// Example: Facebook share
document.querySelector('.share-facebook')?.addEventListener('click', function() {
trackProductShare('Facebook', '@Model.Id', '@Html.Raw(Model.Name)');
});
// Example: Twitter share
document.querySelector('.share-twitter')?.addEventListener('click', function() {
trackProductShare('Twitter', '@Model.Id', '@Html.Raw(Model.Name)');
});
// Example: Email share
document.querySelector('.email-a-friend')?.addEventListener('click', function() {
trackProductShare('Email', '@Model.Id', '@Html.Raw(Model.Name)');
});
</script>
Method 2: Server-Side Event Tracking with C#
Creating an Analytics Service
Step 1: Create Interface
// Services/IAnalyticsService.cs
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Catalog;
namespace YourCompany.Plugin.Analytics.GoogleGA4.Services
{
public interface IAnalyticsService
{
/// <summary>
/// Track custom event
/// </summary>
Task TrackEventAsync(string eventName, Dictionary<string, object> parameters);
/// <summary>
/// Track customer login
/// </summary>
Task TrackLoginAsync(Customer customer);
/// <summary>
/// Track customer registration
/// </summary>
Task TrackRegistrationAsync(Customer customer);
/// <summary>
/// Track product view
/// </summary>
Task TrackProductViewAsync(Product product, Customer customer);
/// <summary>
/// Track category view
/// </summary>
Task TrackCategoryViewAsync(int categoryId, string categoryName, int productCount);
/// <summary>
/// Track search
/// </summary>
Task TrackSearchAsync(string searchTerm, int resultCount);
}
}
Step 2: Implement Service
// Services/AnalyticsService.cs
using Microsoft.AspNetCore.Http;
using Nop.Core;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Customers;
using Nop.Services.Configuration;
using System.Text.Json;
namespace YourCompany.Plugin.Analytics.GoogleGA4.Services
{
public class AnalyticsService : IAnalyticsService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ISettingService _settingService;
private readonly IStoreContext _storeContext;
public AnalyticsService(
IHttpContextAccessor httpContextAccessor,
ISettingService settingService,
IStoreContext storeContext)
{
_httpContextAccessor = httpContextAccessor;
_settingService = settingService;
_storeContext = storeContext;
}
public async Task TrackEventAsync(string eventName, Dictionary<string, object> parameters)
{
var store = await _storeContext.GetCurrentStoreAsync();
var settings = await _settingService.LoadSettingAsync<GoogleGA4Settings>(store.Id);
if (string.IsNullOrEmpty(settings.MeasurementId))
return;
// Add event to ViewData for client-side tracking
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext?.Items != null)
{
if (!httpContext.Items.ContainsKey("GA4Events"))
httpContext.Items["GA4Events"] = new List<(string, Dictionary<string, object>)>();
var events = (List<(string, Dictionary<string, object>)>)httpContext.Items["GA4Events"];
events.Add((eventName, parameters));
}
}
public async Task TrackLoginAsync(Customer customer)
{
var parameters = new Dictionary<string, object>
{
{ "method", "Email" },
{ "customer_id", customer.Id },
{ "customer_group", customer.CustomerRoles.FirstOrDefault()?.SystemName ?? "Guest" }
};
await TrackEventAsync("login", parameters);
}
public async Task TrackRegistrationAsync(Customer customer)
{
var parameters = new Dictionary<string, object>
{
{ "method", "Email" },
{ "customer_id", customer.Id }
};
await TrackEventAsync("sign_up", parameters);
}
public async Task TrackProductViewAsync(Product product, Customer customer)
{
var parameters = new Dictionary<string, object>
{
{ "items", new[]
{
new
{
item_id = product.Id.ToString(),
item_name = product.Name,
item_category = product.ProductCategories.FirstOrDefault()?.Category?.Name ?? ""
}
}
}
};
await TrackEventAsync("view_item", parameters);
}
public async Task TrackCategoryViewAsync(int categoryId, string categoryName, int productCount)
{
var parameters = new Dictionary<string, object>
{
{ "item_list_id", categoryId.ToString() },
{ "item_list_name", categoryName },
{ "number_of_items", productCount }
};
await TrackEventAsync("view_item_list", parameters);
}
public async Task TrackSearchAsync(string searchTerm, int resultCount)
{
var parameters = new Dictionary<string, object>
{
{ "search_term", searchTerm },
{ "number_of_results", resultCount }
};
await TrackEventAsync("search", parameters);
}
}
}
Step 3: Render Events in Layout
Create a ViewComponent to render queued events:
// Components/AnalyticsEventsViewComponent.cs
using Microsoft.AspNetCore.Mvc;
using Nop.Web.Framework.Components;
namespace YourCompany.Plugin.Analytics.GoogleGA4.Components
{
public class AnalyticsEventsViewComponent : NopViewComponent
{
private readonly IHttpContextAccessor _httpContextAccessor;
public AnalyticsEventsViewComponent(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public IViewComponentResult Invoke()
{
var httpContext = _httpContextAccessor.HttpContext;
var events = httpContext?.Items["GA4Events"] as List<(string, Dictionary<string, object>)>;
return View("~/Plugins/YourCompany.Plugin.Analytics.GoogleGA4/Views/Events.cshtml", events ?? new List<(string, Dictionary<string, object>)>());
}
}
}
Step 4: Events View Template
@* Views/Events.cshtml *@
@model List<(string EventName, Dictionary<string, object> Parameters)>
@if (Model.Any())
{
<script>
@foreach (var (eventName, parameters) in Model)
{
<text>
gtag('event', '@eventName', @Html.Raw(System.Text.Json.JsonSerializer.Serialize(parameters)));
</text>
}
</script>
}
Step 5: Use in Controllers
// Example: In a custom controller
using YourCompany.Plugin.Analytics.GoogleGA4.Services;
public class CustomController : BasePublicController
{
private readonly IAnalyticsService _analyticsService;
public CustomController(IAnalyticsService analyticsService)
{
_analyticsService = analyticsService;
}
public async Task<IActionResult> ProductDetails(int productId)
{
var product = await _productService.GetProductByIdAsync(productId);
var customer = await _workContext.GetCurrentCustomerAsync();
// Track product view
await _analyticsService.TrackProductViewAsync(product, customer);
return View(product);
}
}
Method 3: Event Tracking via JavaScript Module
Create a reusable JavaScript module for consistent event tracking.
Analytics Module
// wwwroot/js/analytics-tracking.js
var NopAnalytics = (function() {
'use strict';
// Check if gtag is available
function isGtagAvailable() {
return typeof gtag === 'function';
}
// Track generic event
function trackEvent(eventName, parameters) {
if (!isGtagAvailable()) {
console.warn('Google Analytics not initialized');
return;
}
gtag('event', eventName, parameters || {});
}
// Track form submission
function trackFormSubmit(formName, formType) {
trackEvent('form_submit', {
'form_name': formName,
'form_type': formType
});
}
// Track button click
function trackButtonClick(buttonName, buttonLocation) {
trackEvent('button_click', {
'button_name': buttonName,
'button_location': buttonLocation
});
}
// Track file download
function trackDownload(fileName, fileType) {
trackEvent('file_download', {
'file_name': fileName,
'file_type': fileType,
'file_extension': fileName.split('.').pop()
});
}
// Track external link click
function trackOutboundLink(url, linkText) {
trackEvent('click', {
'event_category': 'outbound',
'event_label': url,
'link_text': linkText,
'transport_type': 'beacon'
});
}
// Track video interaction
function trackVideo(action, videoTitle, videoUrl) {
trackEvent('video_' + action, {
'video_title': videoTitle,
'video_url': videoUrl
});
}
// Track product filter usage
function trackProductFilter(filterType, filterValue) {
trackEvent('filter_products', {
'filter_type': filterType,
'filter_value': filterValue
});
}
// Track product sort
function trackProductSort(sortType) {
trackEvent('sort_products', {
'sort_type': sortType
});
}
// Track review submission
function trackReviewSubmit(productId, productName, rating) {
trackEvent('submit_review', {
'item_id': productId,
'item_name': productName,
'rating': rating
});
}
// Track coupon entry
function trackCouponEntry(couponCode) {
trackEvent('coupon_entry', {
'coupon_code': couponCode
});
}
// Automatically track all file downloads
function initDownloadTracking() {
document.addEventListener('click', function(e) {
var target = e.target.closest('a[href]');
if (!target) return;
var href = target.getAttribute('href');
var fileExtensions = /\.(pdf|doc|docx|xls|xlsx|zip|rar|tar|gz|exe|dmg)$/i;
if (fileExtensions.test(href)) {
var fileName = href.split('/').pop();
trackDownload(fileName, href.split('.').pop());
}
});
}
// Automatically track outbound links
function initOutboundLinkTracking() {
document.addEventListener('click', function(e) {
var target = e.target.closest('a[href]');
if (!target) return;
var href = target.getAttribute('href');
if (href && (href.startsWith('http://') || href.startsWith('https://'))
&& !href.includes(window.location.hostname)) {
trackOutboundLink(href, target.textContent);
}
});
}
// Initialize automatic tracking
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
initDownloadTracking();
initOutboundLinkTracking();
});
} else {
initDownloadTracking();
initOutboundLinkTracking();
}
}
// Public API
return {
init: init,
trackEvent: trackEvent,
trackFormSubmit: trackFormSubmit,
trackButtonClick: trackButtonClick,
trackDownload: trackDownload,
trackOutboundLink: trackOutboundLink,
trackVideo: trackVideo,
trackProductFilter: trackProductFilter,
trackProductSort: trackProductSort,
trackReviewSubmit: trackReviewSubmit,
trackCouponEntry: trackCouponEntry
};
})();
// Auto-initialize
NopAnalytics.init();
Using the Analytics Module
@* Include in your theme layout *@
<script src="~/js/analytics-tracking.js"></script>
@* Use in templates *@
<script>
// Track custom button click
document.querySelector('.special-offer-btn')?.addEventListener('click', function() {
NopAnalytics.trackButtonClick('Special Offer', 'Homepage Hero');
});
// Track product filter
document.querySelector('.filter-price')?.addEventListener('change', function() {
NopAnalytics.trackProductFilter('price', this.value);
});
// Track product sort
document.querySelector('.product-sorting')?.addEventListener('change', function() {
NopAnalytics.trackProductSort(this.value);
});
// Track review submission
document.querySelector('.write-review-form')?.addEventListener('submit', function() {
var rating = document.querySelector('input[name="rating"]:checked')?.value;
NopAnalytics.trackReviewSubmit(
'@Model.ProductId',
'@Model.ProductName',
rating
);
});
</script>
Advanced Event Tracking
Scroll Depth Tracking
// Track how far users scroll on product pages
(function() {
var scrollDepths = [25, 50, 75, 100];
var tracked = {};
window.addEventListener('scroll', function() {
var scrollPercent = Math.round((window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100);
scrollDepths.forEach(function(depth) {
if (scrollPercent >= depth && !tracked[depth]) {
gtag('event', 'scroll_depth', {
'percent_scrolled': depth,
'page_location': window.location.pathname
});
tracked[depth] = true;
}
});
});
})();
Time on Page Tracking
// Track engaged time on page
(function() {
var startTime = new Date().getTime();
var engaged = true;
var totalEngagedTime = 0;
var lastCheck = startTime;
// Track when user becomes inactive
var inactivityEvents = ['blur', 'hidden'];
inactivityEvents.forEach(function(event) {
window.addEventListener(event, function() {
if (engaged) {
totalEngagedTime += (new Date().getTime() - lastCheck);
engaged = false;
}
});
});
// Track when user becomes active again
var activityEvents = ['focus', 'visible'];
activityEvents.forEach(function(event) {
window.addEventListener(event, function() {
if (!engaged) {
lastCheck = new Date().getTime();
engaged = true;
}
});
});
// Send engaged time on page unload
window.addEventListener('beforeunload', function() {
if (engaged) {
totalEngagedTime += (new Date().getTime() - lastCheck);
}
var engagedSeconds = Math.round(totalEngagedTime / 1000);
gtag('event', 'engaged_time', {
'value': engagedSeconds,
'event_category': 'Engagement',
'page_location': window.location.pathname
});
});
})();
Error Tracking
// Track JavaScript errors
window.addEventListener('error', function(e) {
gtag('event', 'exception', {
'description': e.message,
'fatal': false,
'error_location': e.filename + ':' + e.lineno
});
});
// Track AJAX errors
if (typeof $ !== 'undefined') {
$(document).ajaxError(function(event, jqXHR, settings, thrownError) {
gtag('event', 'exception', {
'description': 'AJAX Error: ' + thrownError,
'fatal': false,
'error_location': settings.url
});
});
}
Testing and Debugging Events
Enable Debug Mode
// Add to page temporarily for testing
gtag('config', 'G-XXXXXXXXXX', {
'debug_mode': true
});
Using GA4 DebugView
Enable debug mode in your browser:
// Add to console or page window.gtag_enable_debug_mode = true;Or add query parameter:
https://yourstore.com/?debug_mode=trueOpen GA4 DebugView:
- Go to Google Analytics
- Navigate to Admin > DebugView
- Browse your site
- See events in real-time
Console Logging
// Wrap gtag to log all events
var originalGtag = window.gtag;
window.gtag = function() {
console.log('GA4 Event:', arguments);
originalGtag.apply(window, arguments);
};
Event Naming Best Practices
Recommended Event Names
Use Google's recommended event names when possible:
- Use lowercase with underscores:
add_to_cart(notAddToCart) - Be descriptive:
newsletter_signup(notns) - Be consistent across your site
- Avoid special characters except underscores
- Keep under 40 characters
Custom Event Naming Convention
For NopCommerce-specific events:
{category}_{action}_{object}
Examples:
- product_filter_price
- product_sort_name
- customer_update_profile
- vendor_contact_form
Next Steps
- GA4 Ecommerce Tracking - Track sales and revenue
- GTM Data Layer - Advanced event management
- Events Not Firing - Troubleshoot event issues