Make consent API calls fire-and-forget to avoid blocking UI
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é <emile@getprobo.com>
This commit is contained in:
@@ -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`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ export class CookieBannerClient {
|
||||
return this.consent !== null;
|
||||
}
|
||||
|
||||
async acceptAll(): Promise<ConsentRecord> {
|
||||
acceptAll(): void {
|
||||
const cfg = this.config;
|
||||
|
||||
const consentData: Record<string, boolean> = {};
|
||||
@@ -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<ConsentRecord> {
|
||||
rejectAll(): void {
|
||||
const cfg = this.config;
|
||||
|
||||
const consentData: Record<string, boolean> = {};
|
||||
@@ -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<string, boolean>,
|
||||
): Promise<ConsentRecord> {
|
||||
customize(categories: Record<string, boolean>): void {
|
||||
const cfg = this.config;
|
||||
|
||||
const consentData: Record<string, boolean> = {};
|
||||
@@ -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<string, boolean>,
|
||||
): Promise<ConsentRecord> {
|
||||
): 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<ConsentRecord>(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<ConsentRecord>(url, { method: "POST", body })
|
||||
.then(() => void flush(this.bannerId))
|
||||
.catch(() => enqueue(this.bannerId, url.href, body));
|
||||
}
|
||||
|
||||
private activate(consentData: Record<string, boolean>): void {
|
||||
|
||||
@@ -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" },
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user