Optimizely CMS Analytics: Content Areas, Visitor Groups, | OpsBlu Docs

Optimizely CMS Analytics: Content Areas, Visitor Groups,

Implement analytics on Optimizely CMS (formerly Episerver). Covers Razor view script injection, Content Area tracking, Visitor Group data layers,...

Analytics Architecture on Optimizely CMS

Optimizely CMS (formerly Episerver) is a .NET-based enterprise CMS that uses Razor views for rendering, Content Areas for component-based layouts, and Visitor Groups for personalization. Analytics tracking integrates through:

  • Razor views (.cshtml files) control where scripts are injected. The _Layout.cshtml file provides global injection points, and partial views handle page-type-specific tracking
  • Content Areas render blocks (components) dynamically based on editor configuration. Each block can include its own tracking script for component-level analytics
  • Visitor Groups are Optimizely's personalization rules that show different content to different audiences. Tracking which Visitor Group rules fire enables personalization analytics
  • Optimizely Commerce (formerly Episerver Commerce) adds product catalogs, cart, and checkout with .NET events that hook into the purchase workflow

Optimizely CMS runs on ASP.NET Core (CMS 12+) or .NET Framework (CMS 11 and earlier). The analytics integration patterns differ slightly between versions, but the core concepts are the same.


Installing Tracking Scripts

Via Razor Layout

Add GTM to the shared layout file:

@* Views/Shared/_Layout.cshtml *@
<!DOCTYPE html>
<html>
<head>
    @RenderSection("Head", required: false)

    <!-- Google Tag Manager -->
    <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=
    'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
    })(window,document,'script','dataLayer','GTM-XXXXXX');</script>
</head>
<body>
    <!-- GTM noscript -->
    <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXX"
    height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>

    @RenderBody()
    @RenderSection("Scripts", required: false)
</body>
</html>

Environment-Based Loading

Use Optimizely's configuration to load different tracking IDs per environment:

@inject IConfiguration Configuration

@{
    var gtmId = Configuration["Analytics:GtmContainerId"];
    var isProduction = Configuration["Environment"] == "Production";
}

@if (isProduction && !string.IsNullOrEmpty(gtmId))
{
    <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=
    'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
    })(window,document,'script','dataLayer','@gtmId');</script>
}

Data Layer with Content Properties

Optimizely CMS pages are strongly typed content models. Push page properties to the data layer from the Razor view:

@* Views/StandardPage/Index.cshtml *@
@model StandardPageViewModel

<script>
    window.dataLayer = window.dataLayer || [];
    dataLayer.push({
        'page_type': '@Model.CurrentPage.GetOriginalType().Name',
        'content_id': '@Model.CurrentPage.ContentLink.ID',
        'content_name': '@Html.Raw(Model.CurrentPage.Name.Replace("'", "\\'"))',
        'language': '@Model.CurrentPage.Language.Name',
        'published_date': '@Model.CurrentPage.StartPublish?.ToString("yyyy-MM-dd")',
        'content_area_count': @(Model.CurrentPage.MainContentArea?.FilteredItems?.Count() ?? 0)
    });
</script>

Data Layer from a View Component

For a reusable analytics component across all page types:

// ViewComponents/AnalyticsDataLayerViewComponent.cs
public class AnalyticsDataLayerViewComponent : ViewComponent
{
    private readonly IContentLoader _contentLoader;
    private readonly IPageRouteHelper _pageRouteHelper;

    public AnalyticsDataLayerViewComponent(
        IContentLoader contentLoader,
        IPageRouteHelper pageRouteHelper)
    {
        _contentLoader = contentLoader;
        _pageRouteHelper = pageRouteHelper;
    }

    public IViewComponentResult Invoke()
    {
        var currentPage = _pageRouteHelper.Page;
        if (currentPage == null) return Content(string.Empty);

        var model = new AnalyticsModel
        {
            PageType = currentPage.GetOriginalType().Name,
            ContentId = currentPage.ContentLink.ID,
            PageName = currentPage.Name,
            Language = currentPage.Language.Name,
            PublishDate = (currentPage as IVersionable)?.StartPublish?.ToString("yyyy-MM-dd")
        };

        return View(model);
    }
}

Use it in the layout:

@* _Layout.cshtml *@
@await Component.InvokeAsync("AnalyticsDataLayer")

Content Area Block Tracking

Optimizely's Content Areas render editor-configured blocks. Track which blocks are displayed and their position:

@* Views/Shared/Blocks/_BlockWrapper.cshtml *@
@model IContent

<div class="block-wrapper"
     data-block-type="@Model.GetOriginalType().Name"
     data-block-id="@Model.ContentLink.ID"
     data-block-name="@Model.Name">
    @Html.PropertyFor(m => m)
</div>

