Skip to content

feat: unified consent management#712

Open
harlan-zw wants to merge 12 commits intomainfrom
worktree-consent-mode-unified
Open

feat: unified consent management#712
harlan-zw wants to merge 12 commits intomainfrom
worktree-consent-mode-unified

Conversation

@harlan-zw
Copy link
Copy Markdown
Collaborator

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

Linked issue

Resolves #711
References #243, #710

Type of change

  • Documentation
  • Bug fix
  • Enhancement
  • New feature
  • Chore
  • Breaking change

Summary

Unifies cookie consent across the registry so a single composable can drive Google Consent Mode v2 granular state for every consent-aware third-party script at once. Previously each vendor was wired inconsistently (GA had onBeforeGtagStart, Bing had onBeforeUetStart, TikTok had nothing, Matomo/Mixpanel/PostHog each spoke their own dialect). One cookie banner now fans out to all of them.

What's new

useScriptConsent composable

Superset of useScriptTriggerConsent. Keeps the binary load-gate working verbatim, layers in GCMv2 granular state plus adapter fan-out.

const consent = useScriptConsent({
  default: { ad_storage: 'denied', analytics_storage: 'denied' },
})

await consent                   // resolves on first grant (existing load-gate semantics)
consent.accept() / revoke()     // grant/deny all categories
consent.consented               // Ref<boolean>, true if any category granted
consent.state                   // Ref<ConsentState>
consent.update({ analytics_storage: 'granted' })  // merged + fanned out to adapters

Pass it into any registry script and the script subscribes automatically:

useScriptGoogleTagManager({ id: 'GTM-XXX', scriptOptions: { consent } })
useScriptMetaPixel({ id: '123', scriptOptions: { consent } })

Per-script defaultConsent option

Every consent-aware script now accepts a typed defaultConsent applied inside clientInit before the vendor init / first tracking call. Useful without the composable for static policies.

Script defaultConsent shape Categories observed
Google Analytics Partial<ConsentState> (GCMv2) full pass-through
Google Tag Manager Partial<ConsentState> (GCMv2) full pass-through
Bing UET { ad_storage } ad_storage
Meta Pixel 'granted' | 'denied' ad_storage
TikTok Pixel 'granted' | 'denied' ad_storage
Matomo 'required' | 'given' | 'not-required' analytics_storage
Mixpanel 'opt-in' | 'opt-out' analytics_storage
PostHog 'opt-in' | 'opt-out' analytics_storage
Clarity boolean | Record<string, string> analytics_storage

useScriptTriggerConsent deprecation

Replaced by a @deprecated shim that delegates to useScriptConsent and emits a dev-only warning. All existing call sites work verbatim, rename is the only required change.

Migration

- const consent = useScriptTriggerConsent({
+ const consent = useScriptConsent({
    consent: agreedToCookies,
    postConsentTrigger: 'onNuxtReady',
+   default: { ad_storage: 'denied' }, // optional, new
  })

Full guide: docs/content/docs/1.guides/3.consent.md (includes OneTrust/Cookiebot recipes + vendor mapping table).

Not shipped

  • Reddit Pixel, X Pixel: no documented runtime consent API, skipped.
  • Snapchat Pixel: snaptr('grantConsent') exists anecdotally but was not verified against vendor docs. Easy follow-up.

Design notes

  • GCMv2 is the canonical schema. Non-GCM vendors (Meta, TikTok, Matomo, Mixpanel, PostHog, Clarity) project a lossy subset via their adapter; mapping is documented per-script.
  • Precedence: if both consent (composable) and defaultConsent (per-script) are set, the composable wins; a dev-only warning fires.
  • Escape hatches (onBeforeGtagStart, onBeforeUetStart) retained.
  • Adapter definitions live in a dedicated consent-adapters.ts using type-only imports so registry.ts (build-time) stays decoupled from runtime utilities.

Test plan

  • useScriptConsent unit tests: default state, update merging, batching, binary compat with useScriptTriggerConsent
  • Per-script defaultConsent tests: vendor call appears in clientInit before init/track
  • Per-adapter projection tests: applyDefault / applyUpdate map GCM state to vendor calls correctly
  • Existing useScriptTriggerConsent tests pass unchanged through the deprecation shim
  • Manual: cookie banner + granular toggle with GA + GTM + Meta + TikTok loaded

…scripts

Unify per-script native consent APIs under a single defaultConsent option
(fired inside clientInit before init/track) and expose a consentAdapter on
each registry entry for consumption by useScriptConsent (Scope B).

Scripts covered: tiktokPixel (#711), metaPixel, googleAnalytics, bingUet,
clarity. Adds shared ConsentState/ConsentAdapter types. GCMv2 is the
canonical schema; non-GCM vendors (Meta, TikTok) project lossy from
ad_storage. Clarity projects from analytics_storage.

Bing's onBeforeUetStart and GA's onBeforeGtagStart are kept as escape
hatches. TikTok now exposes ttq.grantConsent/revokeConsent/holdConsent
stubs on the pre-load queue.
…mo / Mixpanel / PostHog

Adds a shared `ConsentAdapter` contract (`applyDefault`, `applyUpdate`) and
a GCM-style `ConsentState` so `useScriptTriggerConsent` and similar tooling
can drive consent without knowing vendor specifics. Each script now accepts
a typed `defaultConsent` option that resolves BEFORE the vendor init / first
tracking call:

- Google Tag Manager: Partial<ConsentState> (GCMv2) — pushes
  `['consent','default',state]` before the `gtm.js` start event; adapter
  pushes `['consent','update',state]` for runtime changes.
- Matomo: `'required' | 'given' | 'not-required'` — queues
  `requireConsent` (+ `setConsentGiven`) ahead of `setSiteId` / trackPageView;
  adapter maps `analytics_storage` to `setConsentGiven` / `forgetConsentGiven`.
- Mixpanel: `'opt-in' | 'opt-out'` — opt-out passes
  `opt_out_tracking_by_default: true` to `mixpanel.init`; opt-in queues
  `opt_in_tracking`; adapter maps `analytics_storage` to
  `opt_in_tracking` / `opt_out_tracking`.
- PostHog: `'opt-in' | 'opt-out'` — opt-out passes
  `opt_out_capturing_by_default: true` to `posthog.init`; opt-in calls
  `opt_in_capturing()` after init; adapter maps `analytics_storage` to
  `opt_in_capturing` / `opt_out_capturing`.

Tests cover clientInit ordering and adapter behaviour for all four scripts.
Docs updated for Matomo / Mixpanel / PostHog (GTM already documented).
Adds a single `useScriptConsent` composable that supersedes `useScriptTriggerConsent`.
Keeps the existing binary load gate behaviour (`consent` Ref/Promise/boolean plus
`postConsentTrigger`) while adding Google Consent Mode v2 granular state, batched
`update()` fan out, and adapter registration so registry scripts can subscribe to
category changes via their `consentAdapter`.

- New `useScriptConsent` composable with reactive `state`, `update()`, `register()`,
  `accept`/`revoke`, and an awaitable load gate Promise
- `useScriptTriggerConsent` is now a dev-warning shim that delegates to
  `useScriptConsent`; existing call sites remain unchanged
- Adds `ConsentAdapter`, `ConsentState`, `ConsentCategoryValue`, and
  `UseScriptConsentOptions` to the public type surface
- Wires optional `consent` + `_consentAdapter` registration into the base `useScript`
  composable so Scope A's registry adapters auto subscribe
- Unit tests cover default state, merging, batching, registration, binary compat,
  and the migration shim
- Reworks the consent guide with GCMv2 schema, vendor mapping table, OneTrust and
  Cookiebot recipes, and a migration section
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 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 Apr 14, 2026 2:39pm

…ys build-safe

Importing adapter constants from runtime/registry/*.ts pulled utils.ts -> nuxt/app into module evaluation, breaking nuxt-module-build prepare. Adapters are pure data, moved to a dedicated consent-adapters.ts using type-only imports of proxy types.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 14, 2026

Open in StackBlitz

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

commit: 3b68b6c

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

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
📝 Walkthrough

Walkthrough

Adds a new composable useScriptConsent that models granular Google Consent Mode v2 state, supports adapter registration, batched updates, and a promise-like post-consent load gate; useScriptTriggerConsent is retained as a deprecated wrapper. Introduces ConsentState, ConsentAdapter types, per-script consentAdapter hooks, and defaultConsent options across many registry scripts (GTM, GA, Matomo, Mixpanel, PostHog, Meta, TikTok, Bing UET, Clarity). Integrates adapter wiring into useScript, extends Partytown forwarding, updates schemas/registry types, adds consent-adapter implementations, documentation updates, and multiple unit/integration tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.64% 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 PR title 'feat: unified consent management' directly summarizes the main change: introducing a unified composable for managing cookie consent across all registry scripts.
Description check ✅ Passed The PR description is comprehensive and clearly related to the changeset, detailing the new useScriptConsent composable, per-script defaultConsent options, deprecation of useScriptTriggerConsent, and migration guidance with examples.
Linked Issues check ✅ Passed The PR successfully addresses #711 by implementing TikTok Pixel consent mode with defaultConsent supporting 'hold' state and runtime grant/revoke methods (grantConsent/revokeConsent/holdConsent), plus unified consent management across all supported vendors via useScriptConsent composable.
Out of Scope Changes check ✅ Passed All code changes are in scope: documentation updates for consent guides, new useScriptConsent composable, per-script defaultConsent implementations, consent adapters for vendors, and comprehensive test coverage for the unified consent system.

✏️ 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 worktree-consent-mode-unified

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: 10

🧹 Nitpick comments (2)
packages/script/src/runtime/registry/posthog.ts (1)

109-111: Redundant opt_in_capturing() call when defaultConsent: 'opt-in'.

PostHog starts with capturing enabled by default. Calling opt_in_capturing() after init when defaultConsent === 'opt-in' is a no-op in normal circumstances. This explicit call may be intentional for clarity, but it's worth noting that PostHog's default behavior is already to capture.

If the intent is to ensure explicit opt-in state regardless of SDK defaults, consider adding a comment clarifying this:

📝 Suggested comment for clarity
               window.posthog = instance
-              // Apply explicit opt-in AFTER init (opt-out is handled by init config above).
+              // Apply explicit opt-in AFTER init to ensure opted-in state even if PostHog
+              // SDK defaults change in the future. (opt-out is handled by init config above).
               if (options?.defaultConsent === 'opt-in')
                 instance.opt_in_capturing?.()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/script/src/runtime/registry/posthog.ts` around lines 109 - 111, The
explicit call to instance.opt_in_capturing() after init when
options?.defaultConsent === 'opt-in' is redundant because PostHog captures by
default; either remove that call to avoid no-op behavior or, if you intend to
keep it for clarity, add a concise inline comment next to the
instance.opt_in_capturing?.() invocation stating that PostHog defaults to
capturing so this call is only retained to make the opt-in intent explicit
(reference: options?.defaultConsent and instance.opt_in_capturing usage around
the init flow).
docs/content/scripts/matomo-analytics.md (1)

86-119: LGTM!

The Consent Mode documentation is comprehensive, with a clear table explaining the three defaultConsent values and practical runtime consent control examples.

Minor: Static analysis flagged passive voice at lines 88 and 92. These are stylistic suggestions and don't affect clarity, but if you'd like to address them:

📝 Optional: Active voice rewrites
-Matomo has a built-in [tracking-consent API](https://developer.matomo.org/guides/tracking-consent). Nuxt Scripts exposes it via the `defaultConsent` option, which is applied BEFORE the first tracker call.
+Matomo has a built-in [tracking-consent API](https://developer.matomo.org/guides/tracking-consent). Nuxt Scripts exposes it via the `defaultConsent` option, which the module applies BEFORE the first tracker call.
-| `'required'` | Pushes `['requireConsent']`. Nothing is tracked until the user opts in. |
+| `'required'` | Pushes `['requireConsent']`. The tracker records nothing until the user opts in. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/scripts/matomo-analytics.md` around lines 86 - 119, Update the
two passive-voice sentences in the Consent Mode section to active voice: change
the sentence that reads "Nuxt Scripts exposes it via the `defaultConsent`
option, which is applied BEFORE the first tracker call" to an active form like
"Nuxt Scripts exposes the `defaultConsent` option and applies it before the
first tracker call," and change the table row for `'not-required'` from "Default
Matomo behaviour (no consent gating)" to an active phrasing such as "Matomo's
default behaviour (no consent gating)"; keep references to
`useScriptMatomoAnalytics`, `defaultConsent`, and `proxy._paq.push` intact and
ensure examples still compile to the same behaviour.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/content/docs/1.guides/3.consent.md`:
- Around line 110-124: The table claims multiple scripts map GCMv2 categories
via adapters but includes entries without a consentAdapter; update the table to
only list scripts that actually export a consentAdapter (the ones added in this
PR: GA/GTM, Matomo, Mixpanel, PostHog, TikTok, Meta, Bing UET, Clarity) and
remove X Pixel, Reddit Pixel, Snapchat Pixel, Hotjar, Crisp, and Intercom from
the granular-mapping rows; also add a short sentence referencing
useScriptConsent’s binary load-gate behavior for scripts lacking a
consentAdapter so docs match registry behavior (look for the table and the terms
consentAdapter and useScriptConsent to locate where to change).

In `@docs/content/scripts/mixpanel-analytics.md`:
- Around line 91-103: Add the missing opt_in_tracking and opt_out_tracking
method signatures to the MixpanelAnalyticsApi stub so TypeScript users get
proper completion for proxy.mixpanel.opt_in_tracking() and
proxy.mixpanel.opt_out_tracking(); locate the MixpanelAnalyticsApi interface (or
its stub type) and add two optional methods with appropriate signatures (e.g.,
opt_in_tracking?: () => void and opt_out_tracking?: () => void) to match the
runtime Mixpanel SDK while keeping them optional so runtime behavior and
defensive optional chaining remain unchanged.

In `@packages/script/src/registry.ts`:
- Around line 449-463: The Partytown forwards lists are missing the consent
methods used by the consent adapters; update the partytown.forwards arrays for
the mixpanel, tiktokPixel (ttq) and clarity entries to include the consent
method names referenced by the adapters — add "mixpanel.opt_in_tracking" and
"mixpanel.opt_out_tracking" to the mixpanel forwards entry, add
"ttq.grantConsent", "ttq.revokeConsent", and "ttq.holdConsent" to the
tiktokPixel/ttq forwards entry, and add "clarity" (or "clarity.consent" if
explicit method forwarding is required) to the clarity forwards entry so calls
from applyDefault/applyUpdate (mixpanel.opt_in_tracking/opt_out_tracking,
ttq.grantConsent/revokeConsent/holdConsent, and clarity('consent', ...)) are
forwarded across the Partytown boundary.

In `@packages/script/src/runtime/composables/useScriptConsent.ts`:
- Around line 96-101: The flush and registration logic must be made resilient so
one failing adapter doesn't break all; wrap each call to
sub.adapter.applyUpdate(...) inside a try/catch in the flush (the block inside
nextTick) so exceptions from one subscription are caught and logged/ignored and
do not stop iteration, and change register() so it does not add the subscription
to subscriptions before calling sub.adapter.applyDefault(...): call applyDefault
first and only add the subscription to the Set if applyDefault succeeds (or if
you must add first, ensure you remove the subscription from subscriptions in the
catch handler when applyDefault throws); also apply the same per-subscription
try/catch pattern for the applyDefault call path to prevent a bad adapter from
remaining registered.
- Around line 172-198: The watch on `consented` inside `useScriptConsent.ts` is
currently persistent and will re-run `options.postConsentTrigger` on subsequent
accept/revoke cycles; change it to a one-shot by capturing the stop handle
returned by `watch(consented, ...)` and calling that stop() immediately after
you invoke the runner(resolve) (or after any async `postConsentTrigger`
completion) so the watcher is torn down once the Promise resolves; ensure all
branches that call `runner(resolve)` or resolve via
`options.postConsentTrigger.then`/`.then()` call stop() after completion so the
trigger runs only once.

In `@packages/script/src/runtime/registry/mixpanel-analytics.ts`:
- Around line 70-81: The comment and implementation diverge: either update the
comment to state that 'opt-in' skips opt_out flag and calls
mp.push(['opt_in_tracking']) (current behavior), or change the logic so "default
out then flip in" is enforced; to do the latter update the optOutByDefault
computation used by mp.init (symbol: optOutByDefault) so it becomes true
whenever options?.defaultConsent is provided (e.g., optOutByDefault =
options?.defaultConsent !== undefined), then keep the
mp.push(['opt_in_tracking']) branch for options?.defaultConsent === 'opt-in' so
the code will init opted-out and then flip in as described; adjust the comment
accordingly near mp.init and mp.push usage.

In `@packages/script/src/runtime/registry/schemas.ts`:
- Around line 715-721: The docs for the defaultConsent schema are misleading:
update the comment for defaultConsent (and the analogous Mixpanel block) to
explain that 'opt-out' is applied by passing a configuration to init (e.g.,
posthog.init / mixpanel.init) while 'opt-in' is performed after the SDK instance
is returned by calling the instance method (e.g., posthog.opt_in_capturing() /
mixpanel.opt_in_capturing()); mention that 'opt-out' happens during init and
'opt-in' happens post-init to reflect actual sequencing and keep references to
defaultConsent, posthog.init, posthog.opt_in_capturing(), and opt-out init
config (and the Mixpanel equivalents) so reviewers can find the exact docs to
edit.
- Around line 925-930: The JSDoc for the defaultConsent schema mentions
ttq.consent.grant() and ttq.consent.revoke(), but the runtime uses
ttq.grantConsent() and ttq.revokeConsent(); update the comment above the
defaultConsent declaration to reference the correct methods (ttq.grantConsent()
and ttq.revokeConsent()) and keep the rest of the description unchanged so
integrators see the actual API names used by the runtime.

In `@packages/script/src/runtime/registry/tiktok-pixel.ts`:
- Around line 60-67: applyTikTokConsent currently treats missing/undecided
ad_storage as a no-op so TikTok never enters hold mode; update
applyTikTokConsent to explicitly call proxy.ttq.holdConsent() when ad_storage is
undefined/undecided (in addition to calling proxy.ttq.grantConsent() for
'granted' and proxy.ttq.revokeConsent() for 'denied'), and make the same change
in the defaultConsent handling (the branch in the file around the defaultConsent
logic) so the SDK can place TikTok into hold before any init/page events run.

In `@packages/script/src/runtime/types.ts`:
- Around line 175-189: The NuxtUseScriptOptionsSerializable type currently
allows unserializable fields; update the omit list to exclude both consent and
_consentAdapter so they don't appear in serializable configs: locate the
NuxtUseScriptOptionsSerializable type (where other keys are Omitted) and add
"consent" and "_consentAdapter" to that Omit<> union so functions/Refs/Promise
and the internal adapter are removed from the JSON-safe alias used for
defaultScriptOptions and globals.

---

Nitpick comments:
In `@docs/content/scripts/matomo-analytics.md`:
- Around line 86-119: Update the two passive-voice sentences in the Consent Mode
section to active voice: change the sentence that reads "Nuxt Scripts exposes it
via the `defaultConsent` option, which is applied BEFORE the first tracker call"
to an active form like "Nuxt Scripts exposes the `defaultConsent` option and
applies it before the first tracker call," and change the table row for
`'not-required'` from "Default Matomo behaviour (no consent gating)" to an
active phrasing such as "Matomo's default behaviour (no consent gating)"; keep
references to `useScriptMatomoAnalytics`, `defaultConsent`, and
`proxy._paq.push` intact and ensure examples still compile to the same
behaviour.

In `@packages/script/src/runtime/registry/posthog.ts`:
- Around line 109-111: The explicit call to instance.opt_in_capturing() after
init when options?.defaultConsent === 'opt-in' is redundant because PostHog
captures by default; either remove that call to avoid no-op behavior or, if you
intend to keep it for clarity, add a concise inline comment next to the
instance.opt_in_capturing?.() invocation stating that PostHog defaults to
capturing so this call is only retained to make the opt-in intent explicit
(reference: options?.defaultConsent and instance.opt_in_capturing usage around
the init flow).
🪄 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: 48468b70-f4a2-4a58-aae8-a994f6d0262f

📥 Commits

Reviewing files that changed from the base of the PR and between c4bb80c and bbb68d5.

📒 Files selected for processing (28)
  • docs/content/docs/1.guides/3.consent.md
  • docs/content/scripts/bing-uet.md
  • docs/content/scripts/clarity.md
  • docs/content/scripts/google-analytics.md
  • docs/content/scripts/matomo-analytics.md
  • docs/content/scripts/meta-pixel.md
  • docs/content/scripts/mixpanel-analytics.md
  • docs/content/scripts/posthog.md
  • docs/content/scripts/tiktok-pixel.md
  • packages/script/src/module.ts
  • packages/script/src/registry-types.json
  • packages/script/src/registry.ts
  • packages/script/src/runtime/composables/useScript.ts
  • packages/script/src/runtime/composables/useScriptConsent.ts
  • packages/script/src/runtime/composables/useScriptTriggerConsent.ts
  • packages/script/src/runtime/registry/bing-uet.ts
  • packages/script/src/runtime/registry/clarity.ts
  • packages/script/src/runtime/registry/google-analytics.ts
  • packages/script/src/runtime/registry/matomo-analytics.ts
  • packages/script/src/runtime/registry/meta-pixel.ts
  • packages/script/src/runtime/registry/mixpanel-analytics.ts
  • packages/script/src/runtime/registry/posthog.ts
  • packages/script/src/runtime/registry/schemas.ts
  • packages/script/src/runtime/registry/tiktok-pixel.ts
  • packages/script/src/runtime/types.ts
  • test/nuxt-runtime/consent-default.nuxt.test.ts
  • test/nuxt-runtime/use-script-consent.nuxt.test.ts
  • test/unit/default-consent.test.ts

Comment on lines +91 to +103
```ts
const { proxy } = useScriptMixpanelAnalytics({
token: 'YOUR_TOKEN',
defaultConsent: 'opt-out',
})

function onAccept() {
proxy.mixpanel.opt_in_tracking()
}
function onRevoke() {
proxy.mixpanel.opt_out_tracking()
}
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify if opt_in_tracking/opt_out_tracking are defined in the Mixpanel API interface
ast-grep --pattern $'interface MixpanelAnalyticsApi {
  $$$
}'

# Check for any opt_in/opt_out method definitions
rg -n 'opt_in_tracking|opt_out_tracking' --type ts

Repository: nuxt/scripts

Length of output: 3305


Consider adding opt_in_tracking and opt_out_tracking to the stub interface for TypeScript completeness.

The documented methods proxy.mixpanel.opt_in_tracking() and proxy.mixpanel.opt_out_tracking() are correct for runtime behavior—the real Mixpanel SDK provides them. However, the MixpanelAnalyticsApi stub interface omits these methods, which means TypeScript users following this documentation won't get proper type hints. While the implementation defensively uses optional chaining and the code comment acknowledges these methods aren't part of the stub, adding them to the interface would improve developer experience without affecting runtime behavior.

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

In `@docs/content/scripts/mixpanel-analytics.md` around lines 91 - 103, Add the
missing opt_in_tracking and opt_out_tracking method signatures to the
MixpanelAnalyticsApi stub so TypeScript users get proper completion for
proxy.mixpanel.opt_in_tracking() and proxy.mixpanel.opt_out_tracking(); locate
the MixpanelAnalyticsApi interface (or its stub type) and add two optional
methods with appropriate signatures (e.g., opt_in_tracking?: () => void and
opt_out_tracking?: () => void) to match the runtime Mixpanel SDK while keeping
them optional so runtime behavior and defensive optional chaining remain
unchanged.

Comment on lines +60 to +67
function applyTikTokConsent(state: { ad_storage?: 'granted' | 'denied' }, proxy: TikTokPixelApi) {
if (!state.ad_storage)
return
if (state.ad_storage === 'granted')
proxy.ttq.grantConsent()
else
proxy.ttq.revokeConsent()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

holdConsent() is still unreachable before the first TikTok events.

applyTikTokConsent() and the defaultConsent branch only handle granted/denied. An undecided state is a no-op, so there is still no built-in way to put TikTok into hold mode before the existing init / page calls run.

Also applies to: 123-134

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

In `@packages/script/src/runtime/registry/tiktok-pixel.ts` around lines 60 - 67,
applyTikTokConsent currently treats missing/undecided ad_storage as a no-op so
TikTok never enters hold mode; update applyTikTokConsent to explicitly call
proxy.ttq.holdConsent() when ad_storage is undefined/undecided (in addition to
calling proxy.ttq.grantConsent() for 'granted' and proxy.ttq.revokeConsent() for
'denied'), and make the same change in the defaultConsent handling (the branch
in the file around the defaultConsent logic) so the SDK can place TikTok into
hold before any init/page events run.

@harlan-zw harlan-zw changed the title feat(consent): unified useScriptConsent composable and consent adapters feat: unified consent management Apr 14, 2026
…mock

- Delete test/unit/default-consent.test.ts: imported runtime registry files that resolve #nuxt-scripts/utils virtual module, only available in the nuxt-runtime test project.
- Add test/unit/consent-adapters.test.ts with pure adapter projection tests.
- Fix posthog tests in consent-default.nuxt.test.ts: replace vi.doMock + resetModules with a hoisted vi.mock + mutable posthogInitImpl, so the dynamic import inside clientInit reliably hits the mock.
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: 1

♻️ Duplicate comments (2)
packages/script/src/registry.ts (1)

451-465: ⚠️ Potential issue | 🟠 Major

Forward the new consent APIs through Partytown.

These adapters now call mixpanel.opt_in_tracking() / opt_out_tracking(), ttq.grantConsent() / revokeConsent() / holdConsent(), and clarity('consent', ...), but the corresponding partytown.forwards lists still omit them. In worker mode, those consent updates won't reach the vendor APIs.

♻️ Suggested forwards update
-      partytown: { forwards: ['mixpanel', 'mixpanel.init', 'mixpanel.track', 'mixpanel.identify', 'mixpanel.people.set', 'mixpanel.reset', 'mixpanel.register'] },
+      partytown: { forwards: ['mixpanel', 'mixpanel.init', 'mixpanel.track', 'mixpanel.identify', 'mixpanel.people.set', 'mixpanel.reset', 'mixpanel.register', 'mixpanel.opt_in_tracking', 'mixpanel.opt_out_tracking'] },

-      partytown: { forwards: ['ttq.track', 'ttq.page', 'ttq.identify'] },
+      partytown: { forwards: ['ttq.track', 'ttq.page', 'ttq.identify', 'ttq.grantConsent', 'ttq.revokeConsent', 'ttq.holdConsent'] },

-      partytown: { forwards: [] },
+      partytown: { forwards: ['clarity'] },
Does Partytown require every invoked global or nested method (for example `mixpanel.opt_in_tracking`, `ttq.grantConsent`, and `clarity`) to be explicitly listed in `forward` for calls from the main thread to reach the worker?

Also applies to: 522-523, 625-626

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

In `@packages/script/src/registry.ts` around lines 451 - 465, The Partytown
forwards lists are missing the new consent APIs so calls like
mixpanel.opt_in_tracking()/opt_out_tracking(),
ttq.grantConsent()/revokeConsent()/holdConsent(), and clarity('consent', ...)
invoked by the adapters won’t reach the worker; update the partytown.forwards
arrays referenced near the consentAdapter definitions to include the nested
method names (e.g., add "mixpanel.opt_in_tracking" and
"mixpanel.opt_out_tracking" to the Mixpanel forwards list, add
"ttq.grantConsent", "ttq.revokeConsent", "ttq.holdConsent" to the TT/ttq
forwards, and ensure "clarity" (and if needed "clarity.consent" or the call form
used) is forwarded) and mirror the same additions in the other two forwards
locations mentioned so worker-mode consent updates are delivered to vendor APIs.
packages/script/src/runtime/registry/tiktok-pixel.ts (1)

102-113: ⚠️ Potential issue | 🟠 Major

TikTok's hold state is still unreachable before the first events.

holdConsent() is stubbed here but never called, so this initializer still has no path that puts the pixel into a held state before ttq('init') / ttq('page'). packages/script/src/runtime/registry/consent-adapters.ts has the same no-op branch for missing ad_storage, so both the per-script default flow and the unified consent flow still miss issue #711's “hold until later grant/revoke” behavior.

For TikTok Pixel consent mode, can a site keep the pixel in a held state before the initial `init` / `page` calls, and is `holdConsent()` the API required before a later `grantConsent()` or `revokeConsent()`?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/script/src/runtime/registry/tiktok-pixel.ts` around lines 102 - 113,
The TikTok initializer currently stubs holdConsent but never invokes it, so add
a branch to call the stub when the default should be "held": after creating the
consentMethods stubs (grantConsent, revokeConsent, holdConsent) call
ttq.holdConsent() when options?.defaultConsent === 'hold' (mirroring the
existing granted/denied branches that call
ttq.grantConsent()/ttq.revokeConsent()); also update the unified consent adapter
in packages/script/src/runtime/registry/consent-adapters.ts to call the same
hold path when ad_storage is missing/should be held so the centralized flow and
the per-script ttq initializer both support the "hold until later grant/revoke"
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/unit/default-consent.test.ts`:
- Around line 3-6: The test is trying to mutate import.meta.client at runtime
which won't affect modules; remove the runtime assignment of (import.meta as
any).client and instead configure the test environment so modules see
import.meta.client as true (e.g., add a Vitest/Vite define config like
'import.meta.client': true) or mock the module that reads import.meta.client
(the module that runs useRegistryScript / beforeInit) so you can control its
behavior, or refactor the initialization into a mockable function/composable and
call that from the test to create window.ttq, window.fbq, window.uetq,
window.clarity as needed.

---

Duplicate comments:
In `@packages/script/src/registry.ts`:
- Around line 451-465: The Partytown forwards lists are missing the new consent
APIs so calls like mixpanel.opt_in_tracking()/opt_out_tracking(),
ttq.grantConsent()/revokeConsent()/holdConsent(), and clarity('consent', ...)
invoked by the adapters won’t reach the worker; update the partytown.forwards
arrays referenced near the consentAdapter definitions to include the nested
method names (e.g., add "mixpanel.opt_in_tracking" and
"mixpanel.opt_out_tracking" to the Mixpanel forwards list, add
"ttq.grantConsent", "ttq.revokeConsent", "ttq.holdConsent" to the TT/ttq
forwards, and ensure "clarity" (and if needed "clarity.consent" or the call form
used) is forwarded) and mirror the same additions in the other two forwards
locations mentioned so worker-mode consent updates are delivered to vendor APIs.

In `@packages/script/src/runtime/registry/tiktok-pixel.ts`:
- Around line 102-113: The TikTok initializer currently stubs holdConsent but
never invokes it, so add a branch to call the stub when the default should be
"held": after creating the consentMethods stubs (grantConsent, revokeConsent,
holdConsent) call ttq.holdConsent() when options?.defaultConsent === 'hold'
(mirroring the existing granted/denied branches that call
ttq.grantConsent()/ttq.revokeConsent()); also update the unified consent adapter
in packages/script/src/runtime/registry/consent-adapters.ts to call the same
hold path when ad_storage is missing/should be held so the centralized flow and
the per-script ttq initializer both support the "hold until later grant/revoke"
behavior.
🪄 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: 28993874-31be-4b1d-abd2-c1c79dd411af

📥 Commits

Reviewing files that changed from the base of the PR and between bbb68d5 and 8ae9ff1.

📒 Files selected for processing (8)
  • packages/script/src/registry.ts
  • packages/script/src/runtime/registry/bing-uet.ts
  • packages/script/src/runtime/registry/clarity.ts
  • packages/script/src/runtime/registry/consent-adapters.ts
  • packages/script/src/runtime/registry/google-analytics.ts
  • packages/script/src/runtime/registry/meta-pixel.ts
  • packages/script/src/runtime/registry/tiktok-pixel.ts
  • test/unit/default-consent.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/script/src/runtime/registry/google-analytics.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/script/src/runtime/registry/clarity.ts
  • packages/script/src/runtime/registry/meta-pixel.ts

- Guard adapter fan-out: try/catch in `applyUpdate` flush and `register()` so
  one failing adapter can't poison the controller. Broken adapters no longer
  stay registered.
- Make `postConsentTrigger` one-shot: watcher stops after first grant so
  revoke/accept cycles don't re-run non-idempotent trigger work.
- Exclude `consent` and `_consentAdapter` from `NuxtUseScriptOptionsSerializable`
  since they carry unserializable refs/functions/adapters.
- Add missing Partytown forwards for mixpanel opt-in/out, TikTok
  grant/revoke/hold, and Clarity so consent calls reach the worker.
- TikTok adapter: `applyDefault` with undecided state now calls
  `holdConsent()` so integrators can defer tracking via the composable.
  `applyUpdate` stays a no-op on undecided to preserve prior decisions.
- Fix misleading docs: mixpanel/posthog sequencing (opt-out on init config,
  opt-in after init), TikTok method names (`ttq.grantConsent` not
  `ttq.consent.grant`).
- Simplify mixpanel opt-in condition (no longer forces opt-out-by-default).
- Add `opt_in_tracking` / `opt_out_tracking` to `MixpanelAnalyticsApi` stub.
- Trim consent guide vendor-mapping table to scripts that actually ship an
  adapter; add note about binary-gate behaviour for others.
- TikTok defaultConsent accepts 'hold' so users can defer consent without the composable (addresses CodeRabbit #712).
- Mixpanel stub methods now include opt_in_tracking/opt_out_tracking so pre-load calls queue correctly.
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: 1

🧹 Nitpick comments (1)
packages/script/src/runtime/composables/useScriptConsent.ts (1)

185-220: One-shot trigger logic is correctly implemented, but line 213 has confusing redundant check.

The triggered flag and handle.stop?.() call correctly make the trigger one-shot. However, line 213 checks options?.postConsentTrigger in a ternary when we're already inside the 'onNuxtReady' branch, making the condition always truthy.

♻️ Simplify the redundant condition
        if (options?.postConsentTrigger === 'onNuxtReady') {
-          const idleTimeout = options?.postConsentTrigger ? (nuxtApp ? onNuxtReady : requestIdleCallback) : (cb: () => void) => cb()
+          const idleTimeout = nuxtApp ? onNuxtReady : requestIdleCallback
          runner(() => idleTimeout(resolve))
          return
        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/script/src/runtime/composables/useScriptConsent.ts` around lines 185
- 220, Inside the 'onNuxtReady' branch of the watcher in useScriptConsent (where
options?.postConsentTrigger === 'onNuxtReady'), remove the redundant ternary
that re-checks options?.postConsentTrigger and instead set idleTimeout based
only on whether nuxtApp exists; e.g. replace the current const idleTimeout =
options?.postConsentTrigger ? (nuxtApp ? onNuxtReady : requestIdleCallback) :
(cb: () => void) => cb() with a simpler const idleTimeout = nuxtApp ?
onNuxtReady : requestIdleCallback and keep the existing runner(() =>
idleTimeout(resolve)) call. This targets the postConsentTrigger/onNuxtReady
branch and keeps the one-shot logic (triggered/handle.stop) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/content/docs/1.guides/3.consent.md`:
- Around line 163-221: The OneTrust handler uses a substring match on
OnetrustActiveGroups which can false-positive (e.g., 'C00020' matching 'C0002');
in the apply function that reads (window as any).OnetrustActiveGroups and calls
consent.update, change the check from groups.includes('C0002') / 'C0004' to a
delimiter-aware match — e.g., normalize the groups string by adding surrounding
commas or split into an array and then check for exact IDs (use ',C0002,' /
',C0004,' includes or array.includes('C0002')) before setting analytics_storage
/ ad_storage / ad_user_data / ad_personalization in consent.update.

---

Nitpick comments:
In `@packages/script/src/runtime/composables/useScriptConsent.ts`:
- Around line 185-220: Inside the 'onNuxtReady' branch of the watcher in
useScriptConsent (where options?.postConsentTrigger === 'onNuxtReady'), remove
the redundant ternary that re-checks options?.postConsentTrigger and instead set
idleTimeout based only on whether nuxtApp exists; e.g. replace the current const
idleTimeout = options?.postConsentTrigger ? (nuxtApp ? onNuxtReady :
requestIdleCallback) : (cb: () => void) => cb() with a simpler const idleTimeout
= nuxtApp ? onNuxtReady : requestIdleCallback and keep the existing runner(() =>
idleTimeout(resolve)) call. This targets the postConsentTrigger/onNuxtReady
branch and keeps the one-shot logic (triggered/handle.stop) intact.
🪄 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: 9fae11bf-f5f4-45da-9e0a-e8e2115eddcb

📥 Commits

Reviewing files that changed from the base of the PR and between 429a2fc and 58303b8.

📒 Files selected for processing (9)
  • docs/content/docs/1.guides/3.consent.md
  • packages/script/src/registry.ts
  • packages/script/src/runtime/composables/useScriptConsent.ts
  • packages/script/src/runtime/registry/consent-adapters.ts
  • packages/script/src/runtime/registry/mixpanel-analytics.ts
  • packages/script/src/runtime/registry/schemas.ts
  • packages/script/src/runtime/registry/tiktok-pixel.ts
  • packages/script/src/runtime/types.ts
  • test/unit/consent-adapters.test.ts
✅ Files skipped from review due to trivial changes (1)
  • test/unit/consent-adapters.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/script/src/runtime/registry/tiktok-pixel.ts
  • packages/script/src/runtime/registry/schemas.ts
  • packages/script/src/runtime/registry/consent-adapters.ts

Comment on lines +163 to 221
### OneTrust

```ts
const consent = useScriptConsent({
default: {
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied',
},
})

onNuxtReady(() => {
function apply() {
const groups = (window as any).OnetrustActiveGroups as string | undefined
if (!groups)
return
consent.update({
analytics_storage: groups.includes('C0002') ? 'granted' : 'denied',
ad_storage: groups.includes('C0004') ? 'granted' : 'denied',
ad_user_data: groups.includes('C0004') ? 'granted' : 'denied',
ad_personalization: groups.includes('C0004') ? 'granted' : 'denied',
})
}

apply()
window.addEventListener('OneTrustGroupsUpdated', apply)
})
```

### Cookiebot

```ts
const consent = useScriptConsent({
default: {
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied',
},
})

onNuxtReady(() => {
function apply() {
const cb = (window as any).Cookiebot
if (!cb?.consent)
return
consent.update({
analytics_storage: cb.consent.statistics ? 'granted' : 'denied',
ad_storage: cb.consent.marketing ? 'granted' : 'denied',
ad_user_data: cb.consent.marketing ? 'granted' : 'denied',
ad_personalization: cb.consent.marketing ? 'granted' : 'denied',
})
}

apply()
window.addEventListener('CookiebotOnAccept', apply)
window.addEventListener('CookiebotOnDecline', apply)
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

OneTrust OnetrustActiveGroups OneTrustGroupsUpdated JavaScript API 2024

💡 Result:

OnetrustActiveGroups and OneTrustGroupsUpdated are key elements of the OneTrust Web CMP JavaScript API for managing user consent groups. OnetrustActiveGroups is a global data layer variable (window.OnetrustActiveGroups) that contains a comma-separated string of IDs for active consent groups (those with user consent or always active). It is populated after script initialization and updated on consent changes. Example usage for Google Consent Mode: window.OnetrustActiveGroups.includes(',C0002,') ? 'granted' : 'denied' OneTrustGroupsUpdated is a window event triggered on script load/refresh and when user consent preferences update. It provides an array of active group IDs via event.detail. window.addEventListener("OneTrustGroupsUpdated", event => { console.log(Active groups: ${event.detail.join(", ")}); }); These are used together for real-time consent-aware logic, such as enabling/disabling trackers. Official docs confirm this as of 2024 references in related pages. No specific "OneTrust OnetrustActiveGroups OneTrustGroupsUpdated JavaScript API 2024" endpoint; these are client-side CMP features.

Citations:


🌐 Web query:

Cookiebot consent object JavaScript API 2024 CookiebotOnAccept CookiebotOnDecline

💡 Result:

The Cookiebot CMP (Consent Management Platform) provides a JavaScript API via the global 'Cookiebot' object, loaded by including the script https://consent.cookiebot.com/uc.js?cbid=YOUR_CBID (replace YOUR_CBID with your domain group ID from your Cookiebot account). Key elements: Consent Object (Cookiebot.consent): Read-only properties indicating user consent for cookie categories: - consent.necessary (bool, default true): Necessary cookies accepted. - consent.preferences (bool, default false): Preferences cookies accepted. - consent.statistics (bool, default false): Statistics cookies accepted. - consent.marketing (bool, default false): Marketing cookies accepted. - consented (bool, default false): Any cookies accepted. - declined (bool, default false): Cookies declined. - hasResponse (bool): User has responded. - method (string): 'implied' or 'explicit' consent method. Methods: - Cookiebot.show: Show consent dialog. - Cookiebot.renew: Renew/change consent (resets and shows dialog). - Cookiebot.runScripts: Execute blocked scripts based on consent (for dynamic content). - Cookiebot.getScript(url, [async], [callback]): Load external script respecting consent. Events (use window.addEventListener): - CookiebotOnAccept: Fires when user accepts cookies (also on page load if previously accepted). - CookiebotOnDecline: Fires when user declines cookies (also on page load if previously declined). - CookiebotOnLoad: When consent loaded (not script load). - CookiebotOnConsentReady: Consent state ready. - CookiebotOnDialogInit: Dialog initialized. Callbacks (define global functions before script loads): - CookiebotCallback_OnAccept: Called on accept. - CookiebotCallback_OnDecline: Called on decline. - CookiebotCallback_OnLoad: Called when consent loaded. Example usage for OnAccept (load GA if statistics consented): window.addEventListener('CookiebotOnAccept', function { if (Cookiebot.consent.statistics) { // Load Google Analytics } }); function CookiebotCallback_OnDecline { // Handle decline } This API remains consistent as of 2025-2026 sources from official Cookiebot developer pages and support docs. Always check consent before loading trackers: if (Cookiebot.consent.statistics) { ... }. For server-side, parse CookieConsent cookie.

Citations:


Fix OneTrust group ID matching pattern.

The Cookiebot implementation correctly uses the Cookiebot.consent object structure (statistics, marketing properties) and listens to the proper events (CookiebotOnAccept/CookiebotOnDecline).

However, the OneTrust code uses an unsafe string matching pattern. Since OnetrustActiveGroups is a comma-separated string of group IDs, groups.includes('C0002') can produce false positives if another group ID contains 'C0002' as a substring (e.g., 'C00020'). Use comma delimiters instead:

Recommended OneTrust pattern
groups.includes(',C0002,') ? 'granted' : 'denied'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/docs/1.guides/3.consent.md` around lines 163 - 221, The OneTrust
handler uses a substring match on OnetrustActiveGroups which can false-positive
(e.g., 'C00020' matching 'C0002'); in the apply function that reads (window as
any).OnetrustActiveGroups and calls consent.update, change the check from
groups.includes('C0002') / 'C0004' to a delimiter-aware match — e.g., normalize
the groups string by adding surrounding commas or split into an array and then
check for exact IDs (use ',C0002,' / ',C0004,' includes or
array.includes('C0002')) before setting analytics_storage / ad_storage /
ad_user_data / ad_personalization in consent.update.

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: 1

🤖 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/runtime/registry/tiktok-pixel.ts`:
- Around line 51-56: Update the JSDoc comments for the consent methods on the
TikTokPixel registry (grantConsent, revokeConsent, holdConsent) to reflect that
they are usable before the SDK loads rather than “Available after the script
loads,” and change the loader reference from "bat.js" to "events.js" to match
the actual loader used; locate and edit the inline notes near the TikTokPixel
interface where those methods are declared and the adjacent loader comment so
the wording accurately states pre-load availability and references events.js.
🪄 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: a9f13662-a271-4daf-81a4-cbf6aad0e1f4

📥 Commits

Reviewing files that changed from the base of the PR and between 58303b8 and 3b68b6c.

📒 Files selected for processing (3)
  • packages/script/src/runtime/registry/mixpanel-analytics.ts
  • packages/script/src/runtime/registry/schemas.ts
  • packages/script/src/runtime/registry/tiktok-pixel.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/script/src/runtime/registry/mixpanel-analytics.ts

Comment on lines +51 to +56
/** Opt user in to tracking. Available after the script loads. */
grantConsent: () => void
/** Opt user out of tracking. Available after the script loads. */
revokeConsent: () => void
/** Defer consent until an explicit grant/revoke. Available after the script loads. */
holdConsent: () => void
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Update the consent-method comments to match the new bootstrap behavior.

Lines 104-109 make these methods usable before the SDK loads, so “Available after the script loads” is now misleading. The inline note also references bat.js, but this loader fetches TikTok events.js on Line 73.

Also applies to: 102-103

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

In `@packages/script/src/runtime/registry/tiktok-pixel.ts` around lines 51 - 56,
Update the JSDoc comments for the consent methods on the TikTokPixel registry
(grantConsent, revokeConsent, holdConsent) to reflect that they are usable
before the SDK loads rather than “Available after the script loads,” and change
the loader reference from "bat.js" to "events.js" to match the actual loader
used; locate and edit the inline notes near the TikTokPixel interface where
those methods are declared and the adjacent loader comment so the wording
accurately states pre-load availability and references events.js.

@harlan-zw
Copy link
Copy Markdown
Collaborator Author

Code review

Found 2 issues:

  1. Adapter registration passes the raw ScriptInstance as the proxy, not instance.proxy. Every consentAdapter accesses vendor APIs off the proxy (e.g. proxy.ttq.grantConsent(), proxy.fbq(...), proxy.uetq.push(...)), so applyDefault / applyUpdate will throw a TypeError at runtime. The granular fan-out for every consent-aware script is broken.

// subscribe so the adapter receives `applyDefault` with current state plus `applyUpdate`
// on each granular update. Scope A wires the adapter on individual registry entries.
if (import.meta.client && options.consent && options._consentAdapter && typeof options.consent.register === 'function') {
if (import.meta.dev && (options as any).defaultConsent) {
console.warn('[nuxt-scripts] Both `consent` (composable) and `defaultConsent` (per-script) are set. The composable takes precedence.')
}
const unregister = options.consent.register(options._consentAdapter, instance)
const _removeWithUnregister = instance.remove
instance.remove = () => {
unregister()
return _removeWithUnregister()
}
}

  1. PostHog declares a consentAdapter in the registry, but because PostHog uses scriptMode: 'npm', useRegistryScript early-returns into createNpmScriptStub before the consent wiring in useScript.ts ever runs. The adapter is never subscribed, so granular GCMv2 fan-out silently does nothing for PostHog even though the consent guide's vendor table promises it.

const options = optionsFn(userOptions as InferIfSchema<O>, { scriptInput: userOptions.scriptInput as UseScriptInput & { src?: string } })
// NEW: Handle NPM-only scripts differently
if (options.scriptMode === 'npm') {
return createNpmScriptStub<T>({
key: String(registryKey),
use: options.scriptOptions?.use,
clientInit: options.clientInit,
trigger: userOptions.scriptOptions?.trigger as any,
}) as any as UseScriptContext<UseFunctionType<NuxtUseScriptOptions<T>, T>>
}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

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.

Tiktok Pixel consent mode

1 participant