pixel: add ensure fbc fallback

This commit is contained in:
Elijah Duffy
2025-12-18 19:22:54 -08:00
parent f2d389ee64
commit 0cd3f10da6
5 changed files with 114 additions and 7 deletions

View File

@@ -1,3 +1,4 @@
import { getFbpFbc } from '../metapixel/fbc.ts';
import type { TrackingManager } from '$lib/tracking.svelte'; import type { TrackingManager } from '$lib/tracking.svelte';
import type { import type {
ConversionErrorResponseBody, ConversionErrorResponseBody,
@@ -42,13 +43,12 @@ export class ConversionClient {
} }
): Promise<ConversionResponseBody | ConversionErrorResponseBody> { ): Promise<ConversionResponseBody | ConversionErrorResponseBody> {
// Extract user data // Extract user data
const fbp = await cookieStore.get('_fbp'); const { fbp, fbc } = getFbpFbc();
const fbc = await cookieStore.get('_fbc');
const user: ConversionUserData = { const user: ConversionUserData = {
...options.user, ...options.user,
fbp: fbp?.value, fbp,
fbc: fbc?.value fbc
}; };
// Get event source URL & extract UTM params if present // Get event source URL & extract UTM params if present

View File

@@ -11,6 +11,7 @@ PixelControl interface.
import type { TrackingManager } from '../tracking.svelte.ts'; import type { TrackingManager } from '../tracking.svelte.ts';
import { onNavigate } from '$app/navigation'; import { onNavigate } from '$app/navigation';
import { PixelControl, type PixelControlOptions } from './pixel-control.ts'; import { PixelControl, type PixelControlOptions } from './pixel-control.ts';
import { ensureFbc, type EnsureFbcOptions } from './fbc.ts';
interface Props { interface Props {
/** /**
@@ -37,6 +38,11 @@ PixelControl interface.
let pixel = $state<PixelControl | null>(null); let pixel = $state<PixelControl | null>(null);
const ensureFbcOptions: EnsureFbcOptions = $derived({
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.');
@@ -47,6 +53,7 @@ PixelControl interface.
trackingManager.runWithConsent(() => { trackingManager.runWithConsent(() => {
if (autoPageView && pixel) { if (autoPageView && pixel) {
pixel.pageView(); pixel.pageView();
ensureFbc(ensureFbcOptions);
} }
}); });
}); });
@@ -55,6 +62,7 @@ PixelControl interface.
trackingManager?.runWithConsent(() => { trackingManager?.runWithConsent(() => {
if (autoPageView && pixel) { if (autoPageView && pixel) {
pixel.pageView(); pixel.pageView();
ensureFbc(ensureFbcOptions);
} }
}); });
}); });

88
src/lib/metapixel/fbc.ts Normal file
View File

@@ -0,0 +1,88 @@
import log from 'loglevel';
export type EnsureFbcOptions = {
days?: number; // cookie lifetime, default 180
domain?: string; // optional cookie domain
sameSite?: 'Lax' | 'Strict' | 'None'; // default Lax
secure?: boolean; // default inferred from location.protocol === 'https:'
pixelLoaded?: boolean; // if true, skip manual set and let Pixel handle it
};
function getParam(name: string): string | undefined {
const params = new URLSearchParams(location.search);
const v = params.get(name);
return v || undefined;
}
function getCookie(name: string): string | undefined {
return document.cookie
.split('; ')
.find((c) => c.startsWith(name + '='))
?.split('=')[1];
}
function setCookie(
name: string,
value: string,
{
days = 180,
domain,
sameSite = 'Lax',
secure
}: Pick<EnsureFbcOptions, 'days' | 'domain' | 'sameSite' | 'secure'> = {}
) {
const d = new Date();
d.setTime(d.getTime() + days * 864e5);
const parts = [
`${name}=${encodeURIComponent(value)}`,
`expires=${d.toUTCString()}`,
'path=/',
`SameSite=${sameSite}`
];
if (domain) parts.push(`domain=${domain}`);
const isSecure = secure ?? location.protocol === 'https:';
if (isSecure) parts.push('Secure');
document.cookie = parts.join('; ');
}
function isValidFbc(value: string | undefined): boolean {
if (!value) return false;
// Expect "fb.1.<unix>.<fbclid>"
if (!value.startsWith('fb.1.')) return false;
const parts = value.split('.');
return parts.length >= 4 && /^\d+$/.test(parts[2]) && parts[3].length > 0;
}
/**
* Ensure _fbc cookie exists when landing URL contains fbclid.
* Call after consent. If pixelLoaded is true, it skips manual setting.
*/
export function ensureFbc(options: EnsureFbcOptions = {}) {
try {
const { pixelLoaded = false, ...cookieOpts } = options;
if (pixelLoaded) throw new Error('Pixel loaded, skipping manual _fbc set'); // Let the Pixel set _fbc if its active
const fbclid = getParam('fbclid');
if (!fbclid) throw new Error('No fbclid param present');
const existing = getCookie('_fbc');
if (isValidFbc(existing)) throw new Error('_fbc cookie already present and valid');
const ts = Math.floor(Date.now() / 1000);
const fbc = `fb.1.${ts}.${fbclid}`;
setCookie('_fbc', fbc, cookieOpts);
log.debug('[ensureFbc] Set _fbc cookie:', fbc);
} catch (e) {
log.debug('[ensureFbc]', (e as Error).message);
}
}
/**
* Helper to read both _fbp and _fbc for your CAPI payload.
*/
export function getFbpFbc(): { fbp?: string; fbc?: string } {
const fbp = getCookie('_fbp');
const fbc = getCookie('_fbc');
return { fbp, fbc };
}

View File

@@ -1,2 +1,4 @@
export { default as MetaPixel } from './MetaPixel.svelte'; export { default as MetaPixel } from './MetaPixel.svelte';
export { PixelControl, type PixelControlOptions } from './pixel-control.ts'; export { PixelControl, type PixelControlOptions } from './pixel-control.ts';
export * from './fbc.ts';
export * from './pixel-control.ts';

View File

@@ -68,7 +68,8 @@ 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 = false; private static _baseLoaded: boolean = $state(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. */
@@ -76,6 +77,11 @@ 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.
@@ -111,15 +117,18 @@ export class PixelControl {
/** Loads the Meta Pixel base script. */ /** Loads the Meta Pixel base script. */
static async load() { static async load() {
if (this._baseLoaded && !!window.fbq) return; if (this._baseLoaded && !this.baseFailed && !!window.fbq) return;
if (!window.fbq) { if (!window.fbq) {
try { try {
await loadMetaPixel(); // Load the Meta Pixel script await loadMetaPixel(); // Load the Meta Pixel script
} catch (e) { } catch (e) {
log.warn('[PixelControl] Failed to load Meta Pixel script, all events will be queued.', 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.'); log.debug('[PixelControl] Meta Pixel base script loaded.');
} }