7 Commits

Author SHA1 Message Date
Elijah Duffy
a9a9f5ed30 0.0.6 2025-12-18 22:08:31 -08:00
Elijah Duffy
0e1a449cb6 conversion: more robust fbp, fbc with header-based attempt 2025-12-18 22:06:23 -08:00
Elijah Duffy
764da5db2e conversion: make event ID optional 2025-12-18 22:04:32 -08:00
Elijah Duffy
6692338b83 pixel: allow disabling conversion API forwarding 2025-12-18 21:27:44 -08:00
Elijah Duffy
3561012fb9 pixel: fix $state usage, fix load ordering 2025-12-18 21:22:07 -08:00
Elijah Duffy
b26f6160f8 set log level to warn in production 2025-12-18 21:21:46 -08:00
Elijah Duffy
1bb202ffa5 pixel: rename pixel-control to svelte file 2025-12-18 19:24:28 -08:00
8 changed files with 50 additions and 41 deletions

View File

@@ -4,7 +4,7 @@
"type": "git", "type": "git",
"url": "https://gitea.auvem.com/svelte-toolkit/spectator.git" "url": "https://gitea.auvem.com/svelte-toolkit/spectator.git"
}, },
"version": "0.0.5", "version": "0.0.6",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",

View File

@@ -113,11 +113,11 @@ export class ConversionControl {
const event = new ServerEvent() const event = new ServerEvent()
.setEventName(eventName) .setEventName(eventName)
.setEventTime(Math.floor(Date.now() / 1000)) .setEventTime(Math.floor(Date.now() / 1000))
.setEventId(details.eventID)
.setUserData(buildUserData(details.userData)) .setUserData(buildUserData(details.userData))
.setActionSource(details.actionSource); .setActionSource(details.actionSource);
if (details?.eventSourceURL) event.setEventSourceUrl(details.eventSourceURL); if (details.eventID) event.setEventId(details.eventID);
if (details.eventSourceURL) event.setEventSourceUrl(details.eventSourceURL);
if (params) { if (params) {
const customData = buildCustomData(params); const customData = buildCustomData(params);
event.setCustomData(customData); event.setCustomData(customData);

View File

@@ -8,8 +8,9 @@ import type {
} from '$lib/types/conversion.js'; } from '$lib/types/conversion.js';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import { getFbpFbcFromCookies } from '../metapixel/fbc.ts';
const getEventIP = (request: Request, getClientAddress: () => string) => { export const getRequestIP = (request: Request, getClientAddress: () => string) => {
return ( return (
request.headers.get('x-forwarded-for') || request.headers.get('x-forwarded-for') ||
request.headers.get('cf-connecting-ip') || request.headers.get('cf-connecting-ip') ||
@@ -26,18 +27,21 @@ const getEventIP = (request: Request, getClientAddress: () => string) => {
export const createConversionRequestHandler: (control: ConversionControl) => RequestHandler = ( export const createConversionRequestHandler: (control: ConversionControl) => RequestHandler = (
control control
) => { ) => {
const handle: RequestHandler = async ({ request, getClientAddress }) => { const handle: RequestHandler = async ({ request, getClientAddress, cookies }) => {
try { try {
const body = (await request.json()) as ConversionRequestBody; const body = (await request.json()) as ConversionRequestBody;
// Build user data with IP and user agent // Build user data with IP and user agent
const ip = getEventIP(request, getClientAddress); const ip = getRequestIP(request, getClientAddress);
const ua = request.headers.get('user-agent'); const ua = request.headers.get('user-agent');
const { fbp, fbc } = getFbpFbcFromCookies(cookies);
const userData: ConversionUserData = { const userData: ConversionUserData = {
...body.user, ...body.user,
ip, ip,
ua: ua ?? body.user?.ua ?? undefined ua: ua ?? body.user?.ua ?? undefined,
fbc: body.user?.fbc ?? fbc,
fbp: body.user?.fbp ?? fbp
}; };
// Build custom data with UTM params if applicable // Build custom data with UTM params if applicable

View File

@@ -13,4 +13,6 @@ export * from './conversion/index.ts';
// set log level to debug if we're in dev mode // set log level to debug if we're in dev mode
if (dev) { if (dev) {
log.setLevel('debug'); log.setLevel('debug');
} else {
log.setLevel('warn');
} }

View File

@@ -38,22 +38,20 @@ PixelControl interface.
let pixel = $state<PixelControl | null>(null); let pixel = $state<PixelControl | null>(null);
const ensureFbcOptions: EnsureFbcOptions = $derived({ const fbcOptions: EnsureFbcOptions = {
sameSite: 'Lax', sameSite: 'Lax'
pixelLoaded: PixelControl.baseLoaded && !PixelControl.baseFailed };
});
onMount(() => { onMount(() => {
if (!trackingManager) { if (!trackingManager) {
throw new Error('MetaPixel component requires a TrackingManager to manage consent.'); throw new Error('MetaPixel component requires a TrackingManager to manage consent.');
} }
PixelControl.load();
pixel = PixelControl.initialize(trackingManager, pixelID, pixelOptions); pixel = PixelControl.initialize(trackingManager, pixelID, pixelOptions);
trackingManager.runWithConsent(() => { trackingManager.runWithConsent(() => {
if (autoPageView && pixel) { if (autoPageView && pixel) {
pixel.pageView(); pixel.pageView();
ensureFbc(ensureFbcOptions); ensureFbc(fbcOptions);
} }
}); });
}); });
@@ -62,7 +60,7 @@ PixelControl interface.
trackingManager?.runWithConsent(() => { trackingManager?.runWithConsent(() => {
if (autoPageView && pixel) { if (autoPageView && pixel) {
pixel.pageView(); pixel.pageView();
ensureFbc(ensureFbcOptions); ensureFbc(fbcOptions);
} }
}); });
}); });

View File

@@ -1,3 +1,4 @@
import type { Cookies } from '@sveltejs/kit';
import log from 'loglevel'; import log from 'loglevel';
export type EnsureFbcOptions = { export type EnsureFbcOptions = {
@@ -79,10 +80,19 @@ export function ensureFbc(options: EnsureFbcOptions = {}) {
} }
/** /**
* Helper to read both _fbp and _fbc for your CAPI payload. * Helper to read both _fbp and _fbc for your CAPI payload from browser cookies.
*/ */
export function getFbpFbc(): { fbp?: string; fbc?: string } { export function getFbpFbc(): { fbp?: string; fbc?: string } {
const fbp = getCookie('_fbp'); const fbp = getCookie('_fbp');
const fbc = getCookie('_fbc'); const fbc = getCookie('_fbc');
return { fbp, fbc }; return { fbp, fbc };
} }
/**
* Helper to read both _fbp and _fbc for your CAPI payload from cookies object.
*/
export function getFbpFbcFromCookies(cookies: Cookies): { fbp?: string; fbc?: string } {
const fbp = cookies.get('_fbp') || undefined;
const fbc = cookies.get('_fbc') || undefined;
return { fbp, fbc };
}

View File

@@ -68,8 +68,7 @@ export class PixelControl {
private _trackingManager: MaybeGetter<TrackingManager | undefined>; private _trackingManager: MaybeGetter<TrackingManager | undefined>;
private _conversionClient?: ConversionClient = undefined; private _conversionClient?: ConversionClient = undefined;
private static _baseLoaded: boolean = $state(false); private static _baseLoaded: boolean = false;
private static _baseFailed: boolean = $state(false);
private static _registeredPixels: Record<string, PixelControl> = {}; private static _registeredPixels: Record<string, PixelControl> = {};
/** Indicates whether the Meta Pixel base script has been loaded. */ /** Indicates whether the Meta Pixel base script has been loaded. */
@@ -77,11 +76,6 @@ export class PixelControl {
return this._baseLoaded; return this._baseLoaded;
} }
/** Indicates whether the Meta Pixel base script has failed to load. */
static get baseFailed(): boolean {
return this._baseFailed;
}
/** /**
* Ensures that the Meta Pixel base has been loaded before * Ensures that the Meta Pixel base has been loaded before
* allowing further operations. * allowing further operations.
@@ -116,20 +110,11 @@ export class PixelControl {
} }
/** Loads the Meta Pixel base script. */ /** Loads the Meta Pixel base script. */
static async load() { static load() {
if (this._baseLoaded && !this.baseFailed && !!window.fbq) return; if (this._baseLoaded && !!window.fbq) return;
if (!window.fbq) { loadMetaPixel(); // Load the Meta Pixel script
try {
await loadMetaPixel(); // Load the Meta Pixel script
} catch (e) {
log.warn('[PixelControl] Failed to load Meta Pixel script, all events will be queued.', e);
this._baseFailed = true;
return;
} finally {
this._baseLoaded = true; this._baseLoaded = true;
} log.debug('[PixelControl] Meta Pixel base script loaded.', this._baseLoaded);
}
log.debug('[PixelControl] Meta Pixel base script loaded.');
} }
/** Tells the Meta pixel that the user has given consent for tracking. */ /** Tells the Meta pixel that the user has given consent for tracking. */
@@ -212,14 +197,15 @@ export class PixelControl {
/** /**
* Sends a PageView event * Sends a PageView event
* @param disableCAPI If true, disables sending this event to the Conversion API
* @throws Error if the Meta Pixel is not initialized. * @throws Error if the Meta Pixel is not initialized.
*/ */
pageView() { pageView(disableCAPI: boolean = false) {
if (!this.consentGuard()) return; if (!this.consentGuard()) return;
let eventID: string | undefined = undefined; let eventID: string | undefined = undefined;
// Optionally, send to conversion API endpoint if configured // Optionally, send to conversion API endpoint if configured
if (this._conversionClient) { if (this._conversionClient && !disableCAPI) {
eventID = crypto.randomUUID(); eventID = crypto.randomUUID();
this._conversionClient this._conversionClient
.trackEvent('PageView', { eventID }) .trackEvent('PageView', { eventID })
@@ -256,13 +242,22 @@ export class PixelControl {
/** /**
* Tracks a standard event for this pixel (uses `trackSingle` under the hood) * Tracks a standard event for this pixel (uses `trackSingle` under the hood)
* @param event Standard event name
* @param params Event parameters
* @param eventID Optional event ID for deduplication with Conversion API
* @param disableCAPI If true, disables sending this event to the Conversion API
* @throws Error if the Meta Pixel is not initialized. * @throws Error if the Meta Pixel is not initialized.
*/ */
track<K extends StandardEventName>(event: K, params?: EventParamsByName[K], eventID?: string) { track<K extends StandardEventName>(
event: K,
params?: EventParamsByName[K],
eventID?: string,
disableCAPI: boolean = false
) {
if (!this.consentGuard()) return; if (!this.consentGuard()) return;
// Optionally, send to conversion API endpoint if configured // Optionally, send to conversion API endpoint if configured
if (this._conversionClient) { if (this._conversionClient && !disableCAPI) {
eventID = eventID ?? crypto.randomUUID(); eventID = eventID ?? crypto.randomUUID();
this._conversionClient this._conversionClient
.trackEvent(event, { eventID: eventID, customData: pixelParamsToCustomData(params ?? {}) }) .trackEvent(event, { eventID: eventID, customData: pixelParamsToCustomData(params ?? {}) })

View File

@@ -51,7 +51,7 @@ export type ConversionEventParams = {
* Parameters for sending a conversion event to Meta Pixel. * Parameters for sending a conversion event to Meta Pixel.
*/ */
export type ConversionEventDetails = { export type ConversionEventDetails = {
eventID: string; eventID?: string;
actionSource: 'website' | 'app' | 'offline' | 'other'; actionSource: 'website' | 'app' | 'offline' | 'other';
userData: ConversionUserData; userData: ConversionUserData;
eventSourceURL?: string; eventSourceURL?: string;