Skip to content

feat(third-parties): add consent management support to GoogleTagManager #80719

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: canary
Choose a base branch
from
120 changes: 120 additions & 0 deletions packages/third-parties/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,126 @@

## Google Third-Parties

### Google Tag Manager

The `GoogleTagManager` component can be used to instantiate a [Google Tag Manager](https://developers.google.com/tag-manager) container for your page. By default, it fetches the original inline script after hydration occurs on the page.

```js
import { GoogleTagManager } from '@next/third-parties/google'

export default function Page() {
return <GoogleTagManager gtmId="GTM-XYZ" />
}
```

#### Consent Management

The `GoogleTagManager` component supports consent management platforms by allowing you to control script execution and add data attributes for consent management platforms (CMPs). This implementation works with all major CMP platforms including Usercentrics, OneTrust, Cookiebot, Didomi, and custom solutions.

**Usercentrics Integration:**

```js
import { GoogleTagManager } from '@next/third-parties/google'

export default function Page() {
return (
<GoogleTagManager
gtmId="GTM-XYZ"
type="text/plain"
data-usercentrics="Google Tag Manager"
/>
)
}
```

**OneTrust Integration:**

```js
import { GoogleTagManager } from '@next/third-parties/google'

export default function Page() {
return (
<GoogleTagManager
gtmId="GTM-XYZ"
type="text/plain"
data-one-trust-category="C0002"
/>
)
}
```

**Cookiebot Integration:**

```js
import { GoogleTagManager } from '@next/third-parties/google'

export default function Page() {
return (
<GoogleTagManager
gtmId="GTM-XYZ"
type="text/plain"
data-cookieconsent="statistics"
/>
)
}
```

**Didomi Integration:**

```js
import { GoogleTagManager } from '@next/third-parties/google'

export default function Page() {
return (
<GoogleTagManager
gtmId="GTM-XYZ"
type="text/plain"
data-didomi-purposes="analytics"
/>
)
}
```

**Custom Consent Management:**

```js
import { GoogleTagManager } from '@next/third-parties/google'

export default function Page() {
return (
<GoogleTagManager
gtmId="GTM-XYZ"
type="text/plain"
data-consent-category="analytics"
data-consent-required="true"
/>
)
}
```

The `type="text/plain"` attribute prevents the script from executing until your consent management platform changes it to `type="application/javascript"`. The `data-*` attributes allow your CMP to identify and manage the script according to your consent configuration.

#### Sending Events

You can send events using the `sendGTMEvent` function:

```js
import { sendGTMEvent } from '@next/third-parties/google'

export default function Page() {
return (
<div>
<GoogleTagManager gtmId="GTM-XYZ" />
<button
onClick={() => sendGTMEvent({ event: 'buttonClicked', value: 'xyz' })}
>
Send Event
</button>
</div>
)
}
```

### YouTube Embed

The `YouTubeEmbed` component is used to load and display a YouTube embed. This component loads faster by using [lite-youtube-embed](https://github.com/paulirish/lite-youtube-embed) under the hood.
Expand Down
13 changes: 11 additions & 2 deletions packages/third-parties/src/google/gtm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export function GoogleTagManager(props: GTMParams) {
preview,
dataLayer,
nonce,
type = 'application/javascript',
...scriptProps
} = props

currDataLayerName = dataLayerName
Expand All @@ -39,8 +41,12 @@ export function GoogleTagManager(props: GTMParams) {

return (
<>
{/* GTM DataLayer initialization */}
<Script
id="_next-gtm-init"
nonce={nonce}
type={type}
{...scriptProps}
dangerouslySetInnerHTML={{
__html: `
(function(w,l){
Expand All @@ -49,13 +55,16 @@ export function GoogleTagManager(props: GTMParams) {
${dataLayer ? `w[l].push(${JSON.stringify(dataLayer)})` : ''}
})(window,'${dataLayerName}');`,
}}
nonce={nonce}
/>

{/* GTM Script */}
<Script
id="_next-gtm"
nonce={nonce}
data-ntpc="GTM"
src={`${gtmScriptUrl}?id=${gtmId}${gtmLayer}${gtmAuth}${gtmPreview}`}
nonce={nonce}
type={type}
{...scriptProps}
/>
</>
)
Expand Down
5 changes: 5 additions & 0 deletions packages/third-parties/src/types/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export type GTMParams = {
auth?: string
preview?: string
nonce?: string
// Consent management props
type?: 'application/javascript' | 'text/plain'
} & {
// Data attributes for CMP platforms
[K in `data-${string}`]: string
}

export type GAParams = {
Expand Down
87 changes: 82 additions & 5 deletions test/e2e/third-parties/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { nextTestSetup } from 'e2e-utils'
import { waitFor } from 'next-test-utils'
import type { NextInstance } from 'e2e-utils'

describe('@next/third-parties basic usage', () => {
const { next } = nextTestSetup({
const { next }: { next: NextInstance } = nextTestSetup({
files: __dirname,
dependencies: {
'@next/third-parties': 'canary',
Expand Down Expand Up @@ -44,15 +45,91 @@ describe('@next/third-parties basic usage', () => {

expect(gtmScript.length).toBe(1)

const dataLayer = await browser.eval('window.dataLayer')
const dataLayer: unknown[] = await browser.eval('window.dataLayer')
expect(dataLayer.length).toBe(1)

await browser.elementByCss('#gtm-send').click()

const dataLayer2 = await browser.eval('window.dataLayer')
const dataLayer2: unknown[] = await browser.eval('window.dataLayer')
expect(dataLayer2.length).toBe(2)
})

it('renders GTM with consent management for multiple CMP platforms', async () => {
const browser = await next.browser('/gtm-consent')
await waitFor(1000)

// Test standard GTM (should have type="application/javascript" by default)
const standardScripts = await browser.elementsByCss(
'script[src*="GTM-STANDARD"]'
)
expect(standardScripts.length).toBe(1)

const standardType: string | null = await browser.eval(
'document.querySelector("#_next-gtm-init").getAttribute("type")'
)
expect(standardType).toBe('application/javascript')

// Test Usercentrics GTM (should have type="text/plain" and data-usercentrics)
const usercentricsInitScripts = await browser.elementsByCss(
'script[type="text/plain"][data-usercentrics="Google Tag Manager"]'
)
expect(usercentricsInitScripts.length).toBe(2) // init + external script

const usercentricsScripts = await browser.elementsByCss(
'script[src*="GTM-USERCENTRICS"][type="text/plain"]'
)
expect(usercentricsScripts.length).toBe(1)

// Test OneTrust GTM (should have type="text/plain" and data-one-trust-category)
const onetrustInitScripts = await browser.elementsByCss(
'script[type="text/plain"][data-one-trust-category="C0002"]'
)
expect(onetrustInitScripts.length).toBe(2) // init + external script

const onetrustScripts = await browser.elementsByCss(
'script[src*="GTM-ONETRUST"][type="text/plain"]'
)
expect(onetrustScripts.length).toBe(1)

// Test Cookiebot GTM (should have type="text/plain" and data-cookieconsent)
const cookiebotInitScripts = await browser.elementsByCss(
'script[type="text/plain"][data-cookieconsent="statistics"]'
)
expect(cookiebotInitScripts.length).toBe(2) // init + external script

const cookiebotScripts = await browser.elementsByCss(
'script[src*="GTM-COOKIEBOT"][type="text/plain"]'
)
expect(cookiebotScripts.length).toBe(1)

// Test Didomi GTM (should have type="text/plain" and data-didomi-purposes)
const didomiInitScripts = await browser.elementsByCss(
'script[type="text/plain"][data-didomi-purposes="analytics"]'
)
expect(didomiInitScripts.length).toBe(2) // init + external script

const didomiScripts = await browser.elementsByCss(
'script[src*="GTM-DIDOMI"][type="text/plain"]'
)
expect(didomiScripts.length).toBe(1)

// Test custom consent GTM (multiple data attributes)
const customInitScripts = await browser.elementsByCss(
'script[type="text/plain"][data-consent-category="analytics"]'
)
expect(customInitScripts.length).toBe(2) // init + external script

const customScripts = await browser.elementsByCss(
'script[src*="GTM-CUSTOM"][type="text/plain"][data-consent-required="true"]'
)
expect(customScripts.length).toBe(1)

// Test that consent-managed scripts don't execute until consent is given
const dataLayer: unknown[] = await browser.eval('window.dataLayer')
// Only the standard GTM should have initialized dataLayer
expect(dataLayer.length).toBe(1)
})

it('renders GA', async () => {
const browser = await next.browser('/ga')

Expand All @@ -67,12 +144,12 @@ describe('@next/third-parties basic usage', () => {
)

expect(gaScript.length).toBe(1)
const dataLayer = await browser.eval('window.dataLayer')
const dataLayer: unknown[] = await browser.eval('window.dataLayer')
expect(dataLayer.length).toBe(4)

await browser.elementByCss('#ga-send').click()

const dataLayer2 = await browser.eval('window.dataLayer')
const dataLayer2: unknown[] = await browser.eval('window.dataLayer')
expect(dataLayer2.length).toBe(5)
})
})
68 changes: 68 additions & 0 deletions test/e2e/third-parties/pages/gtm-consent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react'
import { GoogleTagManager, sendGTMEvent } from '@next/third-parties/google'

/**
* Test page for GTM consent management functionality
* Demonstrates compatibility with multiple CMP platforms
* @returns {React.ReactElement} The test page component
*/
const Page = () => {
/**
* Handle button click to send GTM event
* @returns {void}
*/
const onClick = () => {
sendGTMEvent({ event: 'buttonClicked', value: 'consent-test' })
}

return (
<div className="container">
<h1>GTM Consent Management</h1>

{/* Standard GTM without consent */}
<GoogleTagManager gtmId="GTM-STANDARD" />

{/* GTM with Usercentrics consent management */}
<GoogleTagManager
gtmId="GTM-USERCENTRICS"
type="text/plain"
data-usercentrics="Google Tag Manager"
/>

{/* GTM with OneTrust consent management */}
<GoogleTagManager
gtmId="GTM-ONETRUST"
type="text/plain"
data-one-trust-category="C0002"
/>

{/* GTM with Cookiebot consent management */}
<GoogleTagManager
gtmId="GTM-COOKIEBOT"
type="text/plain"
data-cookieconsent="statistics"
/>

{/* GTM with Didomi consent management */}
<GoogleTagManager
gtmId="GTM-DIDOMI"
type="text/plain"
data-didomi-purposes="analytics"
/>

{/* GTM with custom consent management */}
<GoogleTagManager
gtmId="GTM-CUSTOM"
type="text/plain"
data-consent-category="analytics"
data-consent-required="true"
/>

<button id="gtm-consent-send" onClick={onClick}>
Send Event
</button>
</div>
)
}

export default Page
Loading