Track custom events in Umbraco to measure user interactions, content engagement, and business-specific actions. This guide covers client-side JavaScript tracking, server-side C# tracking, and Umbraco-specific event patterns.
Prerequisites
Before tracking custom events:
- GA4 Setup Complete - Follow GA4 Setup Guide
- Measurement ID Configured - GA4 tracking code installed
- Umbraco Backoffice Access - For testing and validation
- Development Environment - Visual Studio or VS Code
Client-Side Event Tracking (JavaScript)
Basic Event Tracking in Razor Views
Track events directly in your Umbraco Razor views:
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
<button id="download-brochure" data-file-name="Product-Brochure.pdf">
Download Brochure
</button>
<script>
document.getElementById('download-brochure').addEventListener('click', function() {
gtag('event', 'file_download', {
'file_name': this.getAttribute('data-file-name'),
'content_type': '@Model?.ContentType.Alias',
'node_id': '@Model?.Id'
});
});
</script>
Track Umbraco Content Interactions
Track Article Scroll Depth:
<script>
var scrollDepths = [25, 50, 75, 100];
var triggeredDepths = [];
window.addEventListener('scroll', function() {
var scrollPercent = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
scrollDepths.forEach(function(depth) {
if (scrollPercent >= depth && !triggeredDepths.includes(depth)) {
triggeredDepths.push(depth);
gtag('event', 'scroll', {
'event_category': 'engagement',
'event_label': depth + '%',
'value': depth,
'content_name': '@Model?.Name',
'content_type': '@Model?.ContentType.Alias'
});
}
});
});
</script>
Track Video Plays:
<video id="product-video" controls>
<source src="@Model?.Value("videoUrl")" type="video/mp4">
</video>
<script>
var video = document.getElementById('product-video');
video.addEventListener('play', function() {
gtag('event', 'video_start', {
'video_title': '@Model?.Value("videoTitle")',
'video_url': '@Model?.Value("videoUrl")',
'content_id': '@Model?.Id'
});
});
video.addEventListener('ended', function() {
gtag('event', 'video_complete', {
'video_title': '@Model?.Value("videoTitle")',
'video_url': '@Model?.Value("videoUrl")'
});
});
</script>
Track External Links
<script>
document.addEventListener('DOMContentLoaded', function() {
// Track all external links
document.querySelectorAll('a[href^="http"]').forEach(function(link) {
if (link.hostname !== window.location.hostname) {
link.addEventListener('click', function(e) {
gtag('event', 'click', {
'event_category': 'outbound',
'event_label': this.href,
'transport_type': 'beacon',
'source_page': '@Model?.Name'
});
});
}
});
});
</script>
Server-Side Event Tracking (C#)
Create Analytics Service
IAnalyticsService.cs:
using System.Threading.Tasks;
namespace YourProject.Services
{
public interface IAnalyticsService
{
Task TrackEvent(string eventName, Dictionary<string, object> parameters);
Task TrackPageView(string pagePath, string pageTitle);
}
}
GoogleAnalyticsService.cs:
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
namespace YourProject.Services
{
public class GoogleAnalyticsService : IAnalyticsService
{
private readonly HttpClient _httpClient;
private readonly string _measurementId;
private readonly string _apiSecret;
public GoogleAnalyticsService(HttpClient httpClient, IConfiguration configuration)
{
_httpClient = httpClient;
_measurementId = configuration["Analytics:GoogleAnalytics:MeasurementId"];
_apiSecret = configuration["Analytics:GoogleAnalytics:ApiSecret"];
}
public async Task TrackEvent(string eventName, Dictionary<string, object> parameters)
{
var payload = new
{
client_id = Guid.NewGuid().ToString(), // Or use session/member ID
events = new[]
{
new
{
name = eventName,
@params = parameters
}
}
};
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}";
await _httpClient.PostAsync(url, content);
}
public async Task TrackPageView(string pagePath, string pageTitle)
{
await TrackEvent("page_view", new Dictionary<string, object>
{
{ "page_path", pagePath },
{ "page_title", pageTitle }
});
}
}
}
Register Service in Startup
Program.cs (Umbraco 10+):
using YourProject.Services;
var builder = WebApplication.CreateBuilder(args);
builder.CreateUmbracoBuilder()
.AddBackOffice()
.AddWebsite()
.AddDeliveryApi()
.AddComposers()
.Build();
// Register Analytics Service
builder.Services.AddHttpClient<IAnalyticsService, GoogleAnalyticsService>();
var app = builder.Build();
// ... rest of configuration
Track Events from Controllers
ContactController.cs:
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Web.Common.Controllers;
using YourProject.Services;
namespace YourProject.Controllers
{
public class ContactController : SurfaceController
{
private readonly IAnalyticsService _analyticsService;
public ContactController(
IUmbracoContextAccessor umbracoContextAccessor,
IUmbracoDatabaseFactory databaseFactory,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger profilingLogger,
IPublishedUrlProvider publishedUrlProvider,
IAnalyticsService analyticsService)
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
{
_analyticsService = analyticsService;
}
[HttpPost]
public async Task<IActionResult> SubmitForm(ContactFormModel model)
{
if (ModelState.IsValid)
{
// Process form submission
// ...
// Track event
await _analyticsService.TrackEvent("form_submit", new Dictionary<string, object>
{
{ "form_name", "contact_form" },
{ "form_id", "contact" },
{ "source_page", CurrentPage?.Name }
});
return RedirectToCurrentUmbracoPage();
}
return CurrentUmbracoPage();
}
}
}
Umbraco Forms Integration
Track Form Submissions
Create FormEventHandler.cs:
using Umbraco.Forms.Core.Services.Notifications;
using Microsoft.Extensions.Logging;
using YourProject.Services;
namespace YourProject.EventHandlers
{
public class FormEventHandler : INotificationHandler<FormSubmittedNotification>
{
private readonly IAnalyticsService _analyticsService;
private readonly ILogger<FormEventHandler> _logger;
public FormEventHandler(IAnalyticsService analyticsService, ILogger<FormEventHandler> logger)
{
_analyticsService = analyticsService;
_logger = logger;
}
public async Task Handle(FormSubmittedNotification notification, CancellationToken cancellationToken)
{
try
{
var form = notification.Form;
var record = notification.Record;
await _analyticsService.TrackEvent("form_submit", new Dictionary<string, object>
{
{ "form_id", form.Id.ToString() },
{ "form_name", form.Name },
{ "record_id", record.Id.ToString() },
{ "page_id", record.PageId.ToString() }
});
_logger.LogInformation($"Tracked form submission: {form.Name}");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to track form submission");
}
}
}
}
Register in Composer:
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Notifications;
namespace YourProject.Composers
{
public class FormTrackingComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.AddNotificationHandler<FormSubmittedNotification, FormEventHandler>();
}
}
}
Track Form Field Interactions
In Razor view:
@using Umbraco.Forms.Web
@model Umbraco.Forms.Web.Models.FormViewModel
<script>
document.addEventListener('DOMContentLoaded', function() {
var formElement = document.querySelector('.umbraco-forms-form');
// Track form start (first field interaction)
var formStarted = false;
formElement.querySelectorAll('input, textarea, select').forEach(function(field) {
field.addEventListener('focus', function() {
if (!formStarted) {
formStarted = true;
gtag('event', 'form_start', {
'form_id': '@Model.FormId',
'form_name': '@Model.FormName'
});
}
});
});
// Track form abandonment
window.addEventListener('beforeunload', function(e) {
if (formStarted && !formSubmitted) {
gtag('event', 'form_abandon', {
'form_id': '@Model.FormId',
'form_name': '@Model.FormName'
});
}
});
var formSubmitted = false;
formElement.addEventListener('submit', function() {
formSubmitted = true;
});
});
</script>
Track Content Publishing Events
Create Content Event Handler
ContentPublishingEventHandler.cs:
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using YourProject.Services;
namespace YourProject.EventHandlers
{
public class ContentPublishingEventHandler :
INotificationHandler<ContentPublishedNotification>
{
private readonly IAnalyticsService _analyticsService;
public ContentPublishingEventHandler(IAnalyticsService analyticsService)
{
_analyticsService = analyticsService;
}
public async Task Handle(ContentPublishedNotification notification, CancellationToken cancellationToken)
{
foreach (var node in notification.PublishedEntities)
{
await _analyticsService.TrackEvent("content_published", new Dictionary<string, object>
{
{ "content_type", node.ContentType.Alias },
{ "content_name", node.Name },
{ "node_id", node.Id },
{ "author", node.WriterId }
});
}
}
}
}
Track Member Actions
Track Member Registration
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace YourProject.Controllers
{
public class MemberController : SurfaceController
{
private readonly IMemberManager _memberManager;
private readonly IAnalyticsService _analyticsService;
public MemberController(
IMemberManager memberManager,
IAnalyticsService analyticsService,
/* other dependencies */)
{
_memberManager = memberManager;
_analyticsService = analyticsService;
}
[HttpPost]
public async Task<IActionResult> Register(RegisterModel model)
{
if (ModelState.IsValid)
{
var identityUser = MemberIdentityUser.CreateNew(
model.Email,
model.Email,
"Member",
true,
model.Name
);
var result = await _memberManager.CreateAsync(
identityUser,
model.Password
);
if (result.Succeeded)
{
// Track registration
await _analyticsService.TrackEvent("sign_up", new Dictionary<string, object>
{
{ "method", "email" },
{ "member_type", "Member" }
});
return RedirectToAction("Welcome");
}
}
return CurrentUmbracoPage();
}
[HttpPost]
public async Task<IActionResult> Login(LoginModel model)
{
var result = await _memberManager.SignInAsync(model.Email, model.Password, model.RememberMe);
if (result.Succeeded)
{
await _analyticsService.TrackEvent("login", new Dictionary<string, object>
{
{ "method", "email" }
});
return RedirectToCurrentUmbracoPage();
}
return CurrentUmbracoPage();
}
}
}
Track File Downloads
Track Media Downloads
@{
var pdfFile = Model?.Value<IPublishedContent>("downloadablePdf");
}
@if (pdfFile != null)
{
<a href="@pdfFile.Url()"
class="download-link"
data-file-name="@pdfFile.Name"
data-file-type="@pdfFile.ContentType.Alias"
download>
Download PDF
</a>
<script>
document.querySelectorAll('.download-link').forEach(function(link) {
link.addEventListener('click', function(e) {
gtag('event', 'file_download', {
'file_name': this.getAttribute('data-file-name'),
'file_type': this.getAttribute('data-file-type'),
'link_url': this.href,
'content_id': '@Model?.Id'
});
});
});
</script>
}
Track Search Queries
Track Internal Search
SearchController.cs:
using Examine;
using Umbraco.Cms.Infrastructure.Examine;
namespace YourProject.Controllers
{
public class SearchController : SurfaceController
{
private readonly IExamineManager _examineManager;
private readonly IAnalyticsService _analyticsService;
public SearchController(
IExamineManager examineManager,
IAnalyticsService analyticsService,
/* other dependencies */)
{
_examineManager = examineManager;
_analyticsService = analyticsService;
}
[HttpGet]
public async Task<IActionResult> Search(string query)
{
if (!string.IsNullOrWhiteSpace(query))
{
if (_examineManager.TryGetIndex(Constants.UmbracoIndexes.ExternalIndexName, out var index))
{
var searcher = index.Searcher;
var results = searcher.CreateQuery("content")
.Field("nodeName", query)
.Execute();
// Track search
await _analyticsService.TrackEvent("search", new Dictionary<string, object>
{
{ "search_term", query },
{ "results_count", results.TotalItemCount }
});
return View("SearchResults", results);
}
}
return View("SearchResults", null);
}
}
}
Enhanced Event Tracking
Track Time on Page
<script>
var startTime = new Date().getTime();
window.addEventListener('beforeunload', function() {
var endTime = new Date().getTime();
var timeOnPage = Math.round((endTime - startTime) / 1000); // seconds
gtag('event', 'time_on_page', {
'event_category': 'engagement',
'value': timeOnPage,
'content_name': '@Model?.Name',
'content_type': '@Model?.ContentType.Alias',
'non_interaction': true
});
});
</script>
Track Reading Progress
<script>
var articleContent = document.querySelector('.article-content');
if (articleContent) {
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
gtag('event', 'article_read', {
'event_category': 'engagement',
'event_label': 'Article Read',
'article_title': '@Model?.Name',
'article_id': '@Model?.Id'
});
observer.disconnect();
}
});
}, { threshold: 0.9 }); // 90% visible
observer.observe(articleContent);
}
</script>
Common Event Patterns
Recommended GA4 Events for Umbraco
// Page engagement
gtag('event', 'page_view');
gtag('event', 'scroll');
gtag('event', 'click');
// Forms
gtag('event', 'form_start');
gtag('event', 'form_submit');
gtag('event', 'form_abandon');
// Content interaction
gtag('event', 'file_download');
gtag('event', 'video_start');
gtag('event', 'video_complete');
// User actions
gtag('event', 'sign_up');
gtag('event', 'login');
gtag('event', 'search');
// Custom Umbraco events
gtag('event', 'content_published'); // Server-side
gtag('event', 'member_registration'); // Server-side
Testing and Debugging
Enable Debug Mode
@inject Microsoft.Extensions.Hosting.IHostEnvironment HostEnvironment
<script>
@if (HostEnvironment.IsDevelopment())
{
<text>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({'event': 'gtm.js', 'gtm.start': new Date().getTime()});
// Log all events to console
var originalGtag = window.gtag;
window.gtag = function() {
console.log('GA4 Event:', arguments);
originalGtag.apply(this, arguments);
};
</text>
}
</script>
Test Events in Realtime
- Open Google Analytics → Realtime → Events
- Perform action on Umbraco site
- Verify event appears within seconds
- Check event parameters populate correctly
Next Steps
- Google Analytics E-commerce - Track Vendr/Umbraco Commerce
- Troubleshooting Tracking Issues - Debug event tracking
- GTM Data Layer - Advanced event tracking with GTM