import { getFbpFbc } from '../metapixel/fbc.ts'; import type { TrackingManager } from '$lib/tracking.svelte'; import { capiErrorBodySchema, capiResponseBodySchema, type CAPIErrorBody, type CAPIRequestBody, type CAPIResponseBody } from './handle.ts'; import type { CAPICustomerInfoParams, CAPIEvent } from './event.ts'; import * as v from 'valibot'; import { dev } from '$app/environment'; /** * Client abstracts HTTP communication with a spectator server endpoint for * sending conversion events to Meta Conversions API. */ export class CAPIClient { private _href: string; private _trackingManager: TrackingManager; /** * Creates a new CAPIClient. * * @param serverHref - The spectator server endpoint URL. * @param trackingManager - The tracking manager instance, used for consent status. */ constructor(serverHref: string, trackingManager: TrackingManager) { this._href = serverHref; this._trackingManager = trackingManager; } /** * Sends CAPIEvents to the server endpoint. If no consent is given, no events * are sent and a dummy response is returned. * @param events - The array of CAPIEvents to send. * @returns A promise that resolves to the server response. * @throws Will throw an error if input or response shape validation fails. */ async sendEvents(events: CAPIEvent[]): Promise { // Respond with an empty response if consent is not given if (!this._trackingManager.haveUserConsent()) { if (dev) { console.warn(`[CAPIClient] Consent not given. Skipping sending ${events.length} event(s).`); } return { fbtrace_id: '', events_received: 0, messages: [] }; } // Attempt to build enriched user data const { fbp, fbc } = getFbpFbc(); const enrichedUserData: Partial = { fbp, fbc }; // Build request body const body: CAPIRequestBody = { events: events.map((e) => { e.enrichUserData(enrichedUserData); return e.toObject(); }) }; try { v.parse(v.object({ events: v.array(v.any()) }), body); // Validate body shape } catch (err) { throw new Error(`[CAPIClient] Invalid request body shape: ${(err as Error).message}`); } const response = await fetch(this._href, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const json = await response.json(); try { if (response.ok) { const parsed = v.parse(capiResponseBodySchema, json); return parsed as CAPIResponseBody; } else { const parsed = v.parse(capiErrorBodySchema, json); return parsed as CAPIErrorBody; } } catch (err) { throw new Error(`[CAPIClient] Invalid response shape: ${(err as Error).message}`); } } /** * Shorthand for sending a single CAPIEvent to the server endpoint. * @param event - The CAPIEvent to send. * @returns A promise that resolves to the server response. * @throws Will throw an error if input or response shape validation fails. */ async trackEvent(event: CAPIEvent): Promise { return this.sendEvents([event]); } }