Kentico MVC and Google Analytics: Implementation


You probably feel or know that Kentico MVC and Google Analytics are a good fit. But you might not know how to integrate them properly. After reading this article, you'll be able to do that right.

In this blogpost, you'll learn how to employ Google Analytics (GA) using best practices. Namely, you'll be able to quickly start tracking the performance, in sound with GDPR and other data protection laws. We'll demonstrate the implementation process in the source code of our standard Dancing Goat MVC sample site that you can deploy yourselves using the Kentico installer.

Preparing the Dancing Goat MVC app

To make your future GA testing environment work correctly, your local MVC app needs to be accessed using a fully qualified domain name (FQDN), not under localhost. GA wouldn't be able to distinguish between your localhost and the ones of others. Therefore, you should make sure you can access your DG instance with an FQDN. In Active Directory environments, simply run the following command to find out your FQDN:

ipconfig -all

This will tell you what your hostname and DNS suffix are. Your FQDN can be determined by just connecting the two pieces of information with a dot. Example:

Connection-specific DNS Suffix:

In this case, the FQDN is

If you're curious about whether your DG site needs to be publicly routable, then rest assured, it doesn't have to be. You can sit behind a firewall without any issues.

If you're not in Active Directory, you can factor an arbitrary FQDN in your %WINDIR%\system32\drivers\etc\hosts file. Just pick a domain name that is as unique as possible. Think of others who might have been pushing data to GA with their ad-hoc domain names. The sole fact that your ad-hoc domain name cannot be found in public DNS servers does not necessarily mean that data for this ad-hoc name hasn't been submitted to GA by someone else in the past.

Setting up Google Analytics

You'll need a GA account. To set up one, just navigate to and identify yourself with your Google account. Click Create account to start your free instance of GA. Once your account is ready, set up a property. Finally, you'll want to set up at least one view on your property. It is a good practice to leave the out-of-box view as is, because it gathers all data, right from the start. You may want to rename the out-of-box master view to Raw or something similar.

Once you set up your account, property, and views, we highly recommend setting up Google Tag Manager (GTM) to aggregate all tracking tags, including GA, into a single GTM tag.


Once your GTM container is ready, add the GA tag.


Finally, click the container ID in the top right corner to get the JavaScript code and the <noscript /> tag for your site.


When you deploy GTM to the website, the script starts adding GA cookies to visitors' browsers that make tracking possible. Therefore, we recommend using the incognito window for all your testing.

Now you're ready to start implementing GA via GTM into your MVC site.

About the Kentico consent framework

When running a global website, you need to take into account all regional data protection legislations that your visitors may be covered by.

One of the most strict ones is GDPR and the European directive 2002/58/EC (called EU cookie law). Under these acts, you need to make sure all personal data is gathered based on the visitor's consent.

The Dancing Goat (DG) sample site demoes Kentico consent framework and uses cookies to manipulate all the personal data. The system tracks contact behavior, shopping cart affinity, and other personal data through the Agree action method in the ConsentController class. When a user gives their consent, the Agree method raises the cookie level.

// POST: Consent/Agree [HttpPost] [ValidateAntiForgeryToken] public ActionResult Agree() { var resultStatus = HttpStatusCode.BadRequest; var consent = ConsentInfoProvider.GetConsentInfo(TrackingConsentGenerator.CONSENT_NAME); if (consent != null) { mCookieLevelProvider.SetCurrentCookieLevel(CookieLevel.All); mConsentAgreementService.Agree(ContactManagementContext.CurrentContact, consent); resultStatus = HttpStatusCode.OK; } // Redirect is handled on client by javascript return new HttpStatusCodeResult(resultStatus); }

Conversely, when a visitor clicks the Revoke button on the Privacy page, the PrivacyController's Revoke action method lowers the cookie level. Plus, it initiates a custom scheduled task that deletes the personal data of the current contact from the database.

[HttpPost] [ValidateAntiForgeryToken] public ActionResult Revoke(string consentName) { var consentToRevoke = ConsentInfoProvider.GetConsentInfo(consentName); if (consentToRevoke != null && CurrentContact != null) { mConsentAgreementService.Revoke(CurrentContact, consentToRevoke); if (consentName == TrackingConsentGenerator.CONSENT_NAME) { mCookieLevelProvider.SetCurrentCookieLevel(mCookieLevelProvider.GetDefaultCookieLevel()); ExecuteRevokeTrackingConsentTask(mSiteService.CurrentSite, CurrentContact); } TempData[SUCCESS_RESULT] = true; } else { TempData[ERROR_RESULT] = true; } return RedirectToAction("Index"); }

Activating the data protection demo

Before you start, you should open the Kentico administration. In the Pages application, navigate to the Generator page in the root of the content tree. At the bottom of the page, click the very last Generate button to activate the data protection demo features on the site.


Adding the GTM snippets

Since the consents in the DG site affect just the cookie level, you should be including the GTM snippets based on that. With a default cookie level, the GTM snippets will be omitted, otherwise, they'll be added to the page markup.

Note: Your mileage may vary. You may want to start including the tracking snippets when the cookie level reaches a certain value (e.g., Visitor or All). In our example, we'll stick to the easiest method: detect "anything above the default level".

To toggle the GTM snippets, add references to two new partial views in the code of the Views > Shared > _Layout.cshtml file. One will represent the JavaScript snippet in the <head /> tag: the _GtmHeadScript partial view. The second will be the <noscript /> tag: _GtmNoscriptTag.

<!DOCTYPE html> <html> <head id="head"> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta charset="UTF-8" /> <title>@ViewBag.Title - Dancing Goat</title> @Html.Partial("_GtmHeadScript") <link rel="icon" href="@Url.Content("~/content/images/favicon.png")" type="image/png" /> <link href="@Url.Content("~/Content/Styles/Site.css")" rel="stylesheet" type="text/css" /> <link rel="canonical" href="@Url.CanonicalUrl()" /> @RenderSection("styles", required: false) @Html.Kentico().ABTestLoggerScript() </head> <body> @Html.Partial("_GtmNoscriptTag") <div class="page-wrap"> @Html.Action("Index", "Consent") <header class="header"> <div class="menu"> ...

But there's a small catch.

Our partial views need to determine the cookie level through an object that implements CMS.Helpers.ICurrentCookieLevelProvider.  Luckily, the DG site already uses the AutoFac IoC container that can serve such objects right off the bat. And the DependencyResolverConfig class already has what's needed to serve objects to MVC views.

builder.RegisterSource(new ViewRegistrationSource());

All you need to do is to create a class that inherits from System.Web.Mvc.WebViewPage. This class will contain an ICurrentCookieLevelProvider property. AutoFac will populate that property when each view is created. You don't even need to define a constructor. You just need to write a simple expression-based method that uses the cookie level provider to determine if the current level is the default one or not.

As the sole purpose of the new class is to back the partial views, create the class in Views > Shared.

public class GoogleAnalyticsMarkupBuilder : WebViewPage { public ICurrentCookieLevelProvider CurrentCookieLevelProvider { get; set; } public bool IsCurrentlyDefaultLevel => CurrentCookieLevelProvider.GetCurrentCookieLevel() == CurrentCookieLevelProvider.GetDefaultCookieLevel(); public override void Execute() { } }

Then, just inherit from the GoogleAnalyticsMarkupBuilder class in the _GtmHeadScript.cshtml and  _GtmNoscriptTag.cshtml partial views. Create both views in Views > Shared.

@inherits DancingGoat.Views.Shared.GoogleAnalyticsMarkupBuilder @if (!IsCurrentlyDefaultLevel) { <script> (function (w, d, s, l, i) { w[l] = w[l] || []; w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); var f = d.getElementsByTagName(s)[0], j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src = '' + i + dl; f.parentNode.insertBefore(j, f); })(window, document, 'script', 'dataLayer', 'GTM-XXXX'); </script> }

@inherits DancingGoat.Views.Shared.GoogleAnalyticsMarkupBuilder @if (!IsCurrentlyDefaultLevel) { <noscript> <iframe src="" height="0" width="0" style="display:none;visibility:hidden"></iframe> </noscript> }

Now the GTM snippets are added only when the visitor agrees to that.

Note: In our case, adding of GTM snippets was controlled solely by the cookie level (which in turn was driven by the consents). In your solution, you may want to bind the consents to something different than just the cookie level.

Implementing the right to be forgotten

When it comes to revoking the consent, GDPR tells us to wipe all traces of personal data, should the visitor say so. Let's see what Google has to say in regards to data erasure:

"In some cases, it may be necessary to disable Google Analytics on a page without removing the gtag.js tag. For example, you might do this if your site's privacy policy provides an option for the user to opt-out of Google Analytics.

The gtag.js library includes a window property that, when set to true, disables gtag.js from sending data to Google Analytics. When Google Analytics attempts to set a cookie or send data back to the Google Analytics servers, it will first check if this property is set, and will take no action if the value is set to true.

To disable Analytics programmatically, set the following window property to true:

window['ga-disable-GA_MEASUREMENT_ID'] = true;"

That's pretty much it. You won't find much else in the official documentation. However, even a cookie, when evaluated in connection with other data, can lead to user identification. Under the article 4 of GDPR, even cookies are personal data that can identify people.

Leaving the cookies as they are undermines the point of GDPR. Thus, not only you need to disable the tracking as per the official documentation. You also need to delete the cookies.

Disabling the GA tracking

Let's first take the approach recommended by Google. Add a reference to another partial view in the _Layout.cshtml file.

... @RenderSection("scripts", required: false) @Html.Kentico().ActivityLoggingScript() @Html.Partial("_GoogleAnalyticsDisabler") </body> </html>

Then, create that partial view as Views > Shared > _GoogleAnalyticsDisabler.cshtml. Like so:

@inherits DancingGoat.Views.Shared.GoogleAnalyticsMarkupBuilder @if (IsCurrentlyDefaultLevel) { <script>window["ga-disable-@ApplicationConfig.GoogleAnalyticsPropertyId"] = true;</script> }

Now it's time to delete the cookies.

Registering GA cookies

In Kentico, we recommend managing cookies through cookie levels. When the application starts, you can register any cookie by its name (either a first-party or a third-party one). Each cookie is registered for a certain cookie level.

When a new visitor arrives at the website, the default cookie level is used and cookies of that level are deployed to the browser. When the visitor gives a certain consent, additional cookies are allowed (up to the cookie level bound to the given consent). Finally, when the visitor revokes a consent, the cookie level is lowered and all the respective cookies get automatically deleted (by the default Kentico.AspNet.Mvc NuGet package).

To register the cookies, add code to the Application_Start method in Global.asax.cs. First, create two helper methods.

internal static void RegisterCookieAsAll(string cookieName) => CookieHelper.RegisterCookie(cookieName, CookieLevel.All); private static void RegisterGoogleAnalyticsCookies() { foreach (var cookieName in ApplicationConfig.GoogleAnalyticsCookieNames) { RegisterCookieAsAll(cookieName); } }

Then, use them in the Application_Start method.

/// <summary> /// Occurs when application starts. /// </summary> protected void Application_Start() { // Enable and configure selected Kentico ASP.NET MVC integration features ApplicationConfig.RegisterFeatures(ApplicationBuilder.Current); BundleConfig.RegisterBundles(BundleTable.Bundles); // Register routes including system routes for enabled features RouteConfig.RegisterRoutes(RouteTable.Routes); // Registers implementation to the dependency resolver DependencyResolverConfig.Register(); RegisterGoogleAnalyticsCookies(); }

Also slightly edit the App_Start > Startup.Auth.cs file to meet the DRY principle.

// Register the authentication cookie in the Kentico application and set its cookie level. // This will ensure that the authentication cookie will not be removed when a user revokes the tracking consent. DancingGoatApplication.RegisterCookieAsAll(AUTHENTICATION_COOKIE_NAME);

Then, in App_Start > ApplicationConfig.cs, add code to furnish the specific cookie names.

public const string GoogleAnalyticsPropertyId = "UA-XXXXX-X"; public static string[] GoogleAnalyticsCookieNames => new string[14] { "_ga", "_gid", $"_gat_{GoogleAnalyticsPropertyId}", $"_dc_gtm_{GoogleAnalyticsPropertyId}", "AMP_TOKEN", $"_gac_{GoogleAnalyticsPropertyId}", "__utma", "__utmt", "__utmb", "__utmc", "__utmz", "__utmv", "__utmx", "__utmxx" };

Note: We're using GTM, hence the _dc_gtm_<property-id> name instead of _gat.

So far, so good. The registered cookies will be bound to the All cookie level.

However, Google prepared another small catch for us. Despite the fact that GA cookies, when deployed by the gtag.js script by Google, don't have the domain property set, you need to delete them using not only their names but also with the domain of your website. Since the requirements imposed by Google are non-standard, you should delete the cookies with a simple custom service that supplements the default Kentico functionality.

First, you need an interface (Services > ICookieService.cs).

/// <summary> /// Handles custom cookies. /// </summary> public interface ICookieService : IService { /// <summary> /// Removes custom cookies. /// </summary> /// <param name="cookieName">Cookie name.</param> /// <param name="domain">Cookie domain.</param> void RemoveCookie(string cookieName, string domain = null); /// <summary> /// Determines if a cookie exists in the request. /// </summary> /// <param name="cookieName">Cookie name.</param> /// <returns><see langword="true" /> if the cookie exists.</returns> bool CookieExists(string cookieName); }

Then, implement the interface in Services > CookieService.cs.

public class CookieService : ICookieService { HttpCookieCollection Cookies = CMSHttpContext.Current?.Response?.Cookies; public void RemoveCookie(string cookieName, string domain = null) { if (CookieExists(cookieName)) { var tempCookie = new HttpCookie(cookieName); tempCookie.Expires = DateTime.Now.AddYears(-1); tempCookie.Domain = !string.IsNullOrEmpty(domain) ? domain : Cookies[cookieName].Domain; Cookies.Add(tempCookie); } } public bool CookieExists(string cookieName) { foreach (var key in Cookies?.AllKeys) { if (key.Equals(cookieName, StringComparison.Ordinal)) { return true; } } return false; } }

By adding a duplicate cookie to the collection, the original cookie will get invalidated, thanks to the expiration date being set backwards.

In the ApplicationConfig.cs file, add a constant with the domain name, preceded with a dot.

public const string GoogleAnalyticsCookieDomain = "";

Last but not least, use the service in the PrivacyController class.

private readonly ICookieService mCookieService; ... public PrivacyController(IConsentAgreementService consentAgreementService, ICurrentCookieLevelProvider cookieLevelProvider, ICurrentUserContactProvider currentContactProvider, IWebFarmService webFarmService, ISiteService siteService, ICookieService cookieService) { mConsentAgreementService = consentAgreementService; mCookieLevelProvider = cookieLevelProvider; mCurrentContactProvider = currentContactProvider; mWebFarmService = webFarmService; mSiteService = siteService; mCookieService = cookieService; } ... [HttpPost] [ValidateAntiForgeryToken] public ActionResult Revoke(string consentName) { var consentToRevoke = ConsentInfoProvider.GetConsentInfo(consentName); if (consentToRevoke != null && CurrentContact != null) { mConsentAgreementService.Revoke(CurrentContact, consentToRevoke); if (consentName == TrackingConsentGenerator.CONSENT_NAME) { mCookieLevelProvider.SetCurrentCookieLevel(mCookieLevelProvider.GetDefaultCookieLevel()); foreach (var cookieName in ApplicationConfig.GoogleAnalyticsCookieNames) { mCookieService.RemoveCookie(cookieName, ApplicationConfig.GoogleAnalyticsCookieDomain); } ExecuteRevokeTrackingConsentTask(mSiteService.CurrentSite, CurrentContact); } TempData[SUCCESS_RESULT] = true; } else { TempData[ERROR_RESULT] = true; } return RedirectToAction("Index"); }


That's it! Now you have your Dancing Goat site set up with GA, nicely wrapped with GTM. The tracking snippets get added to the markup only when a visitor agrees to that. When they revoke their consent, the system forgets about the visitor. Collection of data is not only disabled through the property of the window object, as per Google's documentation. Also, the cookies are removed using a service.

Now that you know how to introduce GA to your site, you can look forward to seeing the final parts in this series. They will demonstrate how custom-tailored reports can be crafted in GA. You'll create two custom GA reports that display the same kind of information as what's in the Search engines and Invalid pages reports in Kentico Web Analytics.







Share this article on   LinkedIn

Jan Lenoch

I'm a developer trainer at Kentico. I create hands-on developer trainings of ASP.NET and Kentico Xperience. I listen to the voice of our customers and I improve our training courses on a continuous basis. I also give feedback to the internal teams about developers' experience with Xperience.