diff --git a/src/lib/conversion/client.ts b/src/lib/conversion/client.ts index 6a5cc48..cfbf5d8 100644 --- a/src/lib/conversion/client.ts +++ b/src/lib/conversion/client.ts @@ -1,3 +1,4 @@ +import { getFbpFbc } from '../metapixel/fbc.ts'; import type { TrackingManager } from '$lib/tracking.svelte'; import type { ConversionErrorResponseBody, @@ -42,13 +43,12 @@ export class ConversionClient { } ): Promise { // Extract user data - const fbp = await cookieStore.get('_fbp'); - const fbc = await cookieStore.get('_fbc'); + const { fbp, fbc } = getFbpFbc(); const user: ConversionUserData = { ...options.user, - fbp: fbp?.value, - fbc: fbc?.value + fbp, + fbc }; // Get event source URL & extract UTM params if present diff --git a/src/lib/metapixel/MetaPixel.svelte b/src/lib/metapixel/MetaPixel.svelte index b27bb9a..027d66b 100644 --- a/src/lib/metapixel/MetaPixel.svelte +++ b/src/lib/metapixel/MetaPixel.svelte @@ -11,6 +11,7 @@ PixelControl interface. import type { TrackingManager } from '../tracking.svelte.ts'; import { onNavigate } from '$app/navigation'; import { PixelControl, type PixelControlOptions } from './pixel-control.ts'; + import { ensureFbc, type EnsureFbcOptions } from './fbc.ts'; interface Props { /** @@ -37,6 +38,11 @@ PixelControl interface. let pixel = $state(null); + const ensureFbcOptions: EnsureFbcOptions = $derived({ + sameSite: 'Lax', + pixelLoaded: PixelControl.baseLoaded && !PixelControl.baseFailed + }); + onMount(() => { if (!trackingManager) { throw new Error('MetaPixel component requires a TrackingManager to manage consent.'); @@ -47,6 +53,7 @@ PixelControl interface. trackingManager.runWithConsent(() => { if (autoPageView && pixel) { pixel.pageView(); + ensureFbc(ensureFbcOptions); } }); }); @@ -55,6 +62,7 @@ PixelControl interface. trackingManager?.runWithConsent(() => { if (autoPageView && pixel) { pixel.pageView(); + ensureFbc(ensureFbcOptions); } }); }); diff --git a/src/lib/metapixel/fbc.ts b/src/lib/metapixel/fbc.ts new file mode 100644 index 0000000..7786c18 --- /dev/null +++ b/src/lib/metapixel/fbc.ts @@ -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 = {} +) { + 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.." + 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 it’s 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 }; +} diff --git a/src/lib/metapixel/index.ts b/src/lib/metapixel/index.ts index 654cbaa..ed79cbc 100644 --- a/src/lib/metapixel/index.ts +++ b/src/lib/metapixel/index.ts @@ -1,2 +1,4 @@ export { default as MetaPixel } from './MetaPixel.svelte'; export { PixelControl, type PixelControlOptions } from './pixel-control.ts'; +export * from './fbc.ts'; +export * from './pixel-control.ts'; diff --git a/src/lib/metapixel/pixel-control.ts b/src/lib/metapixel/pixel-control.ts index fdf7e18..7017c01 100644 --- a/src/lib/metapixel/pixel-control.ts +++ b/src/lib/metapixel/pixel-control.ts @@ -68,7 +68,8 @@ export class PixelControl { private _trackingManager: MaybeGetter; 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 = {}; /** Indicates whether the Meta Pixel base script has been loaded. */ @@ -76,6 +77,11 @@ export class PixelControl { 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 * allowing further operations. @@ -111,15 +117,18 @@ export class PixelControl { /** Loads the Meta Pixel base script. */ static async load() { - if (this._baseLoaded && !!window.fbq) return; + if (this._baseLoaded && !this.baseFailed && !!window.fbq) return; if (!window.fbq) { 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.'); }