Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
321 changes: 321 additions & 0 deletions samples/js-application-gtm-injector/.eslintrc.js

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions samples/js-application-gtm-injector/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Logs
logs
*.log
npm-debug.log*

# Dependency directories
node_modules

# Build generated files
dist
lib
lib-dts
lib-commonjs
lib-esm
jest-output
release
solution
temp
*.sppkg
.heft

# Coverage directory used by tools like istanbul
coverage

# OSX
.DS_Store

# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj

# Resx Generated Code
*.resx.ts

# Styles Generated Code
*.scss.ts
22 changes: 22 additions & 0 deletions samples/js-application-gtm-injector/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Ignore everything by default
**

# Use negative patterns to bring back the specific things we want to publish
!/bin/**
!/dist/**
!/EULA/**
!/lib/**
!ThirdPartyNotice.txt

# Ignore certain files in the above folders
/dist/*.stats.*
/dist/**/*.js.map
/lib/**/*.js.map
/lib/**/test/**

# NOTE: These don't need to be specified, because NPM includes them automatically.
#
# package.json
# README (and its variants)
# CHANGELOG (and its variants)
# LICENSE / LICENCE
1 change: 1 addition & 0 deletions samples/js-application-gtm-injector/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v23.0.0
20 changes: 20 additions & 0 deletions samples/js-application-gtm-injector/.yo-rc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"@microsoft/generator-sharepoint": {
"useGulp": false,
"plusBeta": false,
"isCreatingSolution": true,
"nodeVersion": "22.22.0",
"sdksVersions": {},
"version": "1.22.2",
"libraryName": "gtm-tracker",
"libraryId": "1bebcb20-2e53-41a2-9f5c-cba42eec94fe",
"environment": "spo",
"packageManager": "npm",
"solutionName": "gtm-tracker",
"solutionShortDescription": "gtm-tracker description",
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"componentType": "extension",
"extensionType": "ApplicationCustomizer"
}
}
119 changes: 119 additions & 0 deletions samples/js-application-gtm-injector/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Google Tag Manager Injector

## Summary

This SharePoint Framework (SPFx) Application Customizer injects **Google Tag Manager (GTM)** into SharePoint Modern pages, enabling analytics and tag management across your tenant. It initializes the standard GTM `dataLayer`, loads the GTM script via `SPComponentLoader`, and hooks into SharePoint's navigation events to track **Single Page Application (SPA) route changes** — ensuring every page view is captured without a full browser reload.

Built to be compliant with the **SharePoint Online Content Security Policy (CSP)** enforcement introduced in March 2026. Because GTM is loaded dynamically at runtime, `https://www.googletagmanager.com` must be manually registered as a **Trusted Script Source** in the SharePoint Admin Center before deployment. See [Prerequisites](#prerequisites) for the required steps.

![GTM Injector in action](./assets/gtm-injector-demo.png)

## Used SharePoint Framework Version

![version](https://img.shields.io/badge/version-1.22.2-green.svg)

## Applies to

- [SharePoint Framework](https://aka.ms/spfx)
- [Microsoft 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-developer-tenant)

> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)

## Prerequisites

- Node.js v22.14.0 or compatible version
- A Google Tag Manager account and Container ID (e.g. `GTM-XXXXXXX`)
- Tenant Administrator access to the SharePoint Admin Center

### ⚠️ Required: CSP Trusted Script Source Registration

SharePoint Online enforces [Content Security Policy (CSP)](https://learn.microsoft.com/en-us/sharepoint/dev/spfx/content-securty-policy-trusted-script-sources) on all Modern pages from **March 1, 2026** onwards. This solution uses `SPComponentLoader.loadScript()` to load GTM dynamically at runtime — SharePoint does **not** auto-trust sources loaded this way, so you must register the GTM domain manually.

**Via SharePoint Admin Center:**

1. Go to **SharePoint Admin Center** → **Advanced** → **Script sources**
2. Select **Add source**
3. Enter `https://www.googletagmanager.com` and save

> Wildcard expressions (e.g. `*.googletagmanager.com`) are not supported. Use the exact origin.

**Verify:** Load a Modern page with the solution active and append `?csp=enforce` to the URL. Open DevTools (F12) → Console. No `Content Security Policy` violations means the configuration is correct.


## Solution

| Solution | Author(s) |
| ----------- | ------------------------------------------------------- |
| js-application-gtm-injector | [@saiiiiiii](https://github.com/saiiiiiii) |

## Version history

| Version | Date | Comments |
| ------- | ------------- | --------------- |
| 1.0 | April 2026 | Initial release |

## Disclaimer

**THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**

---

## Minimal Path to Awesome

- Clone this repository
- Update your GTM Container ID in `src/extensions/gtmInjector/GtmInjectorApplicationCustomizer.ts`:
```typescript
const GTM_ID: string = 'GTM-XXXXXXX'; // Replace with your GTM Container ID
```
- Ensure that you are at the solution folder
- In the command-line run:
- `npm install -g @rushstack/heft`
- `npm install`
- `heft start`
- Add your SharePoint site URL to `config/serve.json` under `pageUrl`
- Complete the [CSP Trusted Script Source registration](#-required-csp-trusted-script-source-registration) before testing against a CSP-enforced tenant

**To build and package for deployment:**

```bash
heft test --clean --production
heft package-solution --production
```

Upload the generated `.sppkg` from `sharepoint/solution/gtm-tracker.sppkg` to your Tenant App Catalog. The solution is configured with `skipFeatureDeployment: true`, allowing tenant-wide activation directly from the App Catalog.

Other build commands can be listed using `heft --help`.

## Features

This extension injects Google Tag Manager into SharePoint Modern pages and enables full analytics tracking across Single Page Application (SPA) navigations.

This extension illustrates the following concepts:

- Dynamically loading an external analytics script using `SPComponentLoader.loadScript()` in a CSP-compliant manner — avoiding inline scripts which are blocked by SharePoint Online from March 2026 onwards
- Initializing a standard GTM `dataLayer` and pushing the `gtm.js` start event on page load
- Tracking SPA navigations by listening to both SharePoint's `navigatedEvent` (from `this.context.application`) and the browser's native `popstate` event
- Preventing duplicate script injection across component re-renders using a `window.__gtmLoaded` guard flag
- Deploying a tenant-wide Application Customizer with `skipFeatureDeployment: true` for zero-touch rollout

> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions in advance.

> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp.

## References

- [Getting started with SharePoint Framework](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-developer-tenant)
- [Support for Content Security Policy (CSP) in SharePoint Online](https://learn.microsoft.com/en-us/sharepoint/dev/spfx/content-securty-policy-trusted-script-sources)
- [SharePoint Online CSP Enforcement Dates and Guidance](https://techcommunity.microsoft.com/blog/spblog/sharepoint-online-content-security-policy-csp-enforcement-dates-and-guidance/4472662)
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/sharepoint/dev/spfx/publish-to-marketplace-overview)
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
- [Google Tag Manager — Developer Quickstart](https://developers.google.com/tag-platform/tag-manager/web)
- [Heft Documentation](https://heft.rushstack.io/)


## Help

If you encounter any issues while using this sample, [create a new issue](https://github.com/saiiiiiii/sp-dev-fx-extensions/samples/js-application-gtm-injector/issues/new).

<img src="https://m365-visitor-stats.azurewebsites.net/sp-dev-fx-extensions/samples/js-application-gtm-injector" />
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
78 changes: 78 additions & 0 deletions samples/js-application-gtm-injector/assets/sample.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
[
{
"name": "pnp-sp-dev-spfx-extensions-js-application-gtm-injector",
"source": "pnp",
"title": "Google Tag Manager Injector",
"shortDescription": "A SharePoint Framework (SPFx) Application Customizer that injects Google Tag Manager (GTM) into SharePoint Modern pages with full SPA navigation tracking support, built to be compliant with SharePoint Online Content Security Policy (CSP) enforcement.",
"url": "https://github.com/pnp/sp-dev-fx-extensions/tree/main/samples/js-application-gtm-injector",
"downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-extensions/tree/main/samples/js-application-gtm-injector",
"longDescription": [
"A SharePoint Framework (SPFx) Application Customizer that injects Google Tag Manager (GTM) into SharePoint Modern pages. Initializes the standard GTM dataLayer, loads the GTM script via SPComponentLoader, and hooks into SharePoint's navigation events to track Single Page Application (SPA) route changes — ensuring every page view is captured without a full browser reload.",
"Built to be compliant with the SharePoint Online Content Security Policy (CSP) enforcement introduced in March 2026. Because GTM is loaded dynamically at runtime via SPComponentLoader.loadScript(), https://www.googletagmanager.com must be manually registered as a Trusted Script Source in the SharePoint Admin Center before deployment."
],
"creationDateTime": "2026-04-10",
"updateDateTime": "2026-04-10",
"products": [
"SharePoint",
"Office"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "TypeScript"
},
{
"key": "SPFX-VERSION",
"value": "1.22.2"
}
],
"thumbnails": [
{
"name": "gtm-injector-demo.png",
"type": "image",
"order": 101,
"url": "https://github.com/pnp/sp-dev-fx-extensions/raw/main/samples/js-application-gtm-injector/assets/gtm-injector-demo.png",
"alt": "Google Tag Manager"
}
],
"authors": [
{
"gitHubAccount": "saiiiiiii",
"pictureUrl": "https://avatars.githubusercontent.com/u/46020510?v=4",
"name": "Sai Siva Ram Bandaru"
}
],
"references": [
{
"name": "Getting started with SharePoint Framework",
"description": "Introduction about how to develop Microsoft 365 extensions using SharePoint Framework.",
"url": "https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant"
},
{
"name": "Build your first SharePoint Framework Extension",
"description": "Learn how to build your first SharePoint Framework Application Customizer.",
"url": "https://learn.microsoft.com/en-us/sharepoint/dev/spfx/extensions/get-started/build-a-hello-world-extension"
},
{
"name": "Support for Content Security Policy (CSP) in SharePoint Online",
"description": "Learn how CSP works with SPFx solutions, how to identify violations, and how to configure trusted sources in SharePoint Online.",
"url": "https://learn.microsoft.com/en-us/sharepoint/dev/spfx/content-securty-policy-trusted-script-sources"
},
{
"name": "SharePoint Online CSP Enforcement Dates and Guidance",
"description": "Microsoft Community Hub post detailing CSP enforcement timelines and what SPFx developers need to do to prepare.",
"url": "https://techcommunity.microsoft.com/blog/spblog/sharepoint-online-content-security-policy-csp-enforcement-dates-and-guidance/4472662"
},
{
"name": "Google Tag Manager Developer Quickstart",
"description": "Official Google documentation for implementing Google Tag Manager on your website.",
"url": "https://developers.google.com/tag-platform/tag-manager/web"
},
{
"name": "Microsoft 365 Patterns and Practices",
"description": "Guidance, tooling, samples and open-source controls for your Microsoft 365 development.",
"url": "https://aka.ms/m365pnp"
}
]
}
]
18 changes: 18 additions & 0 deletions samples/js-application-gtm-injector/config/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"gtm-injector-application-customizer": {
"components": [
{
"entrypoint": "./lib/extensions/gtmInjector/GtmInjectorApplicationCustomizer.js",
"manifest": "./src/extensions/gtmInjector/GtmInjectorApplicationCustomizer.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"GtmInjectorApplicationCustomizerStrings": "lib/extensions/gtmInjector/loc/{locale}.js"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./release/assets/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "gtm-tracker",
"accessKey": "<!-- ACCESS KEY -->"
}
46 changes: 46 additions & 0 deletions samples/js-application-gtm-injector/config/package-solution.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "gtm-tracker-client-side-solution",
"id": "1bebcb20-2e53-41a2-9f5c-cba42eec94fe",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": "Undefined-1.22.2"
},
"metadata": {
"shortDescription": {
"default": "gtm-tracker description"
},
"longDescription": {
"default": "gtm-tracker description"
},
"screenshotPaths": [],
"videoUrl": "",
"categories": []
},
"features": [
{
"title": "Application Extension - Deployment of custom action",
"description": "Deploys a custom action with ClientSideComponentId association",
"id": "41e2e5a9-8936-4399-a66a-c77eb57774f9",
"version": "1.0.0.0",
"assets": {
"elementManifests": [
"elements.xml",
"ClientSideInstance.xml"
]
}
}
]
},
"paths": {
"zippedPackage": "solution/gtm-tracker.sppkg"
}
}
7 changes: 7 additions & 0 deletions samples/js-application-gtm-injector/config/rig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
// The "rig.json" file directs tools to look for their config files in an external package.
// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",

"rigPackageName": "@microsoft/spfx-web-build-rig"
}
5 changes: 5 additions & 0 deletions samples/js-application-gtm-injector/config/sass.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-sass-plugin.schema.json",

"extends": "@microsoft/spfx-web-build-rig/profiles/default/config/sass.json"
}
Loading
Loading