diff --git a/src/lib/Umami.svelte b/src/lib/Umami.svelte index 4920ed1..4593d5d 100644 --- a/src/lib/Umami.svelte +++ b/src/lib/Umami.svelte @@ -64,7 +64,7 @@ } }); - if (dev) log.info('[Umami] [dev]: tracking disabled'); + if (dev) log.info('[Umami] [dev]: reporting disabled'); onMount(() => { if (dev) { diff --git a/src/lib/capi/client.ts b/src/lib/capi/client.ts index f5c0846..195014c 100644 --- a/src/lib/capi/client.ts +++ b/src/lib/capi/client.ts @@ -31,7 +31,8 @@ export class CAPIClient { } /** - * Sends CAPIEvents to the server endpoint. + * 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. @@ -43,10 +44,8 @@ export class CAPIClient { console.warn(`[CAPIClient] Consent not given. Skipping sending ${events.length} event(s).`); } return { - pixelID: '', - fbTraceID: '', - receivedEvents: 0, - processedEvents: 0, + fbtrace_id: '', + events_received: 0, messages: [] }; } @@ -62,7 +61,11 @@ export class CAPIClient { return e.toObject(); }) }; - v.parse(v.object({ events: v.array(v.any()) }), body); // Validate body shape + 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', @@ -73,12 +76,16 @@ export class CAPIClient { }); const json = await response.json(); - if (response.ok) { - const parsed = v.parse(capiResponseBodySchema, json); - return parsed as CAPIResponseBody; - } else { - const parsed = v.parse(capiErrorBodySchema, json); - return parsed as CAPIErrorBody; + 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}`); } } diff --git a/src/lib/capi/connector.ts b/src/lib/capi/connector.ts index 4ca7cc3..5a81d3f 100644 --- a/src/lib/capi/connector.ts +++ b/src/lib/capi/connector.ts @@ -1,25 +1,11 @@ import { dev } from '$app/environment'; import log from 'loglevel'; import type { CAPIEvent } from './event.ts'; +import * as v from 'valibot'; +import { capiResponseBodySchema, type CAPIResponseBody } from './handle.ts'; const GRAPH_VERSION = 'v24.0'; -/** - * Response body from Meta Conversion API after sending events. - */ -export type CAPIRequestResponseBody = { - /** Dataset or Pixel ID to which the event successfully posted. */ - pixelID: string; - /** fbtrace_id for debugging purposes. */ - fbtrace_id: string; - /** Number of events received that were sent by the request. */ - receivedEvents: number; - /** Number of events successfully posted by the request. */ - processedEvents: number; - /** Messages returned by the server. */ - messages: string[]; -}; - /** * Connector class for Meta Conversion API (CAPI). Abstraction over direct HTTP * requests to Meta's CAPI endpoint. @@ -52,7 +38,7 @@ export class CAPIConnector { * @returns The response from the Meta CAPI. * @throws Will throw an error if the request fails or the API returns an error. */ - async sendEvents(events: CAPIEvent[]): Promise { + async sendEvents(events: CAPIEvent[]): Promise { if (dev && !this._testEventCode) { log.warn( `[CAPIConnector] Sending ${events.length} event(s) in dev mode without a test event code. ` + @@ -60,14 +46,17 @@ export class CAPIConnector { ); } - const url = `https://graph.facebook.com/${GRAPH_VERSION}/${this._pixelID}/events?access_token=${this._accessToken}`; + const url = `https://graph.facebook.com/${GRAPH_VERSION}/${this._pixelID}/events`; const body = { data: events.map((e) => e.toObject()), test_event_code: this._testEventCode }; + log.debug( + `[CAPIConnector] [${this._pixelID}] Sending ${events.length} event(s) to Meta CAPI at ${url} with body: ${JSON.stringify(body, null, 2)}` + ); - const resp = await fetch(url, { + const resp = await fetch(`${url}?access_token=${this._accessToken}`, { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -75,13 +64,20 @@ export class CAPIConnector { body: JSON.stringify(body) }); const json = await resp.json(); - console.log('CAPI response:', json); if (!resp.ok) { throw new Error(`Meta CAPI error ${resp.status}: ${JSON.stringify(json, null, 2)}`); } - return {} as CAPIRequestResponseBody; + try { + const parsed = v.parse(capiResponseBodySchema, json); + log.info( + `[CAPIConnector] [${this._pixelID}] Successfully sent ${events.length} event(s) to Meta CAPI.` + ); + return parsed as CAPIResponseBody; + } catch (err) { + throw new Error(`[CAPIConnector] Invalid response shape: ${(err as Error).message}`); + } } /** @@ -91,7 +87,7 @@ export class CAPIConnector { * @returns The response from the Meta CAPI. * @throws Will throw an error if the request fails or the API returns an error. */ - async trackEvent(event: CAPIEvent): Promise { + async trackEvent(event: CAPIEvent): Promise { return this.sendEvents([event]); } } diff --git a/src/lib/capi/handle.ts b/src/lib/capi/handle.ts index 9ad1ff4..2d8c4a6 100644 --- a/src/lib/capi/handle.ts +++ b/src/lib/capi/handle.ts @@ -21,10 +21,8 @@ export const capiErrorBodySchema = v.object({ export type CAPIErrorBody = v.InferOutput; export const capiResponseBodySchema = v.object({ - pixelID: v.string(), - fbTraceID: v.string(), - receivedEvents: v.number(), - processedEvents: v.number(), + fbtrace_id: v.string(), + events_received: v.number(), messages: v.array(v.string()) }); @@ -40,8 +38,8 @@ export type CAPIResponseBody = v.InferOutput; export const createCAPIHandler: (connector: CAPIConnector) => RequestHandler = (connector) => { const handle: RequestHandler = async ({ request, getClientAddress, cookies }) => { try { - const json = await request.json(); - const parsed = v.parse(capiRequestBodySchema, json); + const jsonBody = await request.json(); + const parsed = v.parse(capiRequestBodySchema, jsonBody); // Build enriched user data with IP, user agent, and fbp/fbc from cookies const ip = getRequestIP(request, getClientAddress); diff --git a/src/lib/index.ts b/src/lib/index.ts index 8cca638..2f7b126 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -13,6 +13,7 @@ export * from './util/ip.ts'; // set log level to debug if we're in dev mode if (dev) { log.setLevel('debug'); + log.debug('[spectator] Log level set to debug'); } else { log.setLevel('warn'); } diff --git a/src/lib/metapixel/pixel-control.ts b/src/lib/metapixel/pixel-control.ts index c1ea397..f68a37b 100644 --- a/src/lib/metapixel/pixel-control.ts +++ b/src/lib/metapixel/pixel-control.ts @@ -192,6 +192,16 @@ export class PixelControl { return trackingManager?.haveUserConsent() ?? false; } + /** Warns if we're in dev mode and no test code is set */ + private devModeWarn() { + if (dev && !this._testEventCode) { + log.warn( + `[PixelControl] [${this._pixelID}] Sending events in dev mode without a test event code. ` + + 'Consider providing a test event code to avoid affecting real data.' + ); + } + } + /** * Shorthand utility to send a PageView event * @param disableCAPI If true, disables sending this event to the Conversion API @@ -235,7 +245,7 @@ export class PixelControl { ) .then((response) => { log.debug( - `[PixelControl] [${this._pixelID}] ${event} event sent to Conversion API with Event ID: ${eventID}, Response: ${JSON.stringify( + `[PixelControl] [${this._pixelID}] ${event} event forwarded to Conversion API with Event ID: ${eventID}, Response: ${JSON.stringify( response, null, 2 @@ -244,10 +254,12 @@ export class PixelControl { }) .catch((error) => { log.error( - `[PixelControl] [${this._pixelID}] Failed to send ${event} event to Conversion API with Event ID: ${eventID}`, + `[PixelControl] [${this._pixelID}] Failed to forward ${event} event to Conversion API with Event ID: ${eventID}`, error ); }); + + return eventID; } /** @@ -265,6 +277,7 @@ export class PixelControl { disableCAPI: boolean = false ) { if (!this.consentGuard()) return; + this.devModeWarn(); // Optionally, send to conversion API endpoint if (!disableCAPI) { @@ -272,19 +285,13 @@ export class PixelControl { } // Send the event to Meta via the pixel - if (!dev || this._testEventCode) { - window.fbq('trackSingle', this._pixelID, event, params, { - eventID, - test_event_code: this._testEventCode - }); - log.debug( - `[PixelControl] [${this._pixelID}] ${event} event sent${dev && ` (test code: ${this._testEventCode})`}.` - ); - } else { - log.info( - `[PixelControl] [${this._pixelID}] ${event} event not sent in development mode without a test event code.` - ); - } + window.fbq('trackSingle', this._pixelID, event, params, { + eventID, + test_event_code: this._testEventCode + }); + log.debug( + `[PixelControl] [${this._pixelID}] ${event} event sent with event ID ${eventID} ${dev && ` (test code: ${this._testEventCode})`}.` + ); } /** @@ -302,25 +309,20 @@ export class PixelControl { disableCAPI: boolean = false ) { if (!this.consentGuard()) return; + this.devModeWarn(); - // Optionally, send to conversdion API endpoint + // Optionally, send to conversion API endpoint if (!disableCAPI) { eventID = this.forwardToCAPI(event, params, eventID); } // Send the event to Meta via the pixel - if (!dev || this._testEventCode) { - window.fbq('trackSingleCustom', this._pixelID, event, params, { - eventID, - test_event_code: this._testEventCode - }); - log.debug( - `[PixelControl] [${this._pixelID}] ${event} custom event sent (test code: ${this._testEventCode}).` - ); - } else { - log.info( - `[PixelControl] [${this._pixelID}] ${event} custom event not sent in development mode without a test event code.` - ); - } + window.fbq('trackSingleCustom', this._pixelID, event, params, { + eventID, + test_event_code: this._testEventCode + }); + log.debug( + `[PixelControl] [${this._pixelID}] ${event} custom event sent with event ID ${eventID} ${dev && ` (test code: ${this._testEventCode})`}.` + ); } }