Skip to content

feat(google-analytics, google-tag-manager): per-region consent defaults#739

Open
zizzfizzix wants to merge 4 commits intonuxt:mainfrom
zizzfizzix:feat/regional-consent-defaults
Open

feat(google-analytics, google-tag-manager): per-region consent defaults#739
zizzfizzix wants to merge 4 commits intonuxt:mainfrom
zizzfizzix:feat/regional-consent-defaults

Conversation

@zizzfizzix
Copy link
Copy Markdown
Contributor

@zizzfizzix zizzfizzix commented May 2, 2026

🔗 Linked issue

None — searched open and closed issues; no existing tracker for this.

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

📚 Description

Extends the defaultConsent API introduced in #712 so that useScriptGoogleAnalytics and useScriptGoogleTagManager accept an array of GCMv2 states, not just a single object. Each entry fires its own gtag('consent', 'default', entry) call, matching Google's documented multi-call pattern for region-specific consent.

useScriptGoogleTagManager({
  id: 'GTM-XXXX',
  defaultConsent: [
    {
      ad_storage: 'denied',
      analytics_storage: 'denied',
      region: ['ES', 'US-AK', /* … EEA codes */],
      wait_for_update: 500,
    },
    { ad_storage: 'granted', analytics_storage: 'granted' }, // unscoped global fallback
  ],
})

Backwards compatible: passing a single object behaves exactly as before. Single-entry arrays are observationally equivalent to the bare object (locked by a test). Empty arrays are a documented no-op.

What changed

  • GA + GTM `defaultConsent` widened to `union(gcmConsentState, array(gcmConsentState))`. Runtime normalizes to an array and iterates.
  • GTM schema strictening (drive-by): the previous `record(string, string|number)` was looser than GA's already-canonical `gcmConsentState` and didn't model `region` as an array. GTM now uses the same canonical type as GA. Schema validation only runs in dev, so the runtime impact is dev-mode warnings only — but anyone passing arbitrary unknown keys in `defaultConsent` today will see a new dev validation error.
  • `ConsentState` interface (drive-by): gains `region: string[]` and `wait_for_update: number` to match what the runtime schema already accepts.
  • GTM `@see` URL alignment (drive-by): both schemas now point at the canonical `?consentmode=advanced#region-specific-behavior` Google docs anchor.
  • `registry-types.json` regenerated via `pnpm generate:types` to reflect the schema changes.
  • New tests in `test/nuxt-runtime/consent-default.nuxt.test.ts`: array ordering vs. `gtm.js` / `gtag('js', …)`, single-entry-array == bare-object equivalence, empty-array no-op. Three for GTM, three for GA. Also closes a pre-existing gap where GA had no consent-ordering test.
  • Docs updated on both GA and GTM script pages with a "Per-region defaults" section + worked example. The GA page's previously-h3 `Consent Mode` section was promoted to h2 to match the cross-page convention; `Customer/Consumer ID Tracking` was promoted along with it to keep the heading hierarchy honest.
  • New example `examples/regional-consent/` showing EEA-denied + global-granted defaults.

Out of scope

  • Bing UET. Microsoft's UET consent mode docs do not support a `region` field; supporting multi-call there without a vendor spec to back it would mislead users. Easy follow-up if Microsoft documents region support later.
  • All other consent-aware scripts (Matomo, Clarity, PostHog, Mixpanel, TikTok Pixel, Meta Pixel) — they have non-GCMv2 consent shapes; per-region work for those would need separate vendor research.

zizzfizzix added 3 commits May 1, 2026 16:11
Allow `defaultConsent` to accept an array of GCMv2 states; each entry
fires its own `['consent','default', …]` push onto the dataLayer before
`gtm.js`, matching Google's documented multi-call pattern for region-
specific defaults.

- Widen `GoogleTagManagerOptions.defaultConsent` from a loose
  `record(string, string|number)` to the canonical
  `union(gcmConsentState, array(gcmConsentState))`. Drive-by tightening:
  unknown keys now fail dev-mode schema validation.
- Augment shared `ConsentState` with the already-runtime-supported
  `region: string[]` and `wait_for_update: number` fields.
- Add three tests: array ordering vs. `gtm.js`, single-entry-array ==
  bare-object equivalence, and empty-array no-op.

Backwards compatible: existing single-object callers behave identically.

Refs: https://developers.google.com/tag-platform/security/guides/consent
Allow `defaultConsent` on `useScriptGoogleAnalytics` to accept an array
of GCMv2 states; each entry fires its own `gtag('consent','default', …)`
before `gtag('js', …)`, matching Google's documented multi-call pattern
for region-specific defaults.

- Widen `GoogleAnalyticsOptions.defaultConsent` to
  `union(gcmConsentState, array(gcmConsentState))`.
- Add three tests mirroring the GTM suite: array ordering vs.
  `gtag('js', …)`, single-entry-array == bare-object equivalence, and
  empty-array no-op. Also closes a pre-existing gap where GA had no
  consent-ordering test at all.

Backwards compatible: existing single-object callers behave identically.

Refs: https://developers.google.com/tag-platform/security/guides/consent
…nt defaults

- Document the new array form of `defaultConsent` on both
  `useScriptGoogleAnalytics` and `useScriptGoogleTagManager` pages,
  including a per-region worked example using `region` and
  `wait_for_update`.
- Add `examples/regional-consent` (mirrors `examples/cookie-consent`)
  showing an EEA-denied + global-granted setup.
- Link the new example from both doc pages alongside the existing
  StackBlitz callouts.
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 2, 2026

@zizzfizzix is attempting to deploy a commit to the Nuxt Team on Vercel.

A member of the Team first needs to authorize it.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 2, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@nuxt/scripts@739

commit: a8bef9a

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

📝 Walkthrough

Walkthrough

This pull request adds support for array-based defaultConsent to Google Analytics and Google Tag Manager integrations, allowing multiple (e.g., region-specific) default consent pushes. Changes include documentation updates with examples, a new examples/regional-consent Nuxt example, schema/type updates to accept a single consent state or an array, runtime updates in GA and GTM to normalize and emit one consent-default push per entry, added region and wait_for_update fields on ConsentState, and tests verifying ordering and behavior for array, single-entry, and empty defaultConsent values.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(google-analytics, google-tag-manager): per-region consent defaults' directly and clearly summarizes the main change: extending defaultConsent to support per-region configuration via arrays of consent states.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description comprehensively explains the feature, scope, backwards compatibility, and implementation details aligned with the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
examples/regional-consent/package.json (1)

11-15: ⚡ Quick win

Pin @nuxt/scripts instead of latest.

This example will drift independently of the repo if the module resolves to a moving target, which can break install/preview reproducibility.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/regional-consent/package.json` around lines 11 - 15, The dependency
"@nuxt/scripts" in package.json is set to the moving tag "latest"; replace that
with a pinned version string (an exact version like "x.y.z" or the same version
used elsewhere in the repo) to ensure reproducible installs—update the
"@nuxt/scripts" entry to a fixed version instead of "latest".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/script/src/registry-types.json`:
- Around line 1634-1638: The "defaultConsent" entry's description was truncated
ending with a dangling "See:"; update the description field for the
defaultConsent property to include the missing reference text (or remove the
trailing "See:" and append the full documentation link or explanatory sentence)
so the metadata is complete and meaningful, and ensure the JSON string is
properly escaped and the registry-types.json remains valid JSON.

In `@packages/script/src/runtime/registry/google-tag-manager.ts`:
- Around line 144-153: Move the default consent initialization to occur before
invoking the user callback so privacy defaults are in place prior to any
dataLayer/gtag calls; specifically, in google-tag-manager.ts set
opts.defaultConsent (the block that builds entries and calls gtag('consent',
'default', entry)) before calling options?.onBeforeGtmStart?.(gtag), ensuring
the existing logic that handles Array.isArray(opts.defaultConsent) and the gtag
calls remains unchanged aside from its new position.

In `@test/nuxt-runtime/consent-default.nuxt.test.ts`:
- Around line 255-273: The test compares dataLayer entries from
useScriptGoogleAnalytics but currently includes the gtag('js', new Date())
timestamp, making the assertion flaky; modify the runWith helper (used by the
test) to locate the 'js' entry (jsIdx) and remove or normalize the timestamp
argument (e.g. delete the second element or set it to a constant) from dl[jsIdx]
before slicing and returning so that fromObject and fromArray comparisons ignore
the timing-dependent payload.
- Around line 111-127: The test compares two GTM init payloads but fails
intermittently because the generated gtm.start timestamp (from Date.now())
differs; update the test around useScriptGoogleTagManager / runWith (the values
returned as dl and the gtm entry found via startIdx) to normalize the gtm.start
value before asserting equality—e.g., locate the gtm object in the sliced arrays
(the element with event === 'gtm.js' and property gtm.start) and set gtm.start
to a fixed number (or delete it) for both fromObject and fromArray so the
toEqual comparison is deterministic.

---

Nitpick comments:
In `@examples/regional-consent/package.json`:
- Around line 11-15: The dependency "@nuxt/scripts" in package.json is set to
the moving tag "latest"; replace that with a pinned version string (an exact
version like "x.y.z" or the same version used elsewhere in the repo) to ensure
reproducible installs—update the "@nuxt/scripts" entry to a fixed version
instead of "latest".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1f14c479-b78d-4a21-90b5-435ed8f100fd

📥 Commits

Reviewing files that changed from the base of the PR and between 4b480b0 and aa9d27b.

📒 Files selected for processing (13)
  • docs/content/scripts/google-analytics.md
  • docs/content/scripts/google-tag-manager.md
  • examples/regional-consent/app.vue
  • examples/regional-consent/assets/css/main.css
  • examples/regional-consent/nuxt.config.ts
  • examples/regional-consent/package.json
  • examples/regional-consent/tsconfig.json
  • packages/script/src/registry-types.json
  • packages/script/src/runtime/registry/google-analytics.ts
  • packages/script/src/runtime/registry/google-tag-manager.ts
  • packages/script/src/runtime/registry/schemas.ts
  • packages/script/src/runtime/types.ts
  • test/nuxt-runtime/consent-default.nuxt.test.ts

Comment thread packages/script/src/registry-types.json
Comment thread packages/script/src/runtime/registry/google-tag-manager.ts Outdated
Comment thread test/nuxt-runtime/consent-default.nuxt.test.ts
Comment thread test/nuxt-runtime/consent-default.nuxt.test.ts
@harlan-zw
Copy link
Copy Markdown
Collaborator

Looks good, thanks! Are you able to take a look at coderabbit comments and fix if relevant + resolve them.

- GTM `clientInit`: apply `defaultConsent` BEFORE `onBeforeGtmStart` so
  custom events pushed by user callbacks honour the configured consent
  state. Brings GTM in line with GA's existing ordering. Pre-existing
  inconsistency surfaced during code review.
- Tests: drop the `+ 1` from the equivalence-test slices so the
  `gtm.js` / `gtag('js', new Date())` timestamps are no longer compared.
  The Date.now() / new Date() values can diverge across the two
  back-to-back runWith calls, which would intermittently flake `toEqual`.
  Ordering relative to those entries is locked by the sibling tests
  already.
- GTM schema JSDoc: remove the trailing "See:" that introduced the
  `@see` links — JSDoc parsers strip the @see lines from the description
  and surface the dangling "See:" in `registry-types.json`.
- Regenerate `registry-types.json` to pick up the cleaned description.

Refs CodeRabbit review on PR nuxt#739.
@zizzfizzix
Copy link
Copy Markdown
Contributor Author

Addressed CodeRabbit's review in a8bef9a:

  1. GTM defaultConsent before onBeforeGtmStart — applied. Real privacy-correctness fix; brings GTM in line with GA's existing ordering. Custom events pushed inside onBeforeGtmStart now honour the configured consent state.
  2. Equivalence-test slices include the gtm.js / gtag('js', new Date()) timestamp — fixed. Dropped the + 1 from both slices so the timestamps are no longer compared. Ordering relative to those entries is locked by the sibling tests already, so coverage is unchanged.
  3. GTM defaultConsent JSDoc ends with dangling See: — fixed. Rewrote the description so it ends with the parenthetical about precedence; the @see lines are still attached but no longer leak a trailing See: into registry-types.json. Regenerated registry-types.json.
  4. Nitpick: pin @nuxt/scripts instead of latest in examples/regional-consent/package.json — leaving as-is. This matches the existing pattern in examples/cookie-consent/package.json, examples/granular-consent/package.json, etc. Pinning across the examples/* tree is a separate decision worth making consistently or not at all; happy to do it as a follow-up if the maintainer wants.

@coderabbitai please re-review.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

Tip

For best results, initiate chat on the files or code changes.

@zizzfizzix Thanks for addressing the feedback! The fixes look well-reasoned — particularly items 1 (privacy-correctness for GTM consent ordering) and 3 (JSDoc cleanup). The rationale for item 4 (keeping latest consistent with the other examples/* packages) makes sense as a separate follow-up concern.

Kicking off a fresh review now.

[full_review]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants