Skip to content

feat(gtm,ga): consent.default() + strict GCMv2 validation#772

Merged
harlan-zw merged 6 commits into
mainfrom
feat/gcm-consent-contract
May 14, 2026
Merged

feat(gtm,ga): consent.default() + strict GCMv2 validation#772
harlan-zw merged 6 commits into
mainfrom
feat/gcm-consent-contract

Conversation

@harlan-zw
Copy link
Copy Markdown
Collaborator

@harlan-zw harlan-zw commented May 14, 2026

🔗 Linked issue

Related to #745 (discussion) — the consent debugging confusion that motivated tightening this surface.

❓ Type of change

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

📚 Description

GA + GTM each hand-rolled their own consent.update() attachment, and unknown GCMv2 keys / non-granted/denied values failed silently. This adds a shared gcmConsent opt-in contract on useRegistryScript — both scripts now declare gcmConsent: { push } and get a typed consent.default/update API attached automatically, with every call validated against the canonical schema (gcmConsentState upgraded to strictObject) and warned via consola on bad input. As a bonus this fixes the GTM onBeforeGtmStart double-invocation on first init.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
scripts-playground Ready Ready Preview, Comment May 14, 2026 3:59am

@harlan-zw harlan-zw changed the title feat(gtm,ga): GCMv2 consent contract with consent.default() + strict validation feat(gtm,ga): consent.default() + strict GCMv2 validation May 14, 2026
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 14, 2026

Open in StackBlitz

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

commit: f93bf74

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 54c24e4f-b18c-4236-ba8d-0a10c769ce45

📥 Commits

Reviewing files that changed from the base of the PR and between 4bd13f0 and f93bf74.

📒 Files selected for processing (2)
  • packages/script/src/runtime/registry/_gcm-consent.ts
  • packages/script/src/runtime/registry/schemas.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/script/src/runtime/registry/_gcm-consent.ts

📝 Walkthrough

Walkthrough

This PR unifies consent handling for Google Analytics and Google Tag Manager by introducing a shared GcmConsentApi type contract with strict schema validation. A new _gcm-consent.ts module exports validation and attachment helpers that wire consent.default() and consent.update() onto script instances. The Google Analytics and Google Tag Manager registry implementations now use the shared API and forward consent actions via gcmConsent.push handlers. The registry utilities enable opt-in GCM consent integration, and documentation and tests are updated to cover runtime-derived defaults and validation warnings.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 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 accurately summarizes the primary changes: adding consent.default() and implementing strict GCMv2 validation across GA and GTM.
Description check ✅ Passed The description clearly relates to the changeset, explaining the shared gcmConsent contract, validation behavior, and the migration of GA/GTM to use it.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gcm-consent-contract

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

🧹 Nitpick comments (2)
docs/content/docs/1.guides/3.consent.md (1)

92-92: 💤 Low value

Consider aligning validation wording with vendor-specific docs.

Line 92 describes validation as "validated and warned via consola", while the GA and GTM docs specify "validated against the canonical GCMv2 schema and warned via consola". Adding "against the canonical GCMv2 schema" here would improve consistency across the documentation set.

📝 Suggested wording
-Every consent-aware `useScriptX()`{lang="ts"} returns a `consent` object typed to the vendor's native API. Combine it with `defaultConsent` for the initial policy (applied in `clientInit` before the vendor fires its first call) and call `consent.*` from your cookie banner to update. For GCMv2 scripts (Google Analytics, Google Tag Manager), `consent.default(state)`{lang="ts"} is also available for runtime-derived defaults; unknown GCMv2 keys and non-`granted`/`denied` values are validated and warned via `consola`.
+Every consent-aware `useScriptX()`{lang="ts"} returns a `consent` object typed to the vendor's native API. Combine it with `defaultConsent` for the initial policy (applied in `clientInit` before the vendor fires its first call) and call `consent.*` from your cookie banner to update. For GCMv2 scripts (Google Analytics, Google Tag Manager), `consent.default(state)`{lang="ts"} is also available for runtime-derived defaults; unknown GCMv2 keys and non-`granted`/`denied` values are validated against the canonical GCMv2 schema and warned via `consola`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/content/docs/1.guides/3.consent.md` at line 92, Update the phrasing in
the consent docs to match vendor docs: change the sentence about validation to
state that unknown GCMv2 keys and non-`granted`/`denied` values are "validated
against the canonical GCMv2 schema and warned via `consola`", preserving
references to useScriptX(), `defaultConsent`, and `consent.default(state)` so
readers can locate the related examples and behavior.
packages/script/src/runtime/registry/google-analytics.ts (1)

135-137: ⚡ Quick win

Use shared ConsentState in the transport callback to avoid type drift.

gcmConsent.push currently types state as local ConsentOptions. Using the shared ConsentState here keeps the transport contract aligned with the canonical consent schema.

Suggested patch
-import type { RegistryScriptInput, UseScriptContext } from '#nuxt-scripts/types'
+import type { ConsentState, RegistryScriptInput, UseScriptContext } from '#nuxt-scripts/types'
@@
       gcmConsent: {
-        push: (proxy: any, action: 'default' | 'update', state: ConsentOptions) => (proxy as GoogleAnalyticsApi).gtag('consent', action, state),
+        push: (proxy: any, action: 'default' | 'update', state: ConsentState) => (proxy as GoogleAnalyticsApi).gtag('consent', action, state),
       },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/script/src/runtime/registry/google-analytics.ts` around lines 135 -
137, The transport callback gcmConsent.push currently types its state parameter
as the local ConsentOptions, causing type drift; update the signature to use the
shared ConsentState type instead (replace ConsentOptions with ConsentState in
the gcmConsent.push declaration and any related imports/usage), ensuring the
transport contract matches the canonical consent schema and updating any import
statements to reference the module that exports ConsentState.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@docs/content/docs/1.guides/3.consent.md`:
- Line 92: Update the phrasing in the consent docs to match vendor docs: change
the sentence about validation to state that unknown GCMv2 keys and
non-`granted`/`denied` values are "validated against the canonical GCMv2 schema
and warned via `consola`", preserving references to useScriptX(),
`defaultConsent`, and `consent.default(state)` so readers can locate the related
examples and behavior.

In `@packages/script/src/runtime/registry/google-analytics.ts`:
- Around line 135-137: The transport callback gcmConsent.push currently types
its state parameter as the local ConsentOptions, causing type drift; update the
signature to use the shared ConsentState type instead (replace ConsentOptions
with ConsentState in the gcmConsent.push declaration and any related
imports/usage), ensuring the transport contract matches the canonical consent
schema and updating any import statements to reference the module that exports
ConsentState.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 16f935e4-d9ad-4a08-ad60-31bf9f0372b9

📥 Commits

Reviewing files that changed from the base of the PR and between 727e025 and 5cbe7ee.

📒 Files selected for processing (10)
  • docs/content/docs/1.guides/3.consent.md
  • docs/content/scripts/google-analytics.md
  • docs/content/scripts/google-tag-manager.md
  • packages/script/src/runtime/registry/_gcm-consent.ts
  • 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
  • packages/script/src/runtime/utils.ts
  • test/nuxt-runtime/consent-default.nuxt.test.ts

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.

🧹 Nitpick comments (1)
test/nuxt-runtime/consent-default.nuxt.test.ts (1)

362-385: ⚡ Quick win

Expand strict-validation tests to cover invalid values and blocked forwarding.

These tests currently validate unknown-key warnings only. Add cases for invalid values (not granted/denied) and assert invalid payloads are not forwarded to dataLayer, so strict GCMv2 behavior is protected end-to-end.

Suggested test extension
+  it('gtm: validateConsentState warns and blocks invalid GCMv2 values in consent.update()', async () => {
+    ;(window as any).dataLayer = []
+    const { logger } = await import('../../packages/script/src/runtime/logger') as any
+    logger.warn.mockClear()
+    const { useScriptGoogleTagManager } = await import('../../packages/script/src/runtime/registry/google-tag-manager')
+    const result: any = useScriptGoogleTagManager({ id: 'GTM-XXXX' })
+    result._opts.clientInit()
+
+    const before = ((window as any).dataLayer as any[]).length
+    result.consent.update({ ad_storage: 'invalid' } as any)
+
+    expect(logger.warn).toHaveBeenCalled()
+    expect(((window as any).dataLayer as any[]).length).toBe(before)
+  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/nuxt-runtime/consent-default.nuxt.test.ts` around lines 362 - 385, Add
tests that besides checking unknown-key warnings also submit GCMv2 payloads
containing invalid values (e.g., not "granted"/"denied") to both
useScriptGoogleTagManager(...).consent.default(...) and .consent.update(...),
and assert logger.warn is called with the offending key/value (use the existing
logger.warn mock and expect.stringContaining). Also assert that invalid payloads
are not forwarded to window.dataLayer (verify dataLayer stays empty or unchanged
after calling result._opts.clientInit() and then result.consent.default(...) /
result.consent.update(...)). Reuse the same setup steps (window.dataLayer = [],
import logger, call useScriptGoogleTagManager and result._opts.clientInit) and
add these new expectations alongside the existing unknown-key tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/nuxt-runtime/consent-default.nuxt.test.ts`:
- Around line 362-385: Add tests that besides checking unknown-key warnings also
submit GCMv2 payloads containing invalid values (e.g., not "granted"/"denied")
to both useScriptGoogleTagManager(...).consent.default(...) and
.consent.update(...), and assert logger.warn is called with the offending
key/value (use the existing logger.warn mock and expect.stringContaining). Also
assert that invalid payloads are not forwarded to window.dataLayer (verify
dataLayer stays empty or unchanged after calling result._opts.clientInit() and
then result.consent.default(...) / result.consent.update(...)). Reuse the same
setup steps (window.dataLayer = [], import logger, call
useScriptGoogleTagManager and result._opts.clientInit) and add these new
expectations alongside the existing unknown-key tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4127be36-2cf1-42d4-b94b-f9c153b527bd

📥 Commits

Reviewing files that changed from the base of the PR and between 5cbe7ee and ef4f65c.

📒 Files selected for processing (4)
  • docs/content/docs/1.guides/3.consent.md
  • docs/content/scripts/google-analytics.md
  • docs/content/scripts/google-tag-manager.md
  • test/nuxt-runtime/consent-default.nuxt.test.ts
✅ Files skipped from review due to trivial changes (3)
  • docs/content/docs/1.guides/3.consent.md
  • docs/content/scripts/google-analytics.md
  • docs/content/scripts/google-tag-manager.md

harlan-zw added 4 commits May 14, 2026 13:40
…t validation

- Add `GcmConsentApi` ({ default, update }) and a shared `gcmConsent`
  opt-in contract for registry scripts. `useRegistryScript` auto-attaches
  `consent.default/update` post-creation and runs every call through the
  canonical GCMv2 schema, warning on unknown keys / bad values via consola.
- `gcmConsentState` upgraded to `strictObject` so typo'd keys
  (e.g. `analytic_storage`) and non-`granted`/`denied` values are caught.
- GTM + GA refactored to declare `gcmConsent: { push }` instead of
  hand-rolling their own consent attachment. Removes the double
  `onBeforeGtmStart` invocation on first init.
- Docs updated to document `consent.default()` and the strict validation.
Leaked in from an unrelated debug-mode branch; the consent test only
exercises the GCMv2 validation path, which uses logger.warn (always
emitted), so the build-time debug constant is unnecessary here.
…tion

- GTM consent.default() queues an Arguments object on dataLayer (the
  gtag.js contract from #771), mirroring the existing consent.update()
  Arguments assertion.
- GA consent.default() pushes gtag('consent','default', state).
- validateConsentState warns on bad GCMv2 *values* (e.g. `ad_storage: 'allow'`),
  not just unknown keys.
- GA gcmConsent.push now types `state` as the canonical `ConsentState`
  (cast to local `ConsentOptions` at the gtag boundary) to keep the
  contract aligned across vendors.
- Consent guide L92 mirrors the vendor-doc phrasing: "validated against
  the canonical GCMv2 schema and warned via `consola`".
@harlan-zw
Copy link
Copy Markdown
Collaborator Author

Addressed two of three CodeRabbit nits in 4bd13f0e:

  • GA gcmConsent.push type drift → switched to canonical ConsentState, cast to ConsentOptions at the gtag call.
  • 3.consent.md L92 wording → mirrored vendor docs ("validated against the canonical GCMv2 schema").

Skipping "invalid payloads should be blocked from forwarding": validation is intentionally non-blocking — silently dropping a malformed payload would be worse UX than warning + forwarding (the existing dataLayer behaviour is what users debug against). The new gtm: validateConsentState warns on bad consent values test in 3b2754b7 already covers the warn-on-invalid-value case.

`gcmConsentState` reverts to `object()` so `defaultConsent` schema parses
don't throw on typos (avoiding a dev-only soft break for misconfigured
users). The runtime `consent.*` API rebuilds a `strictObject` from the
same entries, so unknown keys / non-`granted`/`denied` values still
surface as a `consola` warning — just without crashing the dev parse.
@harlan-zw harlan-zw merged commit 6f74a6c into main May 14, 2026
19 checks passed
@harlan-zw harlan-zw deleted the feat/gcm-consent-contract branch May 14, 2026 04:08
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.

1 participant