Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…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.
commit: |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a new composable Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (2)
packages/script/src/runtime/registry/posthog.ts (1)
109-111: Redundantopt_in_capturing()call whendefaultConsent: 'opt-in'.PostHog starts with capturing enabled by default. Calling
opt_in_capturing()after init whendefaultConsent === '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
defaultConsentvalues 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
📒 Files selected for processing (28)
docs/content/docs/1.guides/3.consent.mddocs/content/scripts/bing-uet.mddocs/content/scripts/clarity.mddocs/content/scripts/google-analytics.mddocs/content/scripts/matomo-analytics.mddocs/content/scripts/meta-pixel.mddocs/content/scripts/mixpanel-analytics.mddocs/content/scripts/posthog.mddocs/content/scripts/tiktok-pixel.mdpackages/script/src/module.tspackages/script/src/registry-types.jsonpackages/script/src/registry.tspackages/script/src/runtime/composables/useScript.tspackages/script/src/runtime/composables/useScriptConsent.tspackages/script/src/runtime/composables/useScriptTriggerConsent.tspackages/script/src/runtime/registry/bing-uet.tspackages/script/src/runtime/registry/clarity.tspackages/script/src/runtime/registry/google-analytics.tspackages/script/src/runtime/registry/matomo-analytics.tspackages/script/src/runtime/registry/meta-pixel.tspackages/script/src/runtime/registry/mixpanel-analytics.tspackages/script/src/runtime/registry/posthog.tspackages/script/src/runtime/registry/schemas.tspackages/script/src/runtime/registry/tiktok-pixel.tspackages/script/src/runtime/types.tstest/nuxt-runtime/consent-default.nuxt.test.tstest/nuxt-runtime/use-script-consent.nuxt.test.tstest/unit/default-consent.test.ts
| ```ts | ||
| const { proxy } = useScriptMixpanelAnalytics({ | ||
| token: 'YOUR_TOKEN', | ||
| defaultConsent: 'opt-out', | ||
| }) | ||
|
|
||
| function onAccept() { | ||
| proxy.mixpanel.opt_in_tracking() | ||
| } | ||
| function onRevoke() { | ||
| proxy.mixpanel.opt_out_tracking() | ||
| } | ||
| ``` |
There was a problem hiding this comment.
🧩 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 tsRepository: 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.
| 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() | ||
| } |
There was a problem hiding this comment.
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.
…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.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
packages/script/src/registry.ts (1)
451-465:⚠️ Potential issue | 🟠 MajorForward the new consent APIs through Partytown.
These adapters now call
mixpanel.opt_in_tracking()/opt_out_tracking(),ttq.grantConsent()/revokeConsent()/holdConsent(), andclarity('consent', ...), but the correspondingpartytown.forwardslists 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 | 🟠 MajorTikTok'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 beforettq('init')/ttq('page').packages/script/src/runtime/registry/consent-adapters.tshas the same no-op branch for missingad_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
📒 Files selected for processing (8)
packages/script/src/registry.tspackages/script/src/runtime/registry/bing-uet.tspackages/script/src/runtime/registry/clarity.tspackages/script/src/runtime/registry/consent-adapters.tspackages/script/src/runtime/registry/google-analytics.tspackages/script/src/runtime/registry/meta-pixel.tspackages/script/src/runtime/registry/tiktok-pixel.tstest/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.
There was a problem hiding this comment.
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
triggeredflag andhandle.stop?.()call correctly make the trigger one-shot. However, line 213 checksoptions?.postConsentTriggerin 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
📒 Files selected for processing (9)
docs/content/docs/1.guides/3.consent.mdpackages/script/src/registry.tspackages/script/src/runtime/composables/useScriptConsent.tspackages/script/src/runtime/registry/consent-adapters.tspackages/script/src/runtime/registry/mixpanel-analytics.tspackages/script/src/runtime/registry/schemas.tspackages/script/src/runtime/registry/tiktok-pixel.tspackages/script/src/runtime/types.tstest/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
| ### 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) | ||
| }) |
There was a problem hiding this comment.
🧩 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:
- 1: https://developer.onetrust.com/onetrust/docs/javascript-api
- 2: https://developer.onetrust.com/onetrust/docs/javascript-events-guide
🌐 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:
- 1: https://usercentrics.com/docs/web/cookiebot/cookiebot-sdk/
- 2: https://www.cookiebot.com/en/developer/
- 3: https://support.cookiebot.com/hc/en-us/articles/360020661139-How-to-find-out-when-the-Cookiebot-script-has-loaded
- 4: https://cookiebot.com/en/developer
- 5: https://cookiebot.com/es/developer
- 6: https://support.cookiebot.com/hc/en-us/articles/6954474867740-Conditionally-loading-code-in-bundles
- 7: https://support.cookiebot.com/hc/en-us/articles/4405978132242-Manual-cookie-blocking
- 8: https://www.waiversign.com
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
packages/script/src/runtime/registry/mixpanel-analytics.tspackages/script/src/runtime/registry/schemas.tspackages/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
| /** 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 |
There was a problem hiding this comment.
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.
Code reviewFound 2 issues:
scripts/packages/script/src/runtime/composables/useScript.ts Lines 286 to 298 in 3b68b6c
scripts/packages/script/src/runtime/utils.ts Lines 68 to 78 in 3b68b6c 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Linked issue
Resolves #711
References #243, #710
Type of 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 hadonBeforeUetStart, TikTok had nothing, Matomo/Mixpanel/PostHog each spoke their own dialect). One cookie banner now fans out to all of them.What's new
useScriptConsentcomposableSuperset of
useScriptTriggerConsent. Keeps the binary load-gate working verbatim, layers in GCMv2 granular state plus adapter fan-out.Pass it into any registry script and the script subscribes automatically:
Per-script
defaultConsentoptionEvery consent-aware script now accepts a typed
defaultConsentapplied insideclientInitbefore the vendor init / first tracking call. Useful without the composable for static policies.defaultConsentshapePartial<ConsentState>(GCMv2)Partial<ConsentState>(GCMv2){ ad_storage }ad_storage'granted' | 'denied'ad_storage'granted' | 'denied'ad_storage'required' | 'given' | 'not-required'analytics_storage'opt-in' | 'opt-out'analytics_storage'opt-in' | 'opt-out'analytics_storageboolean | Record<string, string>analytics_storageuseScriptTriggerConsentdeprecationReplaced by a
@deprecatedshim that delegates touseScriptConsentand emits a dev-only warning. All existing call sites work verbatim, rename is the only required change.Migration
Full guide:
docs/content/docs/1.guides/3.consent.md(includes OneTrust/Cookiebot recipes + vendor mapping table).Not shipped
snaptr('grantConsent')exists anecdotally but was not verified against vendor docs. Easy follow-up.Design notes
consent(composable) anddefaultConsent(per-script) are set, the composable wins; a dev-only warning fires.onBeforeGtagStart,onBeforeUetStart) retained.consent-adapters.tsusing type-only imports so registry.ts (build-time) stays decoupled from runtime utilities.Test plan
useScriptConsentunit tests: default state,updatemerging, batching, binary compat withuseScriptTriggerConsentdefaultConsenttests: vendor call appears inclientInitbefore init/trackapplyDefault/applyUpdatemap GCM state to vendor calls correctlyuseScriptTriggerConsenttests pass unchanged through the deprecation shim