<script>
    window.dataLayer = window.dataLayer || [];
    dataLayer.push({
        'event': 'block_impression',
        'block_type': '@Model.GetOriginalType().Name',
        'block_id': '@Model.ContentLink.ID',
        'block_name': '@Html.Raw(Model.Name.Replace("'", "\\'"))'
    });
</script>

For visibility-based tracking, use an IntersectionObserver on block wrappers rather than firing events on render. This prevents inflating impression counts for blocks below the fold.


Visitor Group Analytics

Visitor Groups are Optimizely's personalization engine. Track which groups matched the current visitor to correlate personalization with conversion data:

// Middleware or ViewComponent
public class VisitorGroupAnalyticsViewComponent : ViewComponent
{
    private readonly IVisitorGroupRepository _visitorGroupRepo;
    private readonly IVisitorGroupRoleRepository _roleRepo;

    public IViewComponentResult Invoke()
    {
        var matchedGroups = new List<string>();
        var allGroups = _visitorGroupRepo.List();

        foreach (var group in allGroups)
        {
            if (HttpContext.User.IsInRole(group.Name))
            {
                matchedGroups.Add(group.Name);
            }
        }

        ViewBag.MatchedGroups = string.Join(",", matchedGroups);
        return View();
    }
}
@* Views/Shared/Components/VisitorGroupAnalytics/Default.cshtml *@
<script>
    window.dataLayer = window.dataLayer || [];
    dataLayer.push({
        'visitor_groups': '@ViewBag.MatchedGroups',
        'is_personalized': @(string.IsNullOrEmpty(ViewBag.MatchedGroups) ? "false" : "true")
    });
</script>

Optimizely Commerce Tracking

For sites using Optimizely Commerce (B2C or B2B Commerce), track ecommerce events through the Commerce API events:

// Services/CommerceAnalyticsService.cs
[ServiceConfiguration(typeof(ICommerceAnalyticsService))]
public class CommerceAnalyticsService : ICommerceAnalyticsService
{
    public string GeneratePurchaseDataLayer(IPurchaseOrder order)
    {
        var items = order.GetAllLineItems().Select(li => new {
            item_id = li.Code,
            item_name = li.DisplayName,
            price = li.PlacedPrice,
            quantity = (int)li.Quantity
        });

        var data = new {
            @event = "purchase",
            ecommerce = new {
                transaction_id = order.OrderNumber,
                value = order.GetTotal().Amount,
                currency = order.Currency.CurrencyCode,
                tax = order.GetTaxTotal().Amount,
                shipping = order.GetShippingTotal().Amount,
                items
            }
        };

        return System.Text.Json.JsonSerializer.Serialize(data);
    }
}

Output on the order confirmation page:

@* Views/Checkout/OrderConfirmation.cshtml *@
@inject ICommerceAnalyticsService AnalyticsService

<script>
    window.dataLayer = window.dataLayer || [];
    dataLayer.push(@Html.Raw(AnalyticsService.GeneratePurchaseDataLayer(Model.Order)));
</script>

Common Errors

Error Cause Fix
Data layer values HTML-encoded Razor auto-escapes output with @ syntax Use @Html.Raw() for JSON output, but sanitize input to prevent XSS
Scripts missing in edit mode Layout conditional skips analytics in CMS preview Check !PageEditing.PageIsInEditMode before injecting production scripts
Content Area blocks fire events on every render No deduplication for blocks rendered multiple times Track block impressions with IntersectionObserver, not inline scripts
Visitor Group data empty Groups evaluated after template renders Move group evaluation to middleware that runs before the view
Commerce events fire for test orders Development orders trigger purchase events Check environment configuration or order status before pushing events
Localized content sends wrong language Using browser language instead of content language Use currentPage.Language.Name from the content model, not Request.Headers
GTM not loading on specific page types Custom page controllers do not use the shared layout Ensure all controllers return views that reference _Layout.cshtml
Slow page load from inline scripts Multiple data layer pushes in synchronous script blocks Combine all pushes into a single script block at the end of <body>
Block tracking counts hidden blocks Collapsed or conditionally hidden Content Area blocks still render Check offsetParent !== null or use display CSS property before tracking

Performance Considerations

  • Output caching: Optimizely's output cache serves pre-rendered HTML. Data layer values in cached pages reflect the first render. Use donut caching or client-side APIs for user-specific data
  • View Component overhead: Analytics view components add server-side processing time. Keep them lightweight and avoid database queries in the Invoke method
  • Script bundling: Use ASP.NET Core's bundling and minification for analytics scripts. Avoid inline scripts in every partial view; consolidate in the layout
  • CDN integration: Optimizely DXP Cloud (formerly DXC) uses a CDN for static assets. External analytics scripts load from their own CDNs and are not affected by Optimizely's asset pipeline
  • Block rendering: Content Areas with many blocks increase DOM size and script execution time. Lazy-load blocks below the fold with JavaScript rather than rendering all blocks server-side