From c942aaa7bb373d9a5a4dcfda98c1072763f4838c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Wed, 22 Apr 2026 10:29:28 +0400 Subject: [PATCH] Make consent API calls fire-and-forget to avoid blocking UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server-side consent record is for audit purposes and does not need to complete before the UI responds. Local state (cookie, script activation) is applied synchronously, and the API call runs in the background with queue fallback on failure. Signed-off-by: Émile Ré --- packages/cookie-banner/src/activation.ts | 52 +++++-------------- packages/cookie-banner/src/client.ts | 48 ++++++----------- .../cookie-banner/src/components/buttons.ts | 38 +++++++------- .../src/components/preference-panel.ts | 19 ++++--- 4 files changed, 57 insertions(+), 100 deletions(-) diff --git a/packages/cookie-banner/src/activation.ts b/packages/cookie-banner/src/activation.ts index 5764b93a2..6416a8db2 100644 --- a/packages/cookie-banner/src/activation.ts +++ b/packages/cookie-banner/src/activation.ts @@ -314,53 +314,29 @@ function deactivateElement(el: Element, label?: string): void { } } -const KNOWN_MULTI_PART_TLDS = new Set([ - "co.uk", - "co.jp", - "co.kr", - "co.nz", - "co.za", - "co.in", - "co.id", - "com.au", - "com.br", - "com.cn", - "com.mx", - "com.tw", - "com.hk", - "com.sg", - "com.ar", - "com.co", - "com.tr", - "net.au", - "org.uk", - "org.au", - "ac.uk", - "gov.uk", - "ne.jp", - "or.jp", -]); - -function getRootDomain(hostname: string): string { +function getCandidateDomains(hostname: string): string[] { const parts = hostname.split("."); - if (parts.length <= 2) { - return parts.length === 2 ? "." + hostname : hostname; + if (parts.length <= 1) return []; + + const candidates: string[] = []; + // Try progressively broader parent domains. The browser silently + // ignores attempts to clear cookies on public suffixes, so + // over-trying is safe and avoids maintaining a TLD list. + for (let i = 0; i < parts.length - 1; i++) { + candidates.push("." + parts.slice(i).join(".")); } - const lastTwo = parts.slice(-2).join("."); - if (KNOWN_MULTI_PART_TLDS.has(lastTwo)) { - return "." + parts.slice(-3).join("."); - } - - return "." + lastTwo; + return candidates; } function removeCookies(names: string[]): void { - const rootDomain = getRootDomain(location.hostname); + const domains = getCandidateDomains(location.hostname); for (const name of names) { document.cookie = `${name}=; path=/; max-age=0`; - document.cookie = `${name}=; path=/; domain=${rootDomain}; max-age=0`; + for (const domain of domains) { + document.cookie = `${name}=; path=/; domain=${domain}; max-age=0`; + } } } diff --git a/packages/cookie-banner/src/client.ts b/packages/cookie-banner/src/client.ts index 8c42195de..76284b3ea 100644 --- a/packages/cookie-banner/src/client.ts +++ b/packages/cookie-banner/src/client.ts @@ -153,7 +153,7 @@ export class CookieBannerClient { return this.consent !== null; } - async acceptAll(): Promise { + acceptAll(): void { const cfg = this.config; const consentData: Record = {}; @@ -161,10 +161,10 @@ export class CookieBannerClient { consentData[cat.name] = true; } - return this.recordConsent("ACCEPT_ALL", consentData); + this.recordConsent("ACCEPT_ALL", consentData); } - async rejectAll(): Promise { + rejectAll(): void { const cfg = this.config; const consentData: Record = {}; @@ -172,12 +172,10 @@ export class CookieBannerClient { consentData[cat.name] = cat.kind === "NECESSARY"; } - return this.recordConsent("REJECT_ALL", consentData); + this.recordConsent("REJECT_ALL", consentData); } - async customize( - categories: Record, - ): Promise { + customize(categories: Record): void { const cfg = this.config; const consentData: Record = {}; @@ -185,39 +183,21 @@ export class CookieBannerClient { consentData[cat.name] = cat.kind === "NECESSARY" || !!categories[cat.name]; } - return this.recordConsent("CUSTOMIZE", consentData); + this.recordConsent("CUSTOMIZE", consentData); } - private async recordConsent( + private recordConsent( action: ConsentAction, consentData: Record, - ): Promise { + ): void { const cfg = this.config; - const url = new URL(`${this.bannerId}/consents`, this.baseUrl); - const body = { - visitor_id: this.visitorId, - version: cfg.version, - action, - consent_data: consentData, - }; - - let record: ConsentRecord | null = null; - try { - record = await fetchJSON(url, { - method: "POST", - body, - }); - void flush(this.bannerId); - } catch { - enqueue(this.bannerId, url.href, body); - } this.consent = { visitor_id: this.visitorId, version: cfg.version, action, consent_data: consentData, - created_at: record?.created_at ?? "", + created_at: "", }; setConsentCookie( @@ -232,12 +212,16 @@ export class CookieBannerClient { this.activate(consentData); - return record ?? { - id: "", + const url = new URL(`${this.bannerId}/consents`, this.baseUrl); + const body = { visitor_id: this.visitorId, + version: cfg.version, action, - created_at: "", + consent_data: consentData, }; + void fetchJSON(url, { method: "POST", body }) + .then(() => void flush(this.bannerId)) + .catch(() => enqueue(this.bannerId, url.href, body)); } private activate(consentData: Record): void { diff --git a/packages/cookie-banner/src/components/buttons.ts b/packages/cookie-banner/src/components/buttons.ts index e3a4ba8ff..ca87dba24 100644 --- a/packages/cookie-banner/src/components/buttons.ts +++ b/packages/cookie-banner/src/components/buttons.ts @@ -34,32 +34,30 @@ class ProboActionButton extends ProboElement { export class ProboAcceptButton extends ProboActionButton { protected handleClick = (): void => { if (!this.root) return; - void this.root.client.acceptAll().then(() => { - this.root!.setState("hidden"); - this.root!.dispatchEvent( - new CustomEvent("probo-consent", { - bubbles: true, - composed: true, - detail: { action: "ACCEPT_ALL" }, - }), - ); - }); + this.root.client.acceptAll(); + this.root.setState("hidden"); + this.root.dispatchEvent( + new CustomEvent("probo-consent", { + bubbles: true, + composed: true, + detail: { action: "ACCEPT_ALL" }, + }), + ); }; } export class ProboRejectButton extends ProboActionButton { protected handleClick = (): void => { if (!this.root) return; - void this.root.client.rejectAll().then(() => { - this.root!.setState("hidden"); - this.root!.dispatchEvent( - new CustomEvent("probo-consent", { - bubbles: true, - composed: true, - detail: { action: "REJECT_ALL" }, - }), - ); - }); + this.root.client.rejectAll(); + this.root.setState("hidden"); + this.root.dispatchEvent( + new CustomEvent("probo-consent", { + bubbles: true, + composed: true, + detail: { action: "REJECT_ALL" }, + }), + ); }; } diff --git a/packages/cookie-banner/src/components/preference-panel.ts b/packages/cookie-banner/src/components/preference-panel.ts index 989f8d013..247252189 100644 --- a/packages/cookie-banner/src/components/preference-panel.ts +++ b/packages/cookie-banner/src/components/preference-panel.ts @@ -78,15 +78,14 @@ export class ProboSaveButton extends ProboElement { private handleClick = (): void => { if (!this.root) return; const draft = { ...this.root.consentDraft }; - void this.root.client.customize(draft).then(() => { - this.root!.setState("hidden"); - this.root!.dispatchEvent( - new CustomEvent("probo-consent", { - bubbles: true, - composed: true, - detail: { action: "CUSTOMIZE", consent_data: draft }, - }), - ); - }); + this.root.client.customize(draft); + this.root.setState("hidden"); + this.root.dispatchEvent( + new CustomEvent("probo-consent", { + bubbles: true, + composed: true, + detail: { action: "CUSTOMIZE", consent_data: draft }, + }), + ); }; }