pixel: add ensure fbc fallback
This commit is contained in:
@@ -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<ConversionResponseBody | ConversionErrorResponseBody> {
|
||||
// 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
|
||||
|
||||
@@ -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<PixelControl | null>(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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
88
src/lib/metapixel/fbc.ts
Normal file
88
src/lib/metapixel/fbc.ts
Normal 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 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 };
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -68,7 +68,8 @@ export class PixelControl {
|
||||
private _trackingManager: MaybeGetter<TrackingManager | 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> = {};
|
||||
|
||||
/** 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;
|
||||
}
|
||||
}
|
||||
log.debug('[PixelControl] Meta Pixel base script loaded.');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user