Analytics Architecture on SharePoint
SharePoint provides two distinct page architectures with different analytics injection methods:
Modern Pages (SharePoint Online / SharePoint 2019+):
- Built on the SharePoint Framework (SPFx)
- Analytics code deployed via SPFx Application Customizers (global scripts) or SPFx Web Parts (page-level)
- No direct
<head>or template editing - All customizations packaged as SPFx solutions (.sppkg)
Classic Pages (SharePoint 2013/2016/on-prem):
- Master pages and page layouts control HTML output
- Direct
<head>injection via master page editing orScriptLinkcustom actions - UserCustomAction API for deploying scripts without master page changes
For Modern SharePoint (the current standard), Application Customizers are the primary method for global analytics deployment. They inject code into every page via the Top and Bottom placeholders.
SPFx Solution (.sppkg)
└── Application Customizer
├── Top Placeholder → <head> equivalent (above page content)
└── Bottom Placeholder → Before </body> equivalent
Installing Tracking Scripts
Method 1: SPFx Application Customizer (Modern Pages - Recommended)
Create an SPFx Application Customizer to inject GTM on every Modern page.
Scaffold the project:
yo @microsoft/sharepoint
# Select "Extension" → "Application Customizer"
# Name: analytics-customizer
src/extensions/analytics/AnalyticsApplicationCustomizer.ts:
import { override } from '@microsoft/decorators';
import {
BaseApplicationCustomizer,
PlaceholderContent,
PlaceholderName
} from '@microsoft/sp-application-base';
export interface IAnalyticsProperties {
gtmId: string;
}
export default class AnalyticsApplicationCustomizer
extends BaseApplicationCustomizer<IAnalyticsProperties> {
private _topPlaceholder: PlaceholderContent | undefined;
@override
public onInit(): Promise<void> {
// Inject GTM script into page head
const gtmId = this.properties.gtmId;
if (gtmId) {
this._injectGtm(gtmId);
}
// Build data layer from SharePoint context
this._buildDataLayer();
// Use Top placeholder for noscript fallback
this.context.placeholderProvider.changedEvent.add(
this, this._renderPlaceholders
);
return Promise.resolve();
}
private _injectGtm(gtmId: string): void {
// Check if already loaded
if (document.querySelector(`script[src*="googletagmanager.com/gtm.js?id=${gtmId}"]`)) {
return;
}
(window as any).dataLayer = (window as any).dataLayer || [];
(window as any).dataLayer.push({
'gtm.start': new Date().getTime(),
event: 'gtm.js'
});
const script = document.createElement('script');
script.async = true;
script.src = `https://www.googletagmanager.com/gtm.js?id=${gtmId}`;
document.head.appendChild(script);
}
private _buildDataLayer(): void {
const ctx = this.context;
(window as any).dataLayer = (window as any).dataLayer || [];
(window as any).dataLayer.push({
'spSiteUrl': ctx.pageContext.site.absoluteUrl,
'spWebUrl': ctx.pageContext.web.absoluteUrl,
'spPageTitle': ctx.pageContext.web.title,
'spListId': ctx.pageContext.list?.id?.toString() || '',
'spListTitle': ctx.pageContext.list?.title || '',
'spUserId': ctx.pageContext.user.loginName,
'spUserEmail': ctx.pageContext.user.email,
'spCultureName': ctx.pageContext.cultureInfo.currentCultureName,
'spIsNoScript': false,
'spPageType': this._getPageType()
});
}
private _getPageType(): string {
const url = window.location.pathname.toLowerCase();
if (url.includes('/sitepages/')) return 'sitePage';
if (url.includes('/lists/')) return 'listView';
if (url.includes('/_layouts/')) return 'systemPage';
return 'other';
}
private _renderPlaceholders(): void {
if (!this._topPlaceholder) {
this._topPlaceholder = this.context.placeholderProvider
.tryCreateContent(PlaceholderName.Top);
if (this._topPlaceholder) {
const gtmId = this.properties.gtmId;
this._topPlaceholder.domElement.innerHTML = `
<noscript>
<iframe src="https://www.googletagmanager.com/ns.html?id=${gtmId}"
height="0" width="0"
style="display:none;visibility:hidden">
</iframe>
</noscript>`;
}
}
}
}
config/serve.json (for local testing):
{
"serveConfigurations": {
"default": {
"customActions": {
"analytics-customizer": {
"location": "ClientSideExtension.ApplicationCustomizer",
"properties": {
"gtmId": "GTM-XXXXXX"
}
}
}
}
}
}
Deploy the solution:
gulp bundle --ship
gulp package-solution --ship
# Upload .sppkg to App Catalog
# Add to site or tenant-wide deployment
Method 2: PnP PowerShell Custom Action (No SPFx Build Required)
For quick deployment without building an SPFx solution, use PnP PowerShell to add a ScriptLink custom action:
# Connect to SharePoint Online
Connect-PnPOnline -Url "https://tenant.sharepoint.com/sites/yoursite" -Interactive
# Add GTM script to all pages in the site
Add-PnPCustomAction `
-Name "GTM-Analytics" `
-Title "Google Tag Manager" `
-Location "ClientSideExtension.ApplicationCustomizer" `
-ClientSideComponentId "GUID-of-your-customizer" `
-ClientSideComponentProperties '{"gtmId":"GTM-XXXXXX"}'
# For Classic pages, use ScriptLink location
Add-PnPCustomAction `
-Name "GTM-Classic" `
-Title "GTM Classic Pages" `
-Location "ScriptLink" `
-ScriptSrc "https://your-cdn.com/gtm-loader.js" `
-Sequence 1000
Method 3: Classic Pages - Master Page Editing
For Classic SharePoint (on-premises or sites still using classic experience):
<!-- In your custom master page, add before </head> -->
<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>
<!-- After <body> tag -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
Data Layer Implementation
The SPFx Application Customizer has access to the SharePoint page context, which provides rich metadata for the data layer:
private _buildExtendedDataLayer(): void {
const ctx = this.context;
const pageContext = ctx.pageContext;
(window as any).dataLayer = (window as any).dataLayer || [];
(window as any).dataLayer.push({
// Site context
'spSiteId': pageContext.site.id.toString(),
'spSiteUrl': pageContext.site.absoluteUrl,
'spWebId': pageContext.web.id.toString(),
'spWebTitle': pageContext.web.title,
'spWebTemplate': pageContext.web.templateName,
// Page context
'spPageTitle': document.title,
'spPageUrl': window.location.pathname,
'spListId': pageContext.list?.id?.toString() || '',
'spListTitle': pageContext.list?.title || '',
'spListItemId': pageContext.listItem?.id || '',
// User context
'spUserLogin': pageContext.user.loginName,
'spUserEmail': pageContext.user.email,
'spUserDisplayName': pageContext.user.displayName,
'spIsExternalUser': pageContext.user.isExternalGuestUser,
// Environment
'spEnvironment': pageContext.legacyPageContext?.env || 'production',
'spIsModern': true,
'spCulture': pageContext.cultureInfo.currentCultureName,
// Hub site
'spHubSiteId': pageContext.legacyPageContext?.hubSiteId || '',
});
}
For Classic pages, build the data layer from the _spPageContextInfo global:
<script>
window.dataLayer = window.dataLayer || [];
if (typeof _spPageContextInfo !== 'undefined') {
window.dataLayer.push({
'spSiteUrl': _spPageContextInfo.siteAbsoluteUrl,
'spWebUrl': _spPageContextInfo.webAbsoluteUrl,
'spWebTitle': _spPageContextInfo.webTitle,
'spPageTitle': document.title,
'spUserId': _spPageContextInfo.userId,
'spUserLogin': _spPageContextInfo.userLoginName,
'spListId': _spPageContextInfo.listId || '',
'spListTitle': _spPageContextInfo.listTitle || '',
'spPageItemId': _spPageContextInfo.pageItemId || '',
'spIsModern': false,
'spCulture': _spPageContextInfo.currentCultureName
});
}
</script>
Common Issues
SPFx Application Customizer loads after page render. The customizer's onInit fires after the SharePoint shell loads, meaning the GTM script loads later than a traditional <head> injection. This can cause missed pageview events if GTM fires before the data layer is built. Use dataLayer.push with the event key to trigger GTM tags after the data layer is ready.
Content Security Policy blocks external scripts. SharePoint Online enforces a CSP that may block scripts from domains not in the allow list. GTM's googletagmanager.com domain is generally allowed, but custom analytics endpoints or third-party pixel domains may be blocked. Check the browser console for CSP violations.
Single-page application navigation. Modern SharePoint uses client-side navigation between pages. The page does not fully reload, so page_view events may not fire on navigation. Implement a navigation listener in your Application Customizer:
this.context.application.navigatedEvent.add(this, () => {
(window as any).dataLayer.push({
event: 'spa_navigation',
spPageUrl: window.location.pathname,
spPageTitle: document.title,
});
});
Duplicate script injection. If the Application Customizer is deployed tenant-wide and also added at the site level, the GTM script may load twice. Always check for existing GTM script tags before injecting.
Classic vs Modern page detection. Some SharePoint sites use a mix of Classic and Modern pages. Your analytics solution must handle both. Check for the presence of _spPageContextInfo (Classic) vs. the SPFx context to determine the page type.
Platform-Specific Considerations
SharePoint analytics implementations must account for the tenant/site/web hierarchy. A single tenant may contain hundreds of site collections, each with multiple subsites. Tenant-wide SPFx deployment (via the tenant App Catalog) is the most efficient way to deploy analytics globally.
User identity is always available in SharePoint (users must be authenticated). This means you can implement user-level analytics tracking without additional login flows, but you must comply with your organization's privacy policies regarding user tracking on intranet sites.
SharePoint Online's ClientSideComponentProperties allows configuring the Application Customizer per site. Use this to pass different GTM container IDs to different sites if needed:
{
"gtmId": "GTM-SITE-SPECIFIC"
}
For SharePoint on-premises environments, SPFx support depends on the server version. SharePoint 2019 supports SPFx 1.4.1. SharePoint 2016 does not support SPFx natively and requires Classic deployment methods (master pages, custom actions, or script editor web parts).
Microsoft Clarity and Application Insights can be deployed through the same SPFx Application Customizer pattern. For organizations using the full Microsoft analytics stack, Application Insights provides server-side telemetry that correlates with SharePoint's built-in usage analytics available in the SharePoint Admin Center.