Skip consent API fetch for new visitors

When visitorId is null (no localStorage entry), the client now
skips both the cookie check and the /consents/:id fetch that
was always returning 404 for first-time visitors. The visitor ID
is created lazily on first consent action instead.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-05-13 11:57:01 +04:00
parent 6c6137f9f2
commit e9d786c5d6
2 changed files with 69 additions and 54 deletions

View File

@@ -33,7 +33,7 @@ import type {
Regulation, Regulation,
VisitorConsent, VisitorConsent,
} from "./types"; } from "./types";
import { getOrCreateVisitorId } from "./visitor"; import { getOrCreateVisitorId, getVisitorId } from "./visitor";
export type { export type {
BannerConfig, BannerConfig,
@@ -49,7 +49,7 @@ export type {
export class CookieBannerClient { export class CookieBannerClient {
private readonly baseUrl: URL; private readonly baseUrl: URL;
private readonly bannerId: string; private readonly bannerId: string;
private readonly visitorId: string; private visitorId: string | null;
private readonly lang: string; private readonly lang: string;
private readonly integrations: ConsentIntegration[]; private readonly integrations: ConsentIntegration[];
@@ -68,7 +68,7 @@ export class CookieBannerClient {
} }
this.baseUrl = new URL(base); this.baseUrl = new URL(base);
this.bannerId = config.bannerId; this.bannerId = config.bannerId;
this.visitorId = getOrCreateVisitorId(config.bannerId); this.visitorId = getVisitorId(config.bannerId);
this.lang = detectLanguage(config.lang); this.lang = detectLanguage(config.lang);
this.integrations = createDefaultIntegrations(); this.integrations = createDefaultIntegrations();
} }
@@ -97,50 +97,52 @@ export class CookieBannerClient {
} }
this.startDetector(config); this.startDetector(config);
const cookie = getConsentCookie(); if (this.visitorId) {
if (cookie && cookie.bid === this.bannerId && cookie.v === config.version && cookie.vid === this.visitorId) { const cookie = getConsentCookie();
this.consent = { if (cookie && cookie.bid === this.bannerId && cookie.v === config.version && cookie.vid === this.visitorId) {
visitor_id: cookie.vid, this.consent = {
version: cookie.v, visitor_id: cookie.vid,
action: cookie.action, version: cookie.v,
consent_data: cookie.data, action: cookie.action,
created_at: "", consent_data: cookie.data,
}; created_at: "",
this._gpcApplied = cookie.action === "GPC"; };
this.activate(cookie.data); this._gpcApplied = cookie.action === "GPC";
void flush(this.bannerId); this.activate(cookie.data);
return; void flush(this.bannerId);
} return;
}
const consentUrl = new URL( const consentUrl = new URL(
`${this.bannerId}/consents/${this.visitorId}`, `${this.bannerId}/consents/${this.visitorId}`,
this.baseUrl, this.baseUrl,
);
const apiConsent = await fetchJSON<VisitorConsent>(consentUrl).catch(
(err) => {
if (err instanceof NotFoundError) {
return null;
}
throw err;
},
);
if (apiConsent && apiConsent.version === config.version) {
this.consent = apiConsent;
this._gpcApplied = apiConsent.action === "GPC";
setConsentCookie(
{
bid: this.bannerId,
v: apiConsent.version,
vid: apiConsent.visitor_id,
action: apiConsent.action,
data: apiConsent.consent_data,
},
config.consent_expiry_days,
); );
this.activate(apiConsent.consent_data); const apiConsent = await fetchJSON<VisitorConsent>(consentUrl).catch(
} else { (err) => {
this.consent = null; if (err instanceof NotFoundError) {
return null;
}
throw err;
},
);
if (apiConsent && apiConsent.version === config.version) {
this.consent = apiConsent;
this._gpcApplied = apiConsent.action === "GPC";
setConsentCookie(
{
bid: this.bannerId,
v: apiConsent.version,
vid: apiConsent.visitor_id,
action: apiConsent.action,
data: apiConsent.consent_data,
},
config.consent_expiry_days,
);
this.activate(apiConsent.consent_data);
} else {
this.consent = null;
}
} }
if (!this.consent && this.gpcDetected) { if (!this.consent && this.gpcDetected) {
@@ -225,6 +227,13 @@ export class CookieBannerClient {
this.recordConsent("CUSTOMIZE", consentData); this.recordConsent("CUSTOMIZE", consentData);
} }
private ensureVisitorId(): string {
if (!this.visitorId) {
this.visitorId = getOrCreateVisitorId(this.bannerId);
}
return this.visitorId;
}
private recordConsent( private recordConsent(
action: ConsentAction, action: ConsentAction,
consentData: Record<string, boolean>, consentData: Record<string, boolean>,
@@ -232,9 +241,10 @@ export class CookieBannerClient {
this._gpcApplied = action === "GPC"; this._gpcApplied = action === "GPC";
const cfg = this.config; const cfg = this.config;
const visitorId = this.ensureVisitorId();
this.consent = { this.consent = {
visitor_id: this.visitorId, visitor_id: visitorId,
version: cfg.version, version: cfg.version,
action, action,
consent_data: consentData, consent_data: consentData,
@@ -245,7 +255,7 @@ export class CookieBannerClient {
{ {
bid: this.bannerId, bid: this.bannerId,
v: cfg.version, v: cfg.version,
vid: this.visitorId, vid: visitorId,
action, action,
data: consentData, data: consentData,
}, },
@@ -256,7 +266,7 @@ export class CookieBannerClient {
const url = new URL(`${this.bannerId}/consents`, this.baseUrl); const url = new URL(`${this.bannerId}/consents`, this.baseUrl);
const body = { const body = {
visitor_id: this.visitorId, visitor_id: visitorId,
version: cfg.version, version: cfg.version,
action, action,
consent_data: consentData, consent_data: consentData,

View File

@@ -14,18 +14,23 @@
import { COOKIE_NAME } from "./cookie"; import { COOKIE_NAME } from "./cookie";
export function getOrCreateVisitorId(bannerId: string): string { export function getVisitorId(bannerId: string): string | null {
const key = `${COOKIE_NAME}:${bannerId}:vid`; const key = `${COOKIE_NAME}:${bannerId}:vid`;
try { try {
const stored = localStorage.getItem(key); return localStorage.getItem(key);
if (stored) {
return stored;
}
} catch { } catch {
// localStorage unavailable return null;
}
}
export function getOrCreateVisitorId(bannerId: string): string {
const existing = getVisitorId(bannerId);
if (existing) {
return existing;
} }
const key = `${COOKIE_NAME}:${bannerId}:vid`;
const array = new Uint8Array(16); const array = new Uint8Array(16);
crypto.getRandomValues(array); crypto.getRandomValues(array);
const id = Array.from(array, (b) => b.toString(16).padStart(2, "0")).join(""); const id = Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");