Conversation
Adds the Usercentrics consent management platform to the registry as useScriptUsercentrics with typed UC_UI access plus a consent helper exposing onConsentChange(), letting users wire other registry scripts' consent triggers directly to UC_CONSENT events. Bundle and proxy are intentionally off and the script is exempt from consent gating since it is the consent surface itself.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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:
📝 WalkthroughWalkthroughThis PR adds comprehensive Usercentrics CMP integration: a new useScriptUsercentrics() composable that registers the Usercentrics loader, typed UC_UI interfaces and consent helper (whenReady, onConsentChange, show/accept/deny), valibot schema and registry types, registry metadata and logo, documentation page, playground example pages, E2E tests with fixtures, and a types assertion for the module registry. Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 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 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: 1
🧹 Nitpick comments (2)
playground/pages/third-parties/usercentrics/nuxt-scripts.vue (1)
20-24: ⚡ Quick winGuard nested consent access in service mapping.
Line 23 dereferences
s.consent.statusdirectly. If a service payload is partial, this can throw and break UI updates; use optional chaining.Proposed defensive tweak
- granted: !!s.consent.status, + granted: !!s.consent?.status,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@playground/pages/third-parties/usercentrics/nuxt-scripts.vue` around lines 20 - 24, The mapping that sets services.value uses s.consent.status directly which can throw if the service payload is partial; update the mapping (where services.value is assigned from window.UC_UI?.getServicesBaseInfo?.()) to guard nested access by using optional chaining for consent (e.g., replace the use of s.consent.status with s?.consent?.status) so missing consent objects won't break the UI when getServicesBaseInfo returns partial entries.packages/script/src/runtime/registry/usercentrics.ts (1)
103-111: ⚡ Quick win
whenReady()hangs indefinitely ifUC_UI_INITIALIZEDnever fires.If the Usercentrics script is blocked (adblocker, network failure, CSP) the
UC_UI_INITIALIZEDevent never arrives. The returnedPromisenever settles, thewindowlistener is never cleaned up, and anyawait consent.whenReady()call in user code stalls permanently. Consider adding anAbortSignal/timeout path or a rejection fallback.♻️ Proposed fix: add optional timeout / abort support
- const whenReady = (): Promise<UsercentricsUI> => new Promise((resolve) => { + const whenReady = (signal?: AbortSignal): Promise<UsercentricsUI> => new Promise((resolve, reject) => { if (window.UC_UI?.isInitialized?.()) return resolve(window.UC_UI) const onInit = () => { window.removeEventListener('UC_UI_INITIALIZED', onInit) + signal?.removeEventListener('abort', onAbort) resolve(window.UC_UI as UsercentricsUI) } + const onAbort = () => { + window.removeEventListener('UC_UI_INITIALIZED', onInit) + reject(signal!.reason ?? new DOMException('whenReady aborted', 'AbortError')) + } window.addEventListener('UC_UI_INITIALIZED', onInit) + signal?.addEventListener('abort', onAbort) })Callers can then pass
AbortSignal.timeout(10_000)for a bounded wait.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/script/src/runtime/registry/usercentrics.ts` around lines 103 - 111, whenReady currently returns a Promise that never settles if the UC_UI_INITIALIZED event never fires; update whenReady to accept an optional AbortSignal or timeout parameter and use it to reject the Promise and remove the UC_UI_INITIALIZED listener when aborted or timed out. Specifically, modify the whenReady function to: check window.UC_UI as before, attach the onInit listener via window.addEventListener('UC_UI_INITIALIZED', onInit), and also attach an abort/timeout handler (or use AbortSignal.timeout) that calls window.removeEventListener('UC_UI_INITIALIZED', onInit) and rejects the Promise with a clear error; ensure both paths clean up listeners and return/throw consistent errors for callers awaiting whenReady or passing an AbortSignal.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/script/src/runtime/registry/usercentrics.ts`:
- Line 87: The current code sets 'data-settings-id' to options.settingsId || ''
which produces a silent empty attribute in production; update the logic that
builds the attribute (the object entry with key 'data-settings-id' referencing
options.settingsId) to perform an explicit runtime guard: if options.settingsId
is missing or empty, throw a clear error (or at minimum omit the
'data-settings-id' attribute) instead of falling back to ''. Implement this
check where the attributes for the Usercentrics script are composed (look for
the object literal containing 'data-settings-id') and use options.settingsId
directly after validating it, e.g. validate typeof and length then either throw
new Error("missing settingsId") or skip adding the 'data-settings-id' key.
---
Nitpick comments:
In `@packages/script/src/runtime/registry/usercentrics.ts`:
- Around line 103-111: whenReady currently returns a Promise that never settles
if the UC_UI_INITIALIZED event never fires; update whenReady to accept an
optional AbortSignal or timeout parameter and use it to reject the Promise and
remove the UC_UI_INITIALIZED listener when aborted or timed out. Specifically,
modify the whenReady function to: check window.UC_UI as before, attach the
onInit listener via window.addEventListener('UC_UI_INITIALIZED', onInit), and
also attach an abort/timeout handler (or use AbortSignal.timeout) that calls
window.removeEventListener('UC_UI_INITIALIZED', onInit) and rejects the Promise
with a clear error; ensure both paths clean up listeners and return/throw
consistent errors for callers awaiting whenReady or passing an AbortSignal.
In `@playground/pages/third-parties/usercentrics/nuxt-scripts.vue`:
- Around line 20-24: The mapping that sets services.value uses s.consent.status
directly which can throw if the service payload is partial; update the mapping
(where services.value is assigned from window.UC_UI?.getServicesBaseInfo?.()) to
guard nested access by using optional chaining for consent (e.g., replace the
use of s.consent.status with s?.consent?.status) so missing consent objects
won't break the UI when getServicesBaseInfo returns partial entries.
🪄 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: cb609490-22c7-4522-a215-d11d31868b5f
📒 Files selected for processing (19)
docs/content/scripts/usercentrics.mdpackages/script/src/registry-logos.tspackages/script/src/registry-types.jsonpackages/script/src/registry.tspackages/script/src/runtime/registry/schemas.tspackages/script/src/runtime/registry/usercentrics.tspackages/script/src/runtime/types.tspackages/script/src/script-meta.tsplayground/pages/index.vueplayground/pages/third-parties/usercentrics/default.vueplayground/pages/third-parties/usercentrics/nuxt-scripts.vuetest/e2e/_usercentrics-suite.tstest/e2e/usercentrics.test.tstest/fixtures/usercentrics/app.vuetest/fixtures/usercentrics/nuxt.config.tstest/fixtures/usercentrics/package.jsontest/fixtures/usercentrics/pages/index.vuetest/fixtures/usercentrics/tsconfig.jsontest/types/types.test-d.ts
Remove the `|| ''` fallback so the type contract is the single source of truth, and add optional chaining when reading `s.consent?.status` from `getServicesBaseInfo()` results.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
playground/pages/third-parties/usercentrics/nuxt-scripts.vue (1)
17-27: ⚡ Quick winHydrate
servicesonce on ready, not only on consent events.At Line 18–25, the list updates only after
UC_CONSENT. If CMP is already initialized, UI remains empty until user action. Consider a one-time refresh viaconsent.whenReady()plus reuse in the event handler.Suggested refactor
if (import.meta.client) { - const off = consent.onConsentChange(() => { + const refreshServices = () => { lastEventAt.value = new Date().toISOString() services.value = (window.UC_UI?.getServicesBaseInfo?.() || []).map(s => ({ id: s.id, name: s.name, granted: !!s.consent?.status, })) - }) + } + + consent.whenReady().then(refreshServices) + const off = consent.onConsentChange(refreshServices) onScopeDispose(off) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@playground/pages/third-parties/usercentrics/nuxt-scripts.vue` around lines 17 - 27, The services list is only populated in the consent.onConsentChange callback so the UI stays empty if the CMP is already ready; extract the update logic into a small function (e.g., updateServices) that sets lastEventAt.value and services.value using window.UC_UI?.getServicesBaseInfo?.(), then inside the import.meta.client block call consent.whenReady().then(updateServices) for a one-time hydration and also register const off = consent.onConsentChange(() => updateServices()) and onScopeDispose(off) to keep the existing event teardown behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/script/src/runtime/registry/usercentrics.ts`:
- Line 12: The UsercentricsService type currently declares consent as required
but code uses s.consent?.status, so change the type for
UsercentricsService.consent to be optional (make consent optional on the
interface/type, e.g., consent?: { status: boolean }) so callers who use
s.consent?.status match the type; update any related type aliases or usages that
reference UsercentricsService.consent to accept the optional shape to prevent
unsafe access.
---
Nitpick comments:
In `@playground/pages/third-parties/usercentrics/nuxt-scripts.vue`:
- Around line 17-27: The services list is only populated in the
consent.onConsentChange callback so the UI stays empty if the CMP is already
ready; extract the update logic into a small function (e.g., updateServices)
that sets lastEventAt.value and services.value using
window.UC_UI?.getServicesBaseInfo?.(), then inside the import.meta.client block
call consent.whenReady().then(updateServices) for a one-time hydration and also
register const off = consent.onConsentChange(() => updateServices()) and
onScopeDispose(off) to keep the existing event teardown 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: 9081cae1-52e9-4caa-be66-5165cc7cbda1
📒 Files selected for processing (2)
packages/script/src/runtime/registry/usercentrics.tsplayground/pages/third-parties/usercentrics/nuxt-scripts.vue
Replace env-var skip gating with a Playwright network stub. The real loader validates settings IDs against the registered origin and refuses to bootstrap on localhost:<random>, so CI previously skipped the behavioural test. Stubbing app.usercentrics.eu and injecting a minimal UC_UI shim via addInitScript lets the UC_CONSENT round-trip assertion run unconditionally.
…eset-id, __ucCmp)
The original PR targeted the legacy CMP v2 surface (app.usercentrics.eu/browser-ui/<ver>/loader.js
+ data-settings-id + UC_UI). New Usercentrics signups only get v3, so the
integration was unreachable for any modern customer. Verified live against a
real v3 ruleset:
- Loader URL is web.cmp.usercentrics.eu/ui/loader.js (no version segment).
- Required attribute is data-ruleset-id (not data-settings-id).
- Programmatic API is window.__ucCmp (UC_UI is not bound for v3 rulesets we
observed); methods isInitialized / isConsentRequired / showFirstLayer /
showSecondLayer / acceptAllConsents / denyAllConsents / getConsentDetails /
refreshScripts live on its prototype.
- Init event is UC_CMP_API_READY; consent change event is UC_UI_CMP_EVENT
(with detail.type ∈ ACCEPT_ALL | DENY_ALL | SAVE | …).
- v2-only fields removed: settingsId, version, tcfEnabled, embeddingType
(TCF / embedding mode now configured server-side in the ruleset).
Schema is now { rulesetId, autoblocker?, language? }. autoblocker injects
web.cmp.usercentrics.eu/modules/autoblocker.js ahead of the loader for
rulesets configured with Auto Blocking. The composable surface exposes
{ ucCmp } in the script result and a typed `consent` helper for
onConsentChange / whenReady / showFirstLayer / showSecondLayer / acceptAll /
denyAll backed by __ucCmp.
Suite stubs the loader and dispatches UC_CMP_API_READY + UC_UI_CMP_EVENT to
keep behavioural tests deterministic on CI; an additional contract test
fetches the live loader and asserts it still references __ucCmp +
UC_CMP_API_READY so a vendor change breaks tests instead of silently
breaking the integration.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/script/src/registry-types.json (1)
980-984: 💤 Low valueConsider narrowing
UsercentricsCmpEventDetail.typeto a documented union.The field is currently
string, but the JSDoc onUsercentricsConsent.onConsentChangealready documents the concrete values callers branch on:'ACCEPT_ALL' | 'DENY_ALL' | 'SAVE'. Using plainstringleaves consumers without autocomplete or exhaustiveness checking on the most important field of the event detail.♻️ Proposed refinement
-"code": "export interface UsercentricsCmpEventDetail {\n type: string\n source?: string\n [key: string]: any\n}" +"code": "export interface UsercentricsCmpEventDetail {\n type: 'ACCEPT_ALL' | 'DENY_ALL' | 'SAVE' | (string & {})\n source?: string\n [key: string]: any\n}"The
(string & {})tail preserves forward compatibility if Usercentrics v3 emits additional event types.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/script/src/registry-types.json` around lines 980 - 984, Update the UsercentricsCmpEventDetail interface's type property from plain string to a narrowed union of the documented event literals to enable autocomplete and exhaustiveness checking; change the signature on the UsercentricsCmpEventDetail interface (the type field) to "'ACCEPT_ALL' | 'DENY_ALL' | 'SAVE' | (string & {})" so existing code handles known values but remains forward-compatible with future event types.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/content/scripts/usercentrics.md`:
- Line 11: Summary: Fix hyphenation of the compound adjective "end-user" in the
docs copy. Replace the phrase "end user consent" with "end-user consent" in the
sentence that currently reads "used to collect, store, and signal end user
consent..." so the text becomes "used to collect, store, and signal end-user
consent..."; ensure only the hyphen is added and no other wording is altered.
---
Nitpick comments:
In `@packages/script/src/registry-types.json`:
- Around line 980-984: Update the UsercentricsCmpEventDetail interface's type
property from plain string to a narrowed union of the documented event literals
to enable autocomplete and exhaustiveness checking; change the signature on the
UsercentricsCmpEventDetail interface (the type field) to "'ACCEPT_ALL' |
'DENY_ALL' | 'SAVE' | (string & {})" so existing code handles known values but
remains forward-compatible with future event types.
🪄 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: 0ffef7b9-ce7b-4714-8064-21e62b4aed5a
📒 Files selected for processing (11)
docs/content/scripts/usercentrics.mdpackages/script/src/registry-types.jsonpackages/script/src/registry.tspackages/script/src/runtime/registry/schemas.tspackages/script/src/runtime/registry/usercentrics.tspackages/script/src/script-meta.tsplayground/pages/third-parties/usercentrics/default.vueplayground/pages/third-parties/usercentrics/nuxt-scripts.vuetest/e2e/_usercentrics-suite.tstest/e2e/usercentrics.test.tstest/fixtures/usercentrics/nuxt.config.ts
🚧 Files skipped from review as they are similar to previous changes (8)
- packages/script/src/script-meta.ts
- packages/script/src/runtime/registry/schemas.ts
- test/e2e/usercentrics.test.ts
- playground/pages/third-parties/usercentrics/default.vue
- packages/script/src/registry.ts
- test/e2e/_usercentrics-suite.ts
- test/fixtures/usercentrics/nuxt.config.ts
- packages/script/src/runtime/registry/usercentrics.ts
# Conflicts: # package.json # playground/pages/index.vue
🔗 Linked issue
Related to #177
❓ Type of change
📚 Description
Adds Usercentrics CMP v3 ("Web CMP") to the registry as
useScriptUsercentrics. Loads the unbundled, unproxied v3 loader fromweb.cmp.usercentrics.eu/ui/loader.js(the consent surface itself must hit origin) and is exempt from consent gating. The composable surfaces the integration win:consent.onConsentChange(cb)overUC_UI_CMP_EVENTandwhenReady()overUC_CMP_API_READYso users can driveuseScriptconsent triggers from Usercentrics events, plus typedshowFirstLayer/showSecondLayer/acceptAll/denyAllhelpers backed by the v3window.__ucCmpprogrammatic API. Schema coversrulesetId(the v3 attribute), optionalautoblocker(injectsautoblocker.jsahead of the loader for Auto Blocking rulesets), and optionallanguage.🧪 Usage
Add
usercentrics: { rulesetId: 'your-ruleset-id' }underscripts.registryinnuxt.config.tsto register the script.🛠 v3 pivot
The original revision of this PR targeted the legacy CMP v2 surface (
app.usercentrics.eu/browser-ui/<ver>/loader.js+data-settings-id+UC_UI). After live-verifying against a real v3 ruleset, the registration was unreachable for any modern Usercentrics customer (new signups only get v3). Pivoted to v3 only:app.usercentrics.eu/browser-ui/${version}/loader.jsweb.cmp.usercentrics.eu/ui/loader.jsdata-settings-iddata-ruleset-idsettingsId+version+tcfEnabled+embeddingTyperulesetId+autoblocker+language(TCF / embedding type are now configured server-side in the ruleset)window.UC_UI(not bound on v3 rulesets we tested)window.__ucCmp(typedUsercentricsCmp)UC_CONSENTlistenerUC_UI_CMP_EVENT(carriesdetail.type)UC_UI_INITIALIZEDforwhenReadyUC_CMP_API_READYLive verification: confirmed
__ucCmpglobal +UC_CMP_API_READYevent + the proto methods (acceptAllConsents,denyAllConsents,showFirstLayer,showSecondLayer,getConsentDetails,isInitialized, …) on a registered v3 ruleset.✅ What's covered
data-ruleset-id;autoblocker: trueinjectsweb.cmp.usercentrics.eu/modules/autoblocker.jsahead of the loader.__ucCmpshim dispatchesUC_CMP_API_READY+UC_UI_CMP_EVENT(ACCEPT_ALL/DENY_ALL); the composable surfaces the events throughconsent.onConsentChange.web.cmp.usercentrics.eu/ui/loader.jsand asserts the body still references__ucCmp+UC_CMP_API_READY, so a vendor-side rename breaks the test instead of silently breaking the integration.rulesetIdso misconfig fails dev validation.UsercentricsApi/UsercentricsCmp/UsercentricsConsent/UsercentricsCmpEventDetailare exported.