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 6 commits into
base: canary
Choose a base branch
from
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
64 changes: 64 additions & 0 deletions packages/third-parties/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,70 @@

## 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.

**Example with Usercentrics:**

```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"
/>
)
}
```

**Other CMP Platforms:**

The same pattern works for other consent management platforms by using their specific data attributes:

- **OneTrust**: `data-one-trust-category="C0002"`
- **Cookiebot**: `data-cookieconsent="statistics"`
- **Didomi**: `data-didomi-purposes="analytics"`
- **Custom**: `data-consent-category="analytics"`

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
32 changes: 27 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,36 @@ 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 support', async () => {
const browser = await next.browser('/gtm-consent')
await waitFor(1000)

// Test consent-managed GTM script has correct type and data attributes
const consentScript = await browser.elementsByCss(
'script[src*="GTM-USERCENTRICS"][type="text/plain"]'
)
expect(consentScript.length).toBe(1)

// Verify data attributes are applied to both init and external scripts
const scriptsWithDataAttr = await browser.elementsByCss(
'script[data-usercentrics="Google Tag Manager"]'
)
expect(scriptsWithDataAttr.length).toBe(2) // init + external script

// Verify consent-managed scripts don't execute (dataLayer should be empty)
const dataLayer: unknown[] = await browser.eval('window.dataLayer || []')
expect(dataLayer.length).toBe(0) // No execution due to type="text/plain"
})

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

Expand All @@ -67,12 +89,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)
})
})
36 changes: 36 additions & 0 deletions test/e2e/third-parties/pages/gtm-consent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react'
import { GoogleTagManager, sendGTMEvent } from '@next/third-parties/google'

/**
* Test page for GTM consent management functionality
* Tests only the new consent management feature
* @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>

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

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

export default Page
1 change: 0 additions & 1 deletion test/e2e/third-parties/pages/gtm.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ const Page = () => {
<button id="gtm-send" onClick={onClick}>
Click
</button>
<GoogleTagManager gtmId="GTM-XYZ" />
</div>
)
}
Expand Down
Loading