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 { 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
|
||||||
|
|||||||
@@ -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
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 { 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';
|
||||||
|
|||||||
@@ -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.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user