From ac3aabc1ed4d09d3afd752c031c9925d8f0ac79f Mon Sep 17 00:00:00 2001 From: Elijah Duffy Date: Wed, 24 Dec 2025 12:01:50 -0800 Subject: [PATCH] improve logs & capi event source URL handling --- src/lib/capi/event.ts | 57 ++++++++++++++++++++++- src/lib/capi/handle.ts | 4 +- src/lib/metapixel/pixel-control.ts | 72 +++++++++++++++++++----------- 3 files changed, 105 insertions(+), 28 deletions(-) diff --git a/src/lib/capi/event.ts b/src/lib/capi/event.ts index 962509d..fd8cfb6 100644 --- a/src/lib/capi/event.ts +++ b/src/lib/capi/event.ts @@ -1,6 +1,7 @@ import type { StandardEventName } from '../types/fbq.js'; import * as v from 'valibot'; import crypto from 'node:crypto'; +import log from 'loglevel'; const sha256 = (value: string): string => { return crypto.createHash('sha256').update(value).digest('hex'); @@ -202,7 +203,11 @@ export type CAPIEventOptions = { eventTime?: Date; userData: CAPICustomerInfoParams; customData?: CAPIStandardParams; - /** Required if actionSource is set to 'website' */ + /** + * Required by Meta if actionSource is set to 'website'. If not provided and + * actionSource is 'website', a warning will be emitted and the field may be + * filled by the server receiving the event, however, accuracy is not guaranteed. + */ eventSourceURL?: string; optOut?: boolean; eventID?: string; @@ -220,6 +225,12 @@ export class CAPIEvent { return this._params; } + /** Returns human-readable event name, optionally including event ID. */ + get readableName() { + if (!this._params) return 'Unknown Event'; + return `${this._params.event_name}${this._params.event_id ? ` (${this._params.event_id})` : ''}`; + } + private constructor() {} /** @@ -249,9 +260,28 @@ export class CAPIEvent { }; event._params = v.parse(MetaServerEventParamsSchema, event._params); + event.paramIntegrationCheck(); return event; } + /** + * Performs integration checks on the event parameters and logs warnings + * if potential issues are detected. + */ + private paramIntegrationCheck() { + if ( + this._params && + this._params.action_source === ActionSource.Website && + !this._params.event_source_url + ) { + console.warn( + `[CAPIEvent] Warning: ${this.readableName} ` + + `with actionSource 'website' is missing eventSourceURL. Provide eventSourceURL to improve ` + + `data quality and avoid blocks by Meta.` + ); + } + } + /** * Unmashals a JSON string or object into a CAPIEvent instance and parses * its parameters to match Meta's expected shape. @@ -265,6 +295,7 @@ export class CAPIEvent { const parsed = v.parse(MetaServerEventParamsSchema, obj); const event = new CAPIEvent(); event._params = parsed; + event.paramIntegrationCheck(); return event; } @@ -309,7 +340,8 @@ export class CAPIEvent { } /** - * Enriches the CAPIEvent with additional user data. + * Enriches the CAPIEvent with additional user data. Will not override + * existing user data fields if already set. * @param additionalData - Additional user data to merge. * @throws Will throw an error if the enriched data fails validation. */ @@ -326,4 +358,25 @@ export class CAPIEvent { // Re-validate after enrichment this._params = v.parse(MetaServerEventParamsSchema, this._params); } + + /** + * Enriches the CAPIEvent with an event source URL. Will not override + * existing event source URL if already set AND will only apply if the + * action source is 'website'. A message will be logged to warn the user + * that an implicit event source URL is being set. + * @param url - The event source URL to set. + */ + unsafeEventSourceURL(url?: string) { + if (!this._params) return; + if (!url) { + log.warn( + `[CAPIEvent] ${this.readableName} could not implicitly set eventSourceURL, event will be submitted without it.` + ); + return; + } + if (!this._params.event_source_url && this._params.action_source === ActionSource.Website) { + this._params.event_source_url = url; + log.warn(`[CAPIEvent] ${this.readableName} implicitly setting eventSourceURL to ${url}`); + } + } } diff --git a/src/lib/capi/handle.ts b/src/lib/capi/handle.ts index 2d8c4a6..e833c36 100644 --- a/src/lib/capi/handle.ts +++ b/src/lib/capi/handle.ts @@ -45,6 +45,7 @@ export const createCAPIHandler: (connector: CAPIConnector) => RequestHandler = ( const ip = getRequestIP(request, getClientAddress); const ua = request.headers.get('user-agent') ?? undefined; const { fbp, fbc } = getFbpFbcFromCookies(cookies); + const eventSourceURL = request.headers.get('referer') ?? undefined; const enrichedUserData: Partial = { clientIP: ip, @@ -53,10 +54,11 @@ export const createCAPIHandler: (connector: CAPIConnector) => RequestHandler = ( fbc }; - // Enrich each event's user data + // Enrich each event's user data & event source URL const events: CAPIEvent[] = parsed.events.map((eventParams) => { const event = CAPIEvent.fromObject(eventParams); event.enrichUserData(enrichedUserData); + event.unsafeEventSourceURL(eventSourceURL); return event; }); diff --git a/src/lib/metapixel/pixel-control.ts b/src/lib/metapixel/pixel-control.ts index d90c63e..95e361e 100644 --- a/src/lib/metapixel/pixel-control.ts +++ b/src/lib/metapixel/pixel-control.ts @@ -12,7 +12,12 @@ import { resolveGetter, type MaybeGetter } from '../util/getter.ts'; import log from 'loglevel'; import { dev } from '$app/environment'; import { CAPIClient } from '../capi/client.ts'; -import { ActionSource, CAPIEvent, type CAPIStandardParams } from '../capi/event.ts'; +import { + ActionSource, + CAPIEvent, + type CAPIEventOptions, + type CAPIStandardParams +} from '../capi/event.ts'; const pixelParamsToCustomData = (params: CommonParams & CustomParams): CAPIStandardParams => { const customData: CAPIStandardParams = {}; @@ -101,7 +106,7 @@ export class PixelControl { this._conversionClient = new CAPIClient(options.conversionHref, resolvedTrackingManager); } else if (options?.conversionHref) { log.warn( - `[PixelControl] Conversion Client ${options.conversionHref} for Meta Pixel [${this._pixelID}] not initialized, TrackingManager is required for user consent.` + `${this.logPrefix} CAPI Client ${options.conversionHref} available but not initialized, TrackingManager is required for user consent.` ); } } @@ -150,7 +155,7 @@ export class PixelControl { // Check for existing PixelControl instance if (this._registeredPixels[pixelID]) { log.warn( - `[PixelControl] Instance for Meta Pixel ID: ${pixelID} already exists. Returning existing instance.` + `${this._registeredPixels[pixelID].logPrefix} Instance already exists. Returning existing instance.` ); return this._registeredPixels[pixelID]; } @@ -161,7 +166,7 @@ export class PixelControl { // Fire initialization window.fbq('init', pixel._pixelID, options?.advancedMatching, options?.initOptions); - log.debug(`[PixelControl] [${pixel._pixelID}] initialized.`); + log.debug(`${pixel.logPrefix} initialized.`); return pixel; } @@ -196,12 +201,17 @@ export class PixelControl { private devModeWarn() { if (dev && !this._testEventCode) { log.warn( - `[PixelControl] [${this._pixelID}] Sending events in dev mode without a test event code. ` + + `${this.logPrefix} Sending events in dev mode without a test event code. ` + 'Consider providing a test event code to avoid affecting real data.' ); } } + /** Returns the log prefix including the pixel ID */ + private get logPrefix(): string { + return `[PixelControl] [${this._pixelID}]`; + } + /** * Shorthand utility to send a PageView event * @param disableCAPI If true, disables sending this event to the Conversion API @@ -230,23 +240,28 @@ export class PixelControl { eventID = crypto.randomUUID(); } + const opts: CAPIEventOptions = { + eventName: event, + eventID: eventID, + actionSource: ActionSource.Website, + eventSourceURL: window.location.href, + eventTime: new Date(), + userData: { + clientUserAgent: navigator.userAgent + }, + customData: params ? pixelParamsToCustomData(params) : undefined + }; + const ev = CAPIEvent.fromOpts(opts); + + log.debug( + `${this.logPrefix} ${ev.readableName} forwarding to CAPI with body: ${JSON.stringify(ev.params, null, 2)}` + ); + this._conversionClient - .trackEvent( - CAPIEvent.fromOpts({ - eventName: event, - eventID: eventID, - actionSource: ActionSource.Website, - eventSourceURL: window.location.href, - eventTime: new Date(), - userData: { - clientUserAgent: navigator.userAgent - }, - customData: params ? pixelParamsToCustomData(params) : undefined - }) - ) + .trackEvent(ev) .then((response) => { log.debug( - `[PixelControl] [${this._pixelID}] ${event} event forwarded to Conversion API with Event ID: ${eventID}, Response: ${JSON.stringify( + `${this.logPrefix} ${ev.readableName} forwarded to CAPI, response: ${JSON.stringify( response, null, 2 @@ -254,15 +269,22 @@ export class PixelControl { ); }) .catch((error) => { - log.error( - `[PixelControl] [${this._pixelID}] Failed to forward ${event} event to Conversion API with Event ID: ${eventID}`, - error - ); + log.error(`${this.logPrefix} ${ev.readableName} failed to forward to CAPI. Error: `, error); }); return eventID; } + private getEventReadableName( + event: StandardEventName | string, + eventID?: string, + testCode?: string + ): string { + return ( + event + (eventID ? ` (${eventID})` : '') + (testCode ? ` [test_event_code: ${testCode}]` : '') + ); + } + /** * Tracks a standard event for this pixel (uses `trackSingle` under the hood) * @param event Standard event name @@ -291,7 +313,7 @@ export class PixelControl { test_event_code: this._testEventCode }); log.debug( - `[PixelControl] [${this._pixelID}] ${event} event sent with event ID ${eventID} ${dev && ` (test code: ${this._testEventCode})`}.` + `${this.logPrefix} ${this.getEventReadableName(event, eventID, this._testEventCode)} sent.` ); } @@ -323,7 +345,7 @@ export class PixelControl { test_event_code: this._testEventCode }); log.debug( - `[PixelControl] [${this._pixelID}] ${event} custom event sent with event ID ${eventID} ${dev && ` (test code: ${this._testEventCode})`}.` + `${this.logPrefix} ${this.getEventReadableName(event, eventID, this._testEventCode)} sent.` ); } }