Sitefinity: Analytics Implementation Guide | OpsBlu Docs

Sitefinity: Analytics Implementation Guide

Technical guide to implementing analytics on Sitefinity CMS, covering widget designers, Razor views, Script Manager, page templates, and Sitefinity...

Analytics Architecture on Sitefinity

Sitefinity is an ASP.NET-based CMS by Progress Software. Pages are built from widgets (server-side controls) arranged in page templates via a drag-and-drop editor. The rendering pipeline follows the ASP.NET MVC/Web Forms lifecycle, with Sitefinity's own page resolution and widget rendering layer on top.

The request flow:

Request → IIS → ASP.NET Pipeline → Sitefinity Route Handler → Page Template → Widget Rendering → HTML

Sitefinity provides a Script Manager widget and page template system for injecting scripts. It also has a built-in analytics product called Sitefinity Insight (formerly Sitefinity Digital Experience Cloud) that collects behavioral data natively.

The platform supports both MVC (Razor) and Web Forms rendering modes. MVC is the default for new projects. All code examples below use the MVC/Razor approach.


Installing Tracking Scripts

Method 1: Script Manager Widget

Sitefinity includes a built-in Script Manager widget that content editors can configure through the admin UI:

  1. Navigate to Administration > Settings > Advanced > Appearance > Frontend scripts
  2. Add a new script entry with the GTM URL

Or configure programmatically:

// Global.asax.cs or via Sitefinity module
protected void Application_Start()
{
    SystemManager.ApplicationStart += (sender, args) =>
    {
        // Register script via the ScriptsConfig
        var config = Config.Get<AppearanceConfig>();
        config.FrontendScripts.Add(new ScriptConfig
        {
            Url = "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX",
            LoadAsync = true,
            Position = ScriptPosition.Head
        });
    };
}

Method 2: Page Template (Razor Layout)

Inject scripts directly in the Razor layout file:

@* ~/Mvc/Views/Layouts/_Master.cshtml *@
<!DOCTYPE html>
<html lang="@System.Threading.Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName">
<head>
    <meta charset="UTF-8" />
    @Html.Section("head")

    <script>
        window.dataLayer = window.dataLayer || [];
    </script>
    <script async src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX"></script>

    @Html.Action("HeadIncludes", "SitefinityPages")
</head>
<body>
    @RenderBody()

    <noscript>
        <iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXX"
                height="0" width="0" style="display:none;visibility:hidden"></iframe>
    </noscript>

    @Html.Action("BottomIncludes", "SitefinityPages")
</body>
</html>

Method 3: Custom Widget

Create a Sitefinity MVC widget that outputs the analytics script. This gives content managers control over placement:

// Controllers/AnalyticsWidgetController.cs
[ControllerToolboxItem(Name = "AnalyticsWidget",
    Title = "Analytics Tracking",
    SectionName = "Scripts & Analytics")]
public class AnalyticsWidgetController : Controller
{
    public ActionResult Index()
    {
        var model = new AnalyticsWidgetModel
        {
            GtmId = Config.Get<AnalyticsConfig>().GtmContainerId,
            Enabled = !SystemManager.IsDesignMode && !SystemManager.IsPreviewMode
        };
        return View("Default", model);
    }
}
@* Views/AnalyticsWidget/Default.cshtml *@
@model AnalyticsWidgetModel

@if (Model.Enabled && !string.IsNullOrEmpty(Model.GtmId))
{
    <script async src="https://www.googletagmanager.com/gtm.js?id=@Model.GtmId"></script>
}

Method 4: Widget Designer for Editor-Configurable Tracking

Add a designer interface so editors can configure the tracking ID per widget instance:

// Designers/AnalyticsWidgetDesigner.cs
public class AnalyticsWidgetDesigner : DesignerBase
{
    public override string DesignerTemplatePath =>
        "~/AnalyticsWidget/AnalyticsWidgetDesigner.ascx";

    [Category("Analytics")]
    [DisplayName("GTM Container ID")]
    public string GtmId { get; set; }
}

Data Layer Implementation

Razor View Data Layer

Build the data layer in the layout using Sitefinity's page and content APIs:

@* In _Master.cshtml or a partial *@
@{
    var pageNode = SiteMapBase.GetActualCurrentNode();
    var page = pageNode as PageSiteNode;
}

<script>
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
        platform: 'sitefinity',
        pageId: '@(page?.Id.ToString() ?? "")',
        pageTitle: '@Html.Raw(HttpUtility.JavaScriptStringEncode(page?.Title ?? ""))',
        pageUrl: '@HttpUtility.JavaScriptStringEncode(page?.Url ?? "")',
        pageTemplate: '@HttpUtility.JavaScriptStringEncode(page?.GetPageData()?.Template?.Name ?? "")',
        language: '@System.Threading.Thread.CurrentThread.CurrentUICulture.Name',
        authenticated: @(ClaimsManager.GetCurrentIdentity().IsAuthenticated.ToString().ToLower())
    });
</script>

Content-Type Specific Data Layer

For detail pages (blog posts, news articles, product pages), extend the data layer with content-specific fields:

// In a custom widget controller or page action filter
public class DataLayerActionFilter : ActionFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        var manager = ManagerBase<ContentItem>.GetManager();
        var detailItem = filterContext.HttpContext.Items["detailItem"] as DynamicContent;

        if (detailItem != null)
        {
            var dataLayer = new Dictionary<string, object>
            {
                { "contentId", detailItem.Id.ToString() },
                { "contentType", detailItem.GetType().Name },
                { "contentTitle", detailItem.GetValue("Title")?.ToString() ?? "" },
                { "contentPublishDate", detailItem.PublicationDate.ToString("o") },
                { "contentAuthor", detailItem.GetValue("Author")?.ToString() ?? "" }
            };

            // Taxonomy fields
            var categories = detailItem.GetValue<TrackedList<Guid>>("Category");
            if (categories != null)
            {
                var taxManager = TaxonomyManager.GetManager();
                var categoryNames = categories
                    .Select(id => taxManager.GetTaxon(id)?.Title ?? "")
                    .Where(t => !string.IsNullOrEmpty(t))
                    .ToList();
                dataLayer["categories"] = categoryNames;
            }

            filterContext.Controller.ViewBag.ContentDataLayer = dataLayer;
        }

        base.OnResultExecuting(filterContext);
    }
}

Render in the view:

@if (ViewBag.ContentDataLayer != null)
{
    <script>
        window.dataLayer = window.dataLayer || [];
        window.dataLayer.push(@Html.Raw(JsonConvert.SerializeObject(ViewBag.ContentDataLayer)));
    </script>
}

Sitefinity Insight Integration

Sitefinity Insight is the platform's native analytics and personalization engine. It collects data via its own JavaScript tracker. If you run Insight alongside a third-party analytics platform, bridge the data:

// Bridge Sitefinity Insight personas/segments to dataLayer
if (window.DataIntelligenceSubmitScript) {
    // Insight exposes the current persona after evaluation
    document.addEventListener('sf-insight-ready', function() {
        var persona = window.sfInsight?.getCurrentPersona();
        var segment = window.sfInsight?.getCurrentSegment();

        window.dataLayer = window.dataLayer || [];
        window.dataLayer.push({
            event: 'insight_persona',
            personaName: persona?.name || '',
            segmentName: segment?.name || ''
        });
    });
}

Insight also provides server-side APIs for accessing contact data:

// Access Insight data in a widget controller
var contactId = DataIntelligenceManager.GetCurrentContactId();
if (contactId != Guid.Empty)
{
    var contact = DataIntelligenceManager.GetContact(contactId);
    // Add contact-level data to the data layer
}

Common Issues

Design Mode / Preview Mode Firing Analytics

Sitefinity's page editor loads the page in design mode. Analytics scripts execute in the editor, sending false data.

Check the system mode before rendering:

// In widget controller
bool isLive = !SystemManager.IsDesignMode
           && !SystemManager.IsPreviewMode
           && !SystemManager.IsInlineEditingMode;
@* In Razor view *@
@if (!SystemManager.IsDesignMode && !SystemManager.IsPreviewMode)
{
    <script async src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX"></script>
}

Output Cache Serving Stale Data Layer

Sitefinity uses ASP.NET output caching. Cached pages serve the same HTML (including data layer) to all users. For pages with user-specific data layer values, either:

  1. Disable output cache for those pages:
[OutputCache(Duration = 0, VaryByParam = "none")]
  1. Use VaryByCustom to cache per authenticated state:
[OutputCache(Duration = 300, VaryByCustom = "authenticated")]
// Global.asax.cs
public override string GetVaryByCustomString(HttpContext context, string custom)
{
    if (custom == "authenticated")
        return context.User.Identity.IsAuthenticated.ToString();
    return base.GetVaryByCustomString(context, custom);
}
  1. Fetch dynamic data client-side via the Sitefinity Web Services API.

Widget Not Rendering After Deployment

If a custom analytics widget does not appear after deployment, check:

  • The widget assembly is in the bin/ folder
  • The ControllerToolboxItem attribute has the correct SectionName
  • The Sitefinity toolbox is refreshed (Administration > Backend pages > Toolbox)
  • The widget's Razor view is in the correct path under Mvc/Views/

Multi-Site Tracking ID Conflicts

Sitefinity supports multi-site configurations. Each site may need a different GTM container. Store tracking IDs in Sitefinity's configuration per site:

// Read site-specific config
var currentSite = SystemManager.CurrentContext.CurrentSite;
var siteConfig = Config.Get<AnalyticsConfig>(currentSite.Id);
var gtmId = siteConfig.GtmContainerId;

Platform-Specific Considerations

ASP.NET MVC vs. Web Forms -- Sitefinity supports both rendering modes. MVC widgets use Razor views and controllers. Web Forms widgets use UserControls (.ascx). The analytics implementation patterns differ: MVC uses @Html.Raw() for JSON output while Web Forms uses <%= %> server tags. Stick with MVC for new projects.

Page templates and layout files -- Sitefinity page templates map to Razor layout files. The base layout (_Master.cshtml) is the equivalent of a <head>/<body> wrapper. Place analytics scripts here to ensure they load on every page that uses the template. Different templates can use different layouts, so verify all active templates include analytics.

Personalization and Insight -- Sitefinity Insight evaluates visitor segments and personas server-side, then applies personalization rules to widget visibility and content. If a widget is hidden by a personalization rule, any analytics events from that widget will not fire. Account for this when analyzing engagement data -- missing events may reflect personalization rules, not user behavior.

Sitefinity Cloud vs. self-hosted -- Sitefinity Cloud is the managed hosting option. It restricts access to IIS configuration and the file system. Analytics scripts should be deployed via the CMS admin (Script Manager or custom widgets) rather than by editing layout files directly on disk. Self-hosted installations allow direct file system access.

Content modules and dynamic types -- Sitefinity's dynamic module builder creates custom content types at runtime. These types may not have consistent property names across installations. When building data layer logic for dynamic content, use reflection or the DynamicContent.GetValue() API rather than hardcoding property names.