GTM Data Layer Implementation on Sitecore | Blue Frog Docs

GTM Data Layer Implementation on Sitecore

Implement GTM data layer on Sitecore with page data, user information, Sitecore context, and dynamic content

GTM Data Layer Implementation on Sitecore

Learn how to implement a robust Google Tag Manager data layer on Sitecore that captures page information, user data, Sitecore context, and dynamic content for powerful tag management.

Understanding the Data Layer

The data layer is a JavaScript object that holds information about the page, user, and interactions. GTM uses this data to fire tags conditionally and pass variables.

Basic Structure:

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
    'event': 'pageview',
    'pageName': 'Home',
    'pageType': 'Landing Page'
});

Prerequisites

Basic Page Data Layer

Initialize Data Layer Before GTM

Always initialize the data layer before GTM loads:

@* /Views/Shared/_Layout.cshtml *@
<!DOCTYPE html>
<html>
<head>
    @* Initialize data layer FIRST *@
    <script>
        window.dataLayer = window.dataLayer || [];
    </script>

    @* Then load GTM *@
    <script>(function(w,d,s,l,i){...})(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
</head>

Sitecore Page Context Data Layer

Capture core Sitecore page information:

@using Sitecore.Data.Items
@using Sitecore.Links

@{
    var currentItem = Sitecore.Context.Item;
    var language = Sitecore.Context.Language.Name;
    var siteName = Sitecore.Context.Site.Name;
}

<script>
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
        'event': 'pageview',
        'page': {
            'id': '@currentItem.ID.ToString()',
            'name': '@currentItem.Name',
            'displayName': '@currentItem.DisplayName',
            'path': '@currentItem.Paths.FullPath',
            'url': '@LinkManager.GetItemUrl(currentItem)',
            'template': {
                'id': '@currentItem.TemplateID.ToString()',
                'name': '@currentItem.TemplateName'
            },
            'language': '@language',
            'version': '@currentItem.Version.Number'
        },
        'site': {
            'name': '@siteName',
            'language': '@language'
        }
    });
</script>

Helper Class for Data Layer

Create a reusable helper for consistent data layer implementation:

// /Helpers/DataLayerHelper.cs
using Newtonsoft.Json;
using Sitecore.Data.Items;
using Sitecore.Links;
using System.Collections.Generic;

namespace YourProject.Helpers
{
    public static class DataLayerHelper
    {
        public static string GetPageDataJson()
        {
            var currentItem = Sitecore.Context.Item;

            if (currentItem == null)
                return "{}";

            var pageData = new
            {
                page = new
                {
                    id = currentItem.ID.ToString(),
                    name = currentItem.Name,
                    displayName = currentItem.DisplayName,
                    path = currentItem.Paths.FullPath,
                    url = LinkManager.GetItemUrl(currentItem),
                    template = new
                    {
                        id = currentItem.TemplateID.ToString(),
                        name = currentItem.TemplateName
                    },
                    language = Sitecore.Context.Language.Name,
                    version = currentItem.Version.Number,
                    created = currentItem.Statistics.Created.ToString("yyyy-MM-dd"),
                    updated = currentItem.Statistics.Updated.ToString("yyyy-MM-dd")
                },
                site = new
                {
                    name = Sitecore.Context.Site?.Name,
                    language = Sitecore.Context.Language.Name,
                    country = Sitecore.Context.Language.CultureInfo.TwoLetterISOLanguageName
                }
            };

            return JsonConvert.SerializeObject(pageData);
        }

        public static string GetUserDataJson()
        {
            var userData = new Dictionary<string, object>
            {
                ["isAuthenticated"] = Sitecore.Context.User?.IsAuthenticated ?? false,
                ["userName"] = Sitecore.Context.User?.IsAuthenticated == true ?
                    Sitecore.Context.User.LocalName : "anonymous"
            };

            // Add xDB data if available
            if (Sitecore.Analytics.Tracker.Current != null &&
                Sitecore.Analytics.Tracker.Current.IsActive)
            {
                var contact = Sitecore.Analytics.Tracker.Current.Contact;
                if (contact != null)
                {
                    userData["contactId"] = contact.ContactId.ToString("N");
                    userData["isNewVisitor"] = contact.System.VisitCount == 0;
                    userData["visitNumber"] = contact.System.VisitCount;
                }
            }

            return JsonConvert.SerializeObject(userData);
        }

        public static string GetCustomFieldsJson(Item item, params string[] fieldNames)
        {
            var fields = new Dictionary<string, string>();

            foreach (var fieldName in fieldNames)
            {
                var field = item.Fields[fieldName];
                if (field != null && !string.IsNullOrEmpty(field.Value))
                {
                    fields[fieldName] = field.Value;
                }
            }

            return JsonConvert.SerializeObject(fields);
        }
    }
}

