diff --git a/docs/content/scripts/google-analytics.md b/docs/content/scripts/google-analytics.md index 5dbcc5ee..4568a5d2 100644 --- a/docs/content/scripts/google-analytics.md +++ b/docs/content/scripts/google-analytics.md @@ -30,9 +30,13 @@ proxy.gtag('event', 'page_view') The proxy exposes the `gtag` and `dataLayer` properties, and you should use them following Google Analytics best practices. -### Consent Mode +## Consent Mode -Google Analytics natively consumes [GCMv2 consent state](https://developers.google.com/tag-platform/security/guides/consent). Set the default with `defaultConsent` (fires `gtag('consent', 'default', ...)`{lang="ts"} before `gtag('js', ...)`{lang="ts"}) and call `consent.update()`{lang="ts"} at runtime: +Google Analytics natively consumes [GCMv2 consent state](https://developers.google.com/tag-platform/security/guides/consent). Set the default with `defaultConsent` (fires `gtag('consent', 'default', state)`{lang="ts"} before `gtag('js', ...)`{lang="ts"}) and call `consent.update()`{lang="ts"} at runtime to flip categories. + +::callout{icon="i-heroicons-play" to="https://stackblitz.com/github/nuxt/scripts/tree/main/examples/regional-consent" target="_blank"} +Try the live [Regional Consent Example](https://stackblitz.com/github/nuxt/scripts/tree/main/examples/regional-consent) on [StackBlitz](https://stackblitz.com). +:: ```vue +``` + +The module forwards each entry verbatim, in input order. Precedence between region-scoped and unscoped defaults is enforced by gtag at runtime, not by ordering. + +## Customer/Consumer ID Tracking For e-commerce or multi-tenant applications where you need to track customer-specific analytics alongside your main tracking: diff --git a/docs/content/scripts/google-tag-manager.md b/docs/content/scripts/google-tag-manager.md index a227e5fb..c84b483c 100644 --- a/docs/content/scripts/google-tag-manager.md +++ b/docs/content/scripts/google-tag-manager.md @@ -44,10 +44,10 @@ useScriptEventPage(({ title, path }) => { ## Consent Mode -Google Tag Manager natively consumes [GCMv2 consent state](https://developers.google.com/tag-platform/security/guides/consent?consentmode=basic). Set the default with `defaultConsent` (pushes `['consent','default', state]` onto the dataLayer before the `gtm.js` event) and call `consent.update()`{lang="ts"} at runtime. +Google Tag Manager natively consumes [GCMv2 consent state](https://developers.google.com/tag-platform/security/guides/consent?consentmode=basic). Set the default with `defaultConsent` (pushes `['consent','default', state]` onto the dataLayer before the `gtm.js` event) and call `consent.update()`{lang="ts"} at runtime. Pass an **array** to `defaultConsent` to fire multiple defaults, for example [region-specific defaults](https://developers.google.com/tag-platform/security/guides/consent?consentmode=advanced#region-specific-behavior) where each entry targets different countries via `region`. ::callout{icon="i-heroicons-play" to="https://stackblitz.com/github/nuxt/scripts/tree/main/examples/cookie-consent" target="_blank"} -Try the live [Cookie Consent Example](https://stackblitz.com/github/nuxt/scripts/tree/main/examples/cookie-consent) or [Granular Consent Example](https://stackblitz.com/github/nuxt/scripts/tree/main/examples/granular-consent) on [StackBlitz](https://stackblitz.com). +Try the live [Cookie Consent Example](https://stackblitz.com/github/nuxt/scripts/tree/main/examples/cookie-consent), [Granular Consent Example](https://stackblitz.com/github/nuxt/scripts/tree/main/examples/granular-consent), or [Regional Consent Example](https://stackblitz.com/github/nuxt/scripts/tree/main/examples/regional-consent) on [StackBlitz](https://stackblitz.com). :: ### Consent Mode v2 Signals @@ -97,7 +97,39 @@ useScriptEventPage(({ title, path }) => { ``` -`onBeforeGtmStart` remains available as a general escape hatch for any other pre-`gtm.start` setup (only when the GTM ID is passed directly to the composable, not via `nuxt.config`). +### Per-region defaults + +Pass an array to `defaultConsent` to fire one `['consent','default', state]` push per entry, in order. This matches Google's [region-specific consent pattern](https://developers.google.com/tag-platform/security/guides/consent?consentmode=advanced#region-specific-behavior): more specific regions (e.g. `US-CA`) override broader ones (`US`); an entry with no `region` is the unscoped global fallback. + +```vue + +``` + +The module forwards each entry verbatim, in input order. Precedence between region-scoped and unscoped defaults is enforced by gtag at runtime, not by ordering. + +`consent.update()`{lang="ts"} accepts any `Partial`{lang="ts"}; missing categories stay at their current value. `onBeforeGtmStart` remains available as a general escape hatch for any other pre-`gtm.start` setup (only when the GTM ID is passed directly to the composable, not via `nuxt.config`). ::script-types :: diff --git a/examples/regional-consent/app.vue b/examples/regional-consent/app.vue new file mode 100644 index 00000000..336198c2 --- /dev/null +++ b/examples/regional-consent/app.vue @@ -0,0 +1,54 @@ + + + diff --git a/examples/regional-consent/assets/css/main.css b/examples/regional-consent/assets/css/main.css new file mode 100644 index 00000000..7c95c6f3 --- /dev/null +++ b/examples/regional-consent/assets/css/main.css @@ -0,0 +1,2 @@ +@import "tailwindcss"; +@import "@nuxt/ui"; diff --git a/examples/regional-consent/nuxt.config.ts b/examples/regional-consent/nuxt.config.ts new file mode 100644 index 00000000..cad233ba --- /dev/null +++ b/examples/regional-consent/nuxt.config.ts @@ -0,0 +1,7 @@ +export default defineNuxtConfig({ + modules: ['@nuxt/scripts', '@nuxt/ui'], + + devtools: { enabled: true }, + css: ['~/assets/css/main.css'], + compatibilityDate: '2025-01-01', +}) diff --git a/examples/regional-consent/package.json b/examples/regional-consent/package.json new file mode 100644 index 00000000..f93e8e06 --- /dev/null +++ b/examples/regional-consent/package.json @@ -0,0 +1,17 @@ +{ + "name": "nuxt-scripts-regional-consent-example", + "type": "module", + "private": true, + "scripts": { + "dev": "nuxt dev", + "build": "nuxt build", + "preview": "nuxt preview" + }, + "dependencies": { + "@nuxt/scripts": "latest", + "@nuxt/ui": "^4.7.1", + "nuxt": "^4.4.4", + "tailwindcss": "^4.2.4", + "vue": "^3.5.33" + } +} diff --git a/examples/regional-consent/tsconfig.json b/examples/regional-consent/tsconfig.json new file mode 100644 index 00000000..4b34df15 --- /dev/null +++ b/examples/regional-consent/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.nuxt/tsconfig.json" +} diff --git a/packages/script/src/registry-types.json b/packages/script/src/registry-types.json index 81e2e78e..2d7cd29d 100644 --- a/packages/script/src/registry-types.json +++ b/packages/script/src/registry-types.json @@ -260,7 +260,7 @@ { "name": "GoogleAnalyticsOptions", "kind": "const", - "code": "export const GoogleAnalyticsOptions = object({\n /**\n * The GA4 measurement ID.\n * @example 'G-XXXXXXXX'\n * @see https://developers.google.com/analytics/devguides/collection/gtagjs\n */\n id: optional(string()),\n /**\n * Global name for the dataLayer variable.\n * @default 'dataLayer'\n * @see https://developers.google.com/analytics/devguides/collection/gtagjs/setting-up-gtag#rename_the_data_layer\n */\n l: optional(string()),\n /**\n * Default GCMv2 consent state fired as `gtag('consent', 'default', ...)` before `gtag('js', ...)`.\n * @see https://developers.google.com/tag-platform/security/guides/consent\n */\n defaultConsent: optional(gcmConsentState),\n})" + "code": "export const GoogleAnalyticsOptions = object({\n /**\n * The GA4 measurement ID.\n * @example 'G-XXXXXXXX'\n * @see https://developers.google.com/analytics/devguides/collection/gtagjs\n */\n id: optional(string()),\n /**\n * Global name for the dataLayer variable.\n * @default 'dataLayer'\n * @see https://developers.google.com/analytics/devguides/collection/gtagjs/setting-up-gtag#rename_the_data_layer\n */\n l: optional(string()),\n /**\n * Default GCMv2 consent state(s) fired as `gtag('consent', 'default', state)` before\n * `gtag('js', ...)`. Pass an array to fire multiple defaults — for example, different\n * defaults per `region` (more specific regions override broader ones at runtime).\n * @see https://developers.google.com/tag-platform/security/guides/consent?consentmode=advanced#region-specific-behavior\n */\n defaultConsent: optional(union([gcmConsentState, array(gcmConsentState)])),\n})" }, { "name": "GoogleAnalyticsConsent", @@ -503,7 +503,7 @@ { "name": "GoogleTagManagerOptions", "kind": "const", - "code": "export const GoogleTagManagerOptions = object({\n /**\n * GTM container ID (format: GTM-XXXXXX)\n * @see https://developers.google.com/tag-platform/tag-manager/web#install-the-container\n */\n id: string(),\n\n /**\n * Optional dataLayer variable name\n * @default 'dataLayer'\n * @see https://developers.google.com/tag-platform/tag-manager/web/datalayer#rename_the_data_layer\n */\n l: optional(string()),\n\n /**\n * Authentication token for environment-specific container versions\n * @see https://support.google.com/tagmanager/answer/6328337\n */\n auth: optional(string()),\n\n /**\n * Preview environment name\n * @see https://support.google.com/tagmanager/answer/6328337\n */\n preview: optional(string()),\n\n /** Forces GTM cookies to take precedence when true */\n cookiesWin: optional(union([boolean(), literal('x')])),\n\n /**\n * Enables debug mode when true\n * @see https://support.google.com/tagmanager/answer/6107056\n */\n debug: optional(union([boolean(), literal('x')])),\n\n /**\n * No Personal Advertising - disables advertising features when true\n * @see https://developers.google.com/tag-platform/tag-manager/templates/consent-apis\n */\n npa: optional(union([boolean(), literal('1')])),\n\n /** Custom dataLayer name (alternative to \"l\" property) */\n dataLayer: optional(string()),\n\n /**\n * Environment name for environment-specific container\n * @see https://support.google.com/tagmanager/answer/6328337\n */\n envName: optional(string()),\n\n /** Referrer policy for analytics requests */\n authReferrerPolicy: optional(string()),\n\n /**\n * Default consent settings for GTM\n * @see https://developers.google.com/tag-platform/tag-manager/templates/consent-apis\n */\n defaultConsent: optional(record(string(), union([string(), number()]))),\n})" + "code": "export const GoogleTagManagerOptions = object({\n /**\n * GTM container ID (format: GTM-XXXXXX)\n * @see https://developers.google.com/tag-platform/tag-manager/web#install-the-container\n */\n id: string(),\n\n /**\n * Optional dataLayer variable name\n * @default 'dataLayer'\n * @see https://developers.google.com/tag-platform/tag-manager/web/datalayer#rename_the_data_layer\n */\n l: optional(string()),\n\n /**\n * Authentication token for environment-specific container versions\n * @see https://support.google.com/tagmanager/answer/6328337\n */\n auth: optional(string()),\n\n /**\n * Preview environment name\n * @see https://support.google.com/tagmanager/answer/6328337\n */\n preview: optional(string()),\n\n /** Forces GTM cookies to take precedence when true */\n cookiesWin: optional(union([boolean(), literal('x')])),\n\n /**\n * Enables debug mode when true\n * @see https://support.google.com/tagmanager/answer/6107056\n */\n debug: optional(union([boolean(), literal('x')])),\n\n /**\n * No Personal Advertising - disables advertising features when true\n * @see https://developers.google.com/tag-platform/tag-manager/templates/consent-apis\n */\n npa: optional(union([boolean(), literal('1')])),\n\n /** Custom dataLayer name (alternative to \"l\" property) */\n dataLayer: optional(string()),\n\n /**\n * Environment name for environment-specific container\n * @see https://support.google.com/tagmanager/answer/6328337\n */\n envName: optional(string()),\n\n /** Referrer policy for analytics requests */\n authReferrerPolicy: optional(string()),\n\n /**\n * Default GCMv2 consent state(s) fired as `['consent','default', state]` onto the dataLayer\n * before the `gtm.js` event. Pass an array to fire multiple defaults — for example,\n * different defaults per `region` (more specific regions override broader ones at runtime).\n * @see https://developers.google.com/tag-platform/tag-manager/templates/consent-apis\n * @see https://developers.google.com/tag-platform/security/guides/consent?consentmode=advanced#region-specific-behavior\n */\n defaultConsent: optional(union([gcmConsentState, array(gcmConsentState)])),\n})" }, { "name": "GoogleTagManagerConsent", @@ -1443,9 +1443,9 @@ }, { "name": "defaultConsent", - "type": "unknown", + "type": "unknown | unknown[]", "required": false, - "description": "Default GCMv2 consent state fired as `gtag('consent', 'default', ...)` before `gtag('js', ...)`." + "description": "Default GCMv2 consent state(s) fired as `gtag('consent', 'default', state)` before `gtag('js', ...)`. Pass an array to fire multiple defaults — for example, different defaults per `region` (more specific regions override broader ones at runtime)." } ], "GoogleMapsOptions": [ @@ -1632,9 +1632,9 @@ }, { "name": "defaultConsent", - "type": "Record", + "type": "unknown | unknown[]", "required": false, - "description": "Default consent settings for GTM" + "description": "Default GCMv2 consent state(s) fired as `['consent','default', state]` onto the dataLayer before the `gtm.js` event. Pass an array to fire multiple defaults — for example, different defaults per `region` (more specific regions override broader ones at runtime)." } ], "HotjarOptions": [ diff --git a/packages/script/src/runtime/registry/google-analytics.ts b/packages/script/src/runtime/registry/google-analytics.ts index b510a258..c27c707c 100644 --- a/packages/script/src/runtime/registry/google-analytics.ts +++ b/packages/script/src/runtime/registry/google-analytics.ts @@ -141,8 +141,13 @@ export function useScriptGoogleAnalytics(_options? // eslint-disable-next-line prefer-rest-params w[dataLayerName].push(arguments) } - if (options?.defaultConsent) - w.gtag('consent', 'default', options.defaultConsent) + if (options?.defaultConsent) { + const entries = Array.isArray(options.defaultConsent) + ? options.defaultConsent + : [options.defaultConsent] + for (const entry of entries) + w.gtag('consent', 'default', entry) + } // eslint-disable-next-line ts/ban-ts-comment // @ts-ignore _options?.onBeforeGtagStart?.(w.gtag) diff --git a/packages/script/src/runtime/registry/google-tag-manager.ts b/packages/script/src/runtime/registry/google-tag-manager.ts index f299269b..a799275b 100644 --- a/packages/script/src/runtime/registry/google-tag-manager.ts +++ b/packages/script/src/runtime/registry/google-tag-manager.ts @@ -141,12 +141,19 @@ export function useScriptGoogleTagManager( // Assign gtag to window for global access (window as any).gtag = gtag + // Set consent defaults before any user-callback gtag/dataLayer pushes, + // so custom events in onBeforeGtmStart honor the configured consent state. + if (opts.defaultConsent) { + const entries = Array.isArray(opts.defaultConsent) + ? opts.defaultConsent + : [opts.defaultConsent] + for (const entry of entries) + gtag('consent', 'default', entry) + } + // Allow custom initialization options?.onBeforeGtmStart?.(gtag) - if (opts.defaultConsent) - gtag('consent', 'default', opts.defaultConsent) - // Push the standard GTM initialization event ;(window as any)[dataLayerName].push({ 'gtm.start': Date.now(), diff --git a/packages/script/src/runtime/registry/schemas.ts b/packages/script/src/runtime/registry/schemas.ts index 796c6ef4..cb0306a3 100644 --- a/packages/script/src/runtime/registry/schemas.ts +++ b/packages/script/src/runtime/registry/schemas.ts @@ -306,10 +306,12 @@ export const GoogleAnalyticsOptions = object({ */ l: optional(string()), /** - * Default GCMv2 consent state fired as `gtag('consent', 'default', ...)` before `gtag('js', ...)`. - * @see https://developers.google.com/tag-platform/security/guides/consent + * Default GCMv2 consent state(s) fired as `gtag('consent', 'default', state)` before + * `gtag('js', ...)`. Pass an array to fire multiple defaults — for example, different + * defaults per `region` (more specific regions override broader ones at runtime). + * @see https://developers.google.com/tag-platform/security/guides/consent?consentmode=advanced#region-specific-behavior */ - defaultConsent: optional(gcmConsentState), + defaultConsent: optional(union([gcmConsentState, array(gcmConsentState)])), }) export const GoogleMapsOptions = object({ @@ -476,10 +478,13 @@ export const GoogleTagManagerOptions = object({ authReferrerPolicy: optional(string()), /** - * Default consent settings for GTM + * Default GCMv2 consent state(s) fired as `['consent','default', state]` onto the dataLayer + * before the `gtm.js` event. Pass an array to fire multiple defaults — for example, + * different defaults per `region` (more specific regions override broader ones at runtime). * @see https://developers.google.com/tag-platform/tag-manager/templates/consent-apis + * @see https://developers.google.com/tag-platform/security/guides/consent?consentmode=advanced#region-specific-behavior */ - defaultConsent: optional(record(string(), union([string(), number()]))), + defaultConsent: optional(union([gcmConsentState, array(gcmConsentState)])), }) export const HotjarOptions = object({ diff --git a/packages/script/src/runtime/types.ts b/packages/script/src/runtime/types.ts index 4d01f6a8..b451b088 100644 --- a/packages/script/src/runtime/types.ts +++ b/packages/script/src/runtime/types.ts @@ -70,6 +70,10 @@ export interface ConsentState { functionality_storage?: ConsentCategoryValue personalization_storage?: ConsentCategoryValue security_storage?: ConsentCategoryValue + /** Region/subdivision codes (ISO 3166-1 alpha-2 or `XX-YY`) this default applies to. */ + region?: string[] + /** Milliseconds to wait for `consent.update()` before firing queued tags. */ + wait_for_update?: number } export type UseScriptContext, C = unknown> = VueScriptInstance & { diff --git a/test/nuxt-runtime/consent-default.nuxt.test.ts b/test/nuxt-runtime/consent-default.nuxt.test.ts index 4620bb24..0cc6374b 100644 --- a/test/nuxt-runtime/consent-default.nuxt.test.ts +++ b/test/nuxt-runtime/consent-default.nuxt.test.ts @@ -81,6 +81,67 @@ describe('consent defaults — clientInit ordering', () => { expect(dl[consentIdx][2]).toMatchObject({ analytics_storage: 'denied', ad_storage: 'denied' }) }) + it('gtm: array form fires multiple ["consent","default",…] entries in input order, all before gtm.js', async () => { + const { useScriptGoogleTagManager } = await import('../../packages/script/src/runtime/registry/google-tag-manager') + const result: any = useScriptGoogleTagManager({ + id: 'GTM-XXXX', + defaultConsent: [ + { analytics_storage: 'denied', region: ['ES', 'US-AK'], wait_for_update: 500 }, + { ad_storage: 'denied' }, + ], + }) + + result._opts.clientInit() + + const dl = (window as any).dataLayer as any[] + expect(Array.isArray(dl)).toBe(true) + + const consentEntries = dl + .map((e, i) => ({ e, i })) + .filter(({ e }) => Array.isArray(e) && e[0] === 'consent' && e[1] === 'default') + const startIdx = dl.findIndex(e => e && typeof e === 'object' && !Array.isArray(e) && e.event === 'gtm.js') + + expect(consentEntries).toHaveLength(2) + expect(startIdx).toBeGreaterThanOrEqual(0) + for (const { i } of consentEntries) expect(i).toBeLessThan(startIdx) + expect(consentEntries[0].e[2]).toMatchObject({ analytics_storage: 'denied', region: ['ES', 'US-AK'], wait_for_update: 500 }) + expect(consentEntries[1].e[2]).toMatchObject({ ad_storage: 'denied' }) + }) + + it('gtm: single-entry array is observationally equivalent to a bare object', async () => { + const { useScriptGoogleTagManager } = await import('../../packages/script/src/runtime/registry/google-tag-manager') + + const runWith = (defaultConsent: any) => { + delete (window as any).dataLayer + const result: any = useScriptGoogleTagManager({ id: 'GTM-XXXX', defaultConsent }) + result._opts.clientInit() + const dl = (window as any).dataLayer as any[] + const startIdx = dl.findIndex(e => e && typeof e === 'object' && !Array.isArray(e) && e.event === 'gtm.js') + // Slice up to (but not including) gtm.js — the entry carries a Date.now() timestamp + // that differs between calls. Ordering relative to gtm.js is locked by the sibling test. + return dl.slice(0, startIdx) + } + + const fromObject = runWith({ ad_storage: 'denied' }) + const fromArray = runWith([{ ad_storage: 'denied' }]) + + expect(fromArray).toEqual(fromObject) + }) + + it('gtm: empty defaultConsent array is a no-op (no consent default entries)', async () => { + const { useScriptGoogleTagManager } = await import('../../packages/script/src/runtime/registry/google-tag-manager') + const result: any = useScriptGoogleTagManager({ id: 'GTM-XXXX', defaultConsent: [] }) + + result._opts.clientInit() + + const dl = (window as any).dataLayer as any[] + const consentEntries = dl.filter(e => Array.isArray(e) && e[0] === 'consent' && e[1] === 'default') + const startIdx = dl.findIndex(e => e && typeof e === 'object' && !Array.isArray(e) && e.event === 'gtm.js') + + expect(consentEntries).toHaveLength(0) + expect(startIdx).toBeGreaterThanOrEqual(0) + }) + it('matomo: "required" pushes requireConsent before setSiteId', async () => { ;(window as any)._paq = [] const { useScriptMatomoAnalytics } = await import('../../packages/script/src/runtime/registry/matomo-analytics') @@ -164,6 +225,69 @@ describe('consent defaults — clientInit ordering', () => { // reliably resolve inside happy-dom's module-mocked environment. We verify the // behaviour end-to-end in the playground instead; unit coverage for posthog // stays on the per-script consent object below. + + it('ga: array form fires multiple gtag("consent","default",…) calls before gtag("js",…)', async () => { + const { useScriptGoogleAnalytics } = await import('../../packages/script/src/runtime/registry/google-analytics') + const result: any = useScriptGoogleAnalytics({ + id: 'G-XXXXXXXX', + defaultConsent: [ + { analytics_storage: 'denied', region: ['ES', 'US-AK'], wait_for_update: 500 }, + { ad_storage: 'denied' }, + ], + }) + + result._opts.clientInit() + + // gtag pushes its `arguments` object onto window.dataLayer. + // Convert each pushed `arguments` to a real array for inspection. + const dl = ((window as any).dataLayer as any[]).map(e => Array.from(e)) + + const consentEntries = dl + .map((e, i) => ({ e, i })) + .filter(({ e }) => e[0] === 'consent' && e[1] === 'default') + const jsIdx = dl.findIndex(e => e[0] === 'js') + + expect(consentEntries).toHaveLength(2) + expect(jsIdx).toBeGreaterThanOrEqual(0) + for (const { i } of consentEntries) expect(i).toBeLessThan(jsIdx) + expect(consentEntries[0].e[2]).toMatchObject({ analytics_storage: 'denied', region: ['ES', 'US-AK'], wait_for_update: 500 }) + expect(consentEntries[1].e[2]).toMatchObject({ ad_storage: 'denied' }) + }) + + it('ga: single-entry array is observationally equivalent to a bare object', async () => { + const { useScriptGoogleAnalytics } = await import('../../packages/script/src/runtime/registry/google-analytics') + + const runWith = (defaultConsent: any) => { + delete (window as any).dataLayer + delete (window as any).gtag + const result: any = useScriptGoogleAnalytics({ id: 'G-XXXXXXXX', defaultConsent }) + result._opts.clientInit() + const dl = ((window as any).dataLayer as any[]).map(e => Array.from(e)) + const jsIdx = dl.findIndex(e => e[0] === 'js') + // Slice up to (but not including) gtag('js', new Date()) — the Date differs between + // calls. Ordering relative to gtag('js', …) is locked by the sibling test. + return dl.slice(0, jsIdx) + } + + const fromObject = runWith({ ad_storage: 'denied' }) + const fromArray = runWith([{ ad_storage: 'denied' }]) + + expect(fromArray).toEqual(fromObject) + }) + + it('ga: empty defaultConsent array is a no-op (no consent default calls)', async () => { + const { useScriptGoogleAnalytics } = await import('../../packages/script/src/runtime/registry/google-analytics') + const result: any = useScriptGoogleAnalytics({ id: 'G-XXXXXXXX', defaultConsent: [] }) + + result._opts.clientInit() + + const dl = ((window as any).dataLayer as any[]).map(e => Array.from(e)) + const consentEntries = dl.filter(e => e[0] === 'consent' && e[1] === 'default') + const jsIdx = dl.findIndex(e => e[0] === 'js') + + expect(consentEntries).toHaveLength(0) + expect(jsIdx).toBeGreaterThanOrEqual(0) // gtag('js', …) still fires + }) }) describe('per-script consent object', () => {