/** * Brevo's web tracker (#56). * * Same shape as paypal.ts: the script is injected at runtime, so there is no * package to import types from and this file declares the sliver actually used. * Deliberately narrow — it describes what is called here, not the whole SDK. * * Two rules hold everywhere in this module, and every exported function is * written so that breaking either is impossible rather than merely discouraged: * * 1. **Nothing loads without a key.** No key means no script, no cookie, no * request. That is what keeps QA out of the production Brevo account. * 2. **Nothing loads without consent.** `start` is the only thing that injects * the script, and its caller gates on the customer's `analytics_consent`, * which the server derives from the wording that customer actually agreed * to. A signed-out visitor is never tracked, because there is no consent * record to consult. */ interface SendinblueSdk { page: (name?: string, properties?: Record) => void; identify: (email: string, attributes?: Record) => void; track: (event: string, properties?: Record) => void; } /** The queue the snippet installs so calls made before load are not lost. */ interface SibQueue { equeue: unknown[]; client_key?: string; [method: string]: unknown; } declare global { interface Window { sendinblue?: SendinblueSdk; sib?: SibQueue; } } /** * Gates every call below. False until `start`, and false again after `stop`. * * Kept separate from `window.sendinblue` being present because the two answer * different questions: the SDK stays in the document once injected, but consent * can be withdrawn within the same page. Checking only for the SDK would keep * reporting after sign-out. */ let enabled = false; let loaded = false; /** * Installs the method queue and injects the script. * * The queue matters: `sa.js` loads asynchronously and the methods have to exist * before it arrives, or the `page()` for the landing route — the one call that * always happens immediately — is dropped. This is Brevo's own snippet, written * out as typed code rather than pasted as an opaque blob. */ function inject(key: string): void { if (loaded) return; loaded = true; const queue: SibQueue = { equeue: [], client_key: key }; window.sib = queue; const sdk = {} as SendinblueSdk; const methods = ['track', 'identify', 'page'] as const; for (const method of methods) { sdk[method] = (...args: unknown[]) => { const ready = queue[method]; if (typeof ready === 'function') { (ready as (...a: unknown[]) => void)(...args); } else { queue.equeue.push({ [method]: args }); } }; } window.sendinblue = sdk; const script = document.createElement('script'); script.id = 'sendinblue-js'; script.async = true; script.src = `https://sibautomation.com/sa.js?key=${encodeURIComponent(key)}`; // A tracker that cannot load must never take the page down with it. There is // no retry and no error surfaced: losing analytics is not worth telling a // customer about, and a visible failure here would be noise on every visit // from anyone running a blocker. script.onerror = () => { enabled = false; }; document.head.appendChild(script); } /** * Begins tracking for a consenting, signed-in customer. * * Safe to call repeatedly — React effects will. The script is injected once; * later calls only re-assert identity, which is what a customer switching * accounts in one session needs. */ export function startBrevoTracking(key: string | null, email: string): void { if (!key) return; inject(key); enabled = true; window.sendinblue?.identify(email); } /** * Stops tracking, on sign-out or when consent is withdrawn. * * **This stops calls, it does not unload the script.** Nothing can un-inject a * script tag or take back the cookies it set, so `sa.js` stays in the document * until the next full page load. What this guarantees is that no further page * view or event is reported, and no identity is re-asserted, which is the part * this application actually controls. Said plainly here because "tracking * stops" is easy to read as a stronger promise than any web tracker can make. */ export function stopBrevoTracking(): void { enabled = false; } /** A route change. No-op unless tracking is currently permitted. */ export function brevoPage(path: string): void { if (!enabled) return; window.sendinblue?.page(path); } /** A named event. No-op unless tracking is currently permitted. */ export function brevoTrack(event: string, properties?: Record): void { if (!enabled) return; window.sendinblue?.track(event, properties); } /** Exported for tests, which need to observe the gate without a real script. */ export function isBrevoTrackingEnabled(): boolean { return enabled; }