Use in Razor:

@using YourProject.Helpers

<script>
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push(@Html.Raw(DataLayerHelper.GetPageDataJson()));
</script>

User and Authentication Data

Track user status and authentication:

@using Sitecore.Security.Accounts

@{
    var user = Sitecore.Context.User;
    var isAuthenticated = user?.IsAuthenticated ?? false;
    var userName = isAuthenticated ? user.LocalName : "anonymous";
    var userRoles = isAuthenticated ? string.Join(",", user.Roles.Select(r => r.Name)) : "";
}

<script>
    window.dataLayer.push({
        'user': {
            'isAuthenticated': @isAuthenticated.ToString().ToLower(),
            'userName': '@userName',
            'userRoles': '@userRoles',
            'userId': '@(isAuthenticated ? user.Profile.Email : "")'
        }
    });
</script>

Integration with Sitecore xDB

Capture Sitecore Analytics data:

@using Sitecore.Analytics

@{
    string contactId = null;
    int visitNumber = 0;
    bool isNewVisitor = true;
    string engagementValue = "0";

    if (Tracker.Current != null && Tracker.Current.IsActive)
    {
        var contact = Tracker.Current.Contact;
        if (contact != null)
        {
            contactId = contact.ContactId.ToString("N");
            visitNumber = contact.System.VisitCount;
            isNewVisitor = visitNumber == 0;
        }

        var interaction = Tracker.Current.Session?.Interaction;
        if (interaction != null)
        {
            engagementValue = interaction.Value.ToString();
        }
    }
}

<script>
    window.dataLayer.push({
        'sitecore': {
            'contactId': '@contactId',
            'visitNumber': @visitNumber,
            'isNewVisitor': @isNewVisitor.ToString().ToLower(),
            'engagementValue': @engagementValue
        }
    });
</script>

Page Category and Taxonomy

Organize pages by category and taxonomy:

@{
    var currentItem = Sitecore.Context.Item;

    // Get category from field or parent structure
    var category = currentItem["Category"];
    var subcategory = currentItem["Subcategory"];

    // Determine page type from template
    var pageType = GetPageType(currentItem.TemplateName);

    // Build breadcrumb from path
    var breadcrumb = GetBreadcrumb(currentItem);
}

<script>
    window.dataLayer.push({
        'page': {
            'category': '@category',
            'subcategory': '@subcategory',
            'type': '@pageType',
            'breadcrumb': '@breadcrumb'
        }
    });
</script>

@functions {
    string GetPageType(string templateName)
    {
        // Map template names to page types
        var mapping = new Dictionary<string, string>
        {
            { "Article Page", "article" },
            { "Landing Page", "landing" },
            { "Product Page", "product" },
            { "Category Page", "category" }
        };

        return mapping.ContainsKey(templateName) ? mapping[templateName] : "standard";
    }

    string GetBreadcrumb(Item item)
    {
        var ancestors = item.Axes.GetAncestors()
            .Where(i => i.Paths.IsContentItem)
            .Select(i => i.DisplayName);

        return string.Join(" > ", ancestors.Concat(new[] { item.DisplayName }));
    }
}

E-commerce Data Layer

For Sitecore Commerce implementations:

@using Sitecore.Commerce.Engine.Connect.Entities

@{
    var cart = GetCurrentCart(); // Your method to get cart
}

@if (cart != null && cart.Lines.Any())
{
    <script>
        window.dataLayer.push({
            'ecommerce': {
                'cart': {
                    'total': @cart.Total.Amount,
                    'currency': '@cart.Total.CurrencyCode',
                    'itemCount': @cart.Lines.Count,
                    'items': [
                        @foreach (var line in cart.Lines)
                        {
                            <text>
                            {
                                'item_id': '@line.Product.ProductId',
                                'item_name': '@line.Product.DisplayName',
                                'price': @line.Product.Price.Amount,
                                'quantity': @line.Quantity
                            },
                            </text>
                        }
                    ]
                }
            }
        });
    </script>
}

Dynamic Content Tracking

Track personalization and dynamic content:

@using Sitecore.Mvc.Analytics.Extensions

@{
    var personalizationData = Html.Sitecore().PersonalizationData();
    var isPersonalized = personalizationData != null;
    var ruleName = personalizationData?.RuleName ?? "default";
}

<script>
    window.dataLayer.push({
        'personalization': {
            'isActive': @isPersonalized.ToString().ToLower(),
            'ruleName': '@ruleName',
            'componentName': '@Model.RenderingItem?.Name'
        }
    });
</script>

Campaign and Source Tracking

Track campaign parameters and traffic sources:

@{
    var utmSource = Request.QueryString["utm_source"];
    var utmMedium = Request.QueryString["utm_medium"];
    var utmCampaign = Request.QueryString["utm_campaign"];
    var utmContent = Request.QueryString["utm_content"];
    var utmTerm = Request.QueryString["utm_term"];
}

@if (!string.IsNullOrEmpty(utmSource))
{
    <script>
        window.dataLayer.push({
            'campaign': {
                'source': '@utmSource',
                'medium': '@utmMedium',
                'name': '@utmCampaign',
                'content': '@utmContent',
                'term': '@utmTerm'
            }
        });
    </script>
}

Form Tracking Data Layer

Track Sitecore Forms with data layer:

// /scripts/forms/form-tracking.js
document.addEventListener('DOMContentLoaded', function() {
    var sitecoreForms = document.querySelectorAll('form[data-sc-fxb-form]');

    sitecoreForms.forEach(function(form) {
        var formName = form.getAttribute('data-sc-fxb-form') || 'Unknown Form';
        var formId = form.id;

        // Track form view
        window.dataLayer.push({
            'event': 'formView',
            'form': {
                'name': formName,
                'id': formId,
                'location': window.location.pathname
            }
        });

        // Track form start
        var formStarted = false;
        form.querySelectorAll('input, textarea, select').forEach(function(input) {
            input.addEventListener('focus', function() {
                if (!formStarted) {
                    formStarted = true;
                    window.dataLayer.push({
                        'event': 'formStart',
                        'form': {
                            'name': formName,
                            'id': formId
                        }
                    });
                }
            }, { once: true });
        });

        // Track form submission
        form.addEventListener('submit', function() {
            window.dataLayer.push({
                'event': 'formSubmit',
                'form': {
                    'name': formName,
                    'id': formId,
                    'location': window.location.pathname
                }
            });
        });
    });
});

Error Tracking Data Layer

Track errors and exceptions:

@if (ViewBag.Error != null)
{
    <script>
        window.dataLayer.push({
            'event': 'error',
            'error': {
                'type': '@ViewBag.Error.Type',
                'message': '@ViewBag.Error.Message',
                'page': window.location.pathname
            }
        });
    </script>
}

JavaScript error tracking:

// /scripts/analytics/error-tracking.js
window.addEventListener('error', function(e) {
    window.dataLayer.push({
        'event': 'javascriptError',
        'error': {
            'message': e.message,
            'filename': e.filename,
            'lineno': e.lineno,
            'colno': e.colno,
            'stack': e.error ? e.error.stack : ''
        }
    });
});

Search Results Data Layer

Track search interactions:

@* Search results page *@
@{
    var searchTerm = Request.QueryString["q"];
    var resultsCount = Model.SearchResults?.Count ?? 0;
}

@if (!string.IsNullOrEmpty(searchTerm))
{
    <script>
        window.dataLayer.push({
            'event': 'search',
            'search': {
                'term': '@searchTerm',
                'resultsCount': @resultsCount,
                'hasResults': @(resultsCount > 0 ? "true" : "false")
            }
        });
    </script>
}

Video Tracking Data Layer

Track video interactions:

// /scripts/analytics/video-tracking.js
document.querySelectorAll('video').forEach(function(video) {
    var videoName = video.getAttribute('data-video-name') || video.src;
    var tracked = {
        play: false,
        progress: {
            '25': false,
            '50': false,
            '75': false,
            '100': false
        }
    };

    video.addEventListener('play', function() {
        if (!tracked.play) {
            tracked.play = true;
            window.dataLayer.push({
                'event': 'videoPlay',
                'video': {
                    'name': videoName,
                    'duration': video.duration,
                    'currentTime': video.currentTime
                }
            });
        }
    });

    video.addEventListener('timeupdate', function() {
        var percentComplete = Math.floor((video.currentTime / video.duration) * 100);

        ['25', '50', '75', '100'].forEach(function(milestone) {
            if (percentComplete >= parseInt(milestone) && !tracked.progress[milestone]) {
                tracked.progress[milestone] = true;
                window.dataLayer.push({
                    'event': 'videoProgress',
                    'video': {
                        'name': videoName,
                        'percent': milestone
                    }
                });
            }
        });
    });
});

SXA-Specific Data Layer

For SXA sites, capture component data:

@using Sitecore.XA.Foundation.Mvc.Extensions

@{
    var pageDesign = Html.Sxa().PageDesign();
    var siteSettings = Html.Sxa().SiteSettings;
    var theme = Html.Sxa().Theme();
}

<script>
    window.dataLayer.push({
        'sxa': {
            'theme': '@theme?.Name',
            'pageDesign': '@pageDesign?.Name',
            'siteName': '@siteSettings?.GetItem()?.Name'
        }
    });
</script>

JSS/Headless Data Layer

For Sitecore JSS applications:

// /src/lib/dataLayer.ts
import { RouteData } from '@sitecore-jss/sitecore-jss-nextjs';

export const pushPageView = (routeData: RouteData) => {
  if (typeof window !== 'undefined' && window.dataLayer) {
    window.dataLayer.push({
      event: 'pageview',
      page: {
        id: routeData.itemId,
        name: routeData.name,
        path: routeData.itemPath,
        template: routeData.templateName,
        language: routeData.itemLanguage,
      },
    });
  }
};

export const pushEvent = (eventName: string, data?: Record<string, any>) => {
  if (typeof window !== 'undefined' && window.dataLayer) {
    window.dataLayer.push({
      event: eventName,
      ...data,
    });
  }
};

Use in Next.js page:

// /src/pages/[[...path]].tsx
import { pushPageView } from '@/lib/dataLayer';

const SitecorePage = ({ layoutData }: SitecorePageProps): JSX.Element => {
  useEffect(() => {
    if (layoutData?.sitecore?.route) {
      pushPageView(layoutData.sitecore.route);
    }
  }, [layoutData]);

  return <Layout layoutData={layoutData} />;
};

Multi-Language Data Layer

Track language-specific data:

@{
    var language = Sitecore.Context.Language;
    var languageName = language.Name;
    var languageCode = language.CultureInfo.TwoLetterISOLanguageName;
    var languageDirection = language.CultureInfo.TextInfo.IsRightToLeft ? "rtl" : "ltr";
}

<script>
    window.dataLayer.push({
        'language': {
            'code': '@languageCode',
            'name': '@languageName',
            'direction': '@languageDirection',
            'displayName': '@language.CultureInfo.DisplayName'
        }
    });
</script>

Performance Metrics Data Layer

Track Core Web Vitals:

// /scripts/analytics/performance-tracking.js
// Track LCP
new PerformanceObserver((list) => {
    const entries = list.getEntries();
    const lastEntry = entries[entries.length - 1];

    window.dataLayer.push({
        'event': 'webVitals',
        'metric': {
            'name': 'LCP',
            'value': Math.round(lastEntry.renderTime || lastEntry.loadTime),
            'rating': lastEntry.renderTime < 2500 ? 'good' : 'poor'
        }
    });
}).observe({ type: 'largest-contentful-paint', buffered: true });

// Track CLS
new PerformanceObserver((list) => {
    let clsValue = 0;
    list.getEntries().forEach((entry) => {
        if (!entry.hadRecentInput) {
            clsValue += entry.value;
        }
    });

    window.dataLayer.push({
        'event': 'webVitals',
        'metric': {
            'name': 'CLS',
            'value': clsValue,
            'rating': clsValue < 0.1 ? 'good' : 'poor'
        }
    });
}).observe({ type: 'layout-shift', buffered: true });

Data Layer Debugging

Debug in Browser Console

// Log all data layer pushes
var originalPush = window.dataLayer.push;
window.dataLayer.push = function() {
    console.log('DataLayer Push:', arguments);
    return originalPush.apply(this, arguments);
};

// View current dataLayer state
console.table(window.dataLayer);

Sitecore Debugging

// Log data layer to Sitecore log
using Sitecore.Diagnostics;

var dataLayerJson = DataLayerHelper.GetPageDataJson();
Log.Info($"Data Layer: {dataLayerJson}", this);

Best Practices

1. Initialize Early

Always initialize dataLayer before GTM loads:

window.dataLayer = window.dataLayer || [];

2. Use Consistent Naming

Follow a naming convention:

  • Use camelCase: pageName, userId
  • Use descriptive names: productId not id
  • Group related data: page.name, user.id

3. Avoid PII

Don't include personally identifiable information:

// Bad
'email': 'user@example.com'

// Good
'hasEmail': true

4. Use Events

Push events for user actions:

window.dataLayer.push({
    'event': 'buttonClick',
    'buttonName': 'Subscribe'
});

5. Clear E-commerce Data

Clear e-commerce objects before pushing new data:

window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
    'event': 'purchase',
    'ecommerce': { /* new data */ }
});

Testing Data Layer

1. GTM Preview Mode

Use GTM Preview to see dataLayer variables in real-time

2. Browser Extensions

  • Google Tag Assistant: Verify dataLayer structure
  • dataLayer Inspector: Chrome extension for debugging

3. Console Testing

// Test dataLayer structure
console.log(window.dataLayer);

// Find specific events
window.dataLayer.filter(item => item.event === 'pageview');

Next Steps

// SYS.FOOTER