feat: iubenda CMP with TCF consent for ads tracking - #6382
Conversation
Load the iubenda Cookie Solution (TCF mode) for GDPR-covered webapp users behind the iubenda_cmp flag, mirror its consent into the existing ilikecookies* cookies, populate the ad tracker consent macros from __tcfapi, send gdpr/gdpr_consent/addtl_consent on ad requests, attach the iubenda consent id to analytics events, and forward the stored TC string to boot via the x-gdpr-consent header.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| lang: 'en', | ||
| enableTcf: true, | ||
| googleAdditionalConsentMode: true, | ||
| perPurposeConsent: true, | ||
| askConsentAtCookiePolicyUpdate: true, | ||
| invalidateConsentWithoutLog: true, | ||
| // consent cookie shared across *.daily.dev, matching the existing | ||
| // `_iub_cs-*` bridge from the marketing homepage | ||
| localConsentDomain: process.env.NEXT_PUBLIC_DOMAIN, | ||
| floatingPreferencesButtonDisplay: false, | ||
| banner: { | ||
| position: 'float-bottom-center', | ||
| acceptButtonDisplay: true, | ||
| rejectButtonDisplay: true, | ||
| customizeButtonDisplay: true, | ||
| closeButtonDisplay: false, | ||
| listPurposes: true, |
There was a problem hiding this comment.
we need to make sure it matches with our homepage and marketing site configuration as well, since there we use iubenda already
There was a problem hiding this comment.
Good call — compared key-by-key against the homepage's live embed and aligned in 0812c3b: added countryDetection, enableLgpd, enableUspr, cookiePolicyInOtherWindow to match. Deliberate differences kept: enableTcf + Google ACM (the point of this PR) and no floating preferences button (settings/privacy is the re-entry point — though note logged-out users currently have no consent re-entry on the webapp, open to enabling the floating button if we care about that).
One finding from the comparison: the homepage embed does not set localConsentDomain, so its consent cookie is host-only on daily.dev and isn't visible to app.daily.dev — meaning the existing homepage→webapp consent bridge likely never worked cross-subdomain. Our side sets localConsentDomain so webapp consent IS visible to the homepage; for the reverse direction the marketing site should add localConsentDomain: 'daily.dev' to its embed (separate change, not this repo).
There was a problem hiding this comment.
there is no app.daily.dev, it worked with getIubendaConsent from 9171dd8
There was a problem hiding this comment.
Thanks for the correction — I wrongly assumed an app. subdomain; since the webapp and homepage share the daily.dev host, the host-only cookie was always readable and the bridge works as that commit intended. Keeping localConsentDomain here anyway so the consent cookie also covers subdomains (e.g. *.preview.app.daily.dev), same scoping our ilikecookies* cookies already use — shout if you'd rather drop it.
startTcfSubscription ran synchronously after appending the script tags, before the stub executed, so __tcfapi was undefined and the subscription no-oped — the TC string never reached the store or localStorage.
countryDetection, LGPD/USPR and cookiePolicyInOtherWindow now match the marketing site so both surfaces classify users the same way.
| * the actual consent. In scope without a TC string, vendors must treat the | ||
| * request as not-consented. | ||
| */ | ||
| export const hasMarketingConsent = (): boolean => { |
There was a problem hiding this comment.
why was this removed, it was added as compatibility layer between our consent (when we did not have iubenda), so it should stay until we deploy it to everyone without feature flag
There was a problem hiding this comment.
You're right — restored in d063964. resolveAdConsent now only uses TCF semantics when actual TCF data exists (CMP loaded and answered); otherwise it falls back to the exact pre-PR behavior via hasMarketingConsent() (first-party marketing cookie or extension → gdpr=0). So flag-off users and the extension keep their current measurement untouched during the transition, and the compatibility layer can be deleted once the flag is rolled out to everyone.
resolveAdConsent falls back to the pre-TCF behavior (first-party marketing cookie / extension => gdpr=0) whenever no TCF data exists, so flag-off users keep their current measurement during the transition.
|
|
||
| export const getStoredTcString = (): string | undefined => { | ||
| try { | ||
| return globalThis?.localStorage?.getItem(TCF_STORAGE_KEY) || undefined; |
There was a problem hiding this comment.
why do we need this localStorage overhead, seems like something that can get out of sync, ideally tcf somehow handles it and exposes value outside of listener?
There was a problem hiding this comment.
Agreed, dropped in 746ffd9. The CMP already persists the TC string itself in the IAB-conventional euconsent-v2 cookie, so getStoredTcString now reads that instead of a localStorage mirror — single source of truth owned by iubenda, nothing to drift. (Some persisted read is unavoidable for boot: the request fires before the CMP script loads, so the live __tcfapi can never answer in time; the in-memory snapshot stays for the reactive in-session consumers.)
| }, | ||
| }; | ||
|
|
||
| IUBENDA_SCRIPTS.forEach((src, index) => { |
There was a problem hiding this comment.
there is a <Script /> next.js component as well that Iubenda can use to render instead of manual work, potentially better
There was a problem hiding this comment.
Considered it — manual injection is deliberate here: the three scripts must execute in order (stub → safe-tcf → core), and dynamically-inserted scripts with async = false are the spec-guaranteed way to get ordered execution with parallel downloads. next/script afterInteractive doesn't guarantee cross-script execution order, and chaining via onLoad would serialize the three downloads (slower waterfall). Happy to switch if you feel strongly, but we'd be trading a spec guarantee for component sugar.
There was a problem hiding this comment.
ok, but seems weird this execution order is needed, is it from iubenda docs?
| onPreferenceRef.current = (pref) => { | ||
| const marketing = pref?.purposes?.['5'] === true; | ||
| saveCookies( | ||
| marketing ? otherGdprConsents : [], |
There was a problem hiding this comment.
i think once we move to iubenda we could just ignore our current cookies no? two integrations do not need to cross write anymore?
There was a problem hiding this comment.
Long-term yes — but not while the CMP is EU-only. Non-EU users never load iubenda (flag + geo gated), so ilikecookies* stays the source of truth for them, and every consumer (Pixels consent mode, settings toggles, YouTube embeds, the adConsent fallback you asked to keep) reads those cookies. The mirror keeps one canonical downstream representation so none of those consumers need to care which banner produced the consent. Once/if iubenda goes global we can port consumers to read the CMP state directly and delete both the mirror and the homegrown banner in one sweep — added it as a follow-up note.
capJavert
left a comment
There was a problem hiding this comment.
@pnvasanth left some comments, but either way I am fine with deploying iubenda just we must ensure everything related to it happens only if feature flag is on, and when off site works as it worked before.
Drop the localStorage mirror; the CMP already persists the TC string in the IAB-conventional euconsent-v2 cookie, so getStoredTcString reads that instead — single source of truth, nothing to drift.
With iubenda_cmp off, ad requests and log events are now byte-identical to pre-CMP behavior: no gdpr/gdpr_consent params, no consent_id. The feed test mocks revert to their original main-branch form, proving the parity.
Changes
Adds the iubenda Cookie Solution (IAB TCF v2.2) as CMP for GDPR-covered users, behind the new
iubenda_cmpGrowthBook flag (default off). Companion PRs: dailydotdev/skadi#352 (deploy first), dailydotdev/daily-api#4027.Iubenda.tsx(webapp only — extension CSP blocks CMP scripts): loads stub → safe-tcf → core in order for GDPR-covered users, mirrors iubenda purposes into the existingilikecookies*cookies (incl. a new withdrawal path inuseCookieConsentthat updates the react-query cache), suppresses the homegrown banner for those users.lib/tcf.ts: store over__tcfapiexposing{gdprApplies, tcString, addtlConsent}; mirrors the TC string to localStorage so the next boot sends it as anx-gdpr-consentheader (lib/boot.ts).resolveAdConsentpreviously abusedgdpr=1to mean "not consented" — nowgdprAppliesstates scope and the TC string carries consent;useAdMacroContextrecomputes on consent change. The existing${GDPR_CONSENT_755}/${ADDTL_CONSENT}macros now populate (pixels, measurement tags, click URLs, incl. the extension measurement frame contract).gdpr/gdpr_consent/addtl_consent; consent is part of the ad queryKey so ads refetch after the banner is answered.Behavior changes: everything is gated on
iubenda_cmp. Flag off (or extension) = byte-identical pre-PR behavior — no CMP script, homegrown banner, legacyhasMarketingConsent()macro semantics, no consent params on ad requests, noconsent_idon log events (the feed test mocks are unchanged from main, which proves the parity). Flag on = TCF banner,gdpr/gdpr_consent/addtl_consenton ad requests, boot consent header,consent_idon events. The single flag-independent change is a bug fix: withdrawing marketing consent in settings now takes effect immediately instead of after a reload.Rollout checklist
Values pulled from the live daily.dev homepage's iubenda embed (same site/policy so consent is shared across
*.daily.dev):NEXT_PUBLIC_IUBENDA_SITE_ID=1334205✓ verified inlined in the preview buildNEXT_PUBLIC_IUBENDA_POLICY_ID=14695236✓ verified inlined in the preview buildpackages/webapp/.env(edit-protected, not included in this PR)enableTcf: true) — verified end-to-end on the preview: TCF banner renders, Accept produces a valid TC string (865 chars) + Google ACM string, ads refetch withgdpr_consent/addtl_consent, prod skadi accepts them (200)iubenda_cmpfeature created (defaultfalse), force-enabled for the test account ✓ verified evaluating on previewlocalConsentDomain: 'daily.dev'to the homepage embed so homepage consent is visible toapp.daily.dev(today its cookie is host-only; webapp→homepage sharing already works)iubenda_cmpflag 0% → EU-targeted (GB geo attributes available from boot) → consider tighteningoutsideGdprbefore 100%ilikecookies*mirror, thehasMarketingConsent()compatibility layer, and the homegrown bannerEvents
New shared property on all log events:
consent_id— the iubenda consent-record correlator (cons.randfrom the_iub_cs-*cookie), unset when no iubenda consent exists. Needs coordination with the analytics pipeline.Preview domain
https://feat-iubenda-cmp-tcf-consent.preview.app.daily.dev