Files
spectator/src/lib/util/meta-pixel-loader.ts
Elijah Duffy 9a72280737 meta pixel: default to disablePushState = true
Breaks SvelteKit SPA which doesn't allow use of history API, requiring
its own wrapper to be used instead.
2025-12-17 22:19:05 -08:00

121 lines
3.9 KiB
TypeScript

import { browser } from '$app/environment';
import log from 'loglevel';
const SCRIPT_SRC = 'https://connect.facebook.net/en_US/fbevents.js';
type QueuedFBQ = ((...args: unknown[]) => void) & {
queue?: unknown[][];
callMethod?: (...args: unknown[]) => void;
loaded?: boolean;
version?: string;
push?: unknown;
disablePushState?: boolean;
allowDuplicatePageViews?: boolean;
};
/**
* Loads the Meta Pixel script and configures the `fbq` function to queue
* commands until the script is fully loaded. You may optionally await the
* returned Promise to ensure the script has loaded before proceeding.
*
* Options:
* - `disablePushState` (default: true) — when true, sets
* `window.fbq.disablePushState = true` before the pixel script loads so the
* pixel does not auto-listen to `history.pushState`/`popstate` (recommended
* for SPA frameworks like Svelte).
* - `allowDuplicatePageViews` (default: false) — when true, sets
* `window.fbq.allowDuplicatePageViews = true` on the stub.
*/
export const loadMetaPixel = (opts?: {
disablePushState?: boolean;
allowDuplicatePageViews?: boolean;
}): Promise<void> => {
// Make sure we're using the browser
if (!browser || !window) {
return Promise.reject(new Error(`Not in browser, can't access window`));
}
// Default behavior: disable pushState handling since Svelte apps manage
// navigation themselves and Meta's auto-patching of history APIs can
// cause duplicate/incorrect pageview events. Consumers can pass
// `opts.disablePushState = false` to opt out.
const disablePushState = opts?.disablePushState ?? true;
const allowDuplicatePageViews = opts?.allowDuplicatePageViews ?? false;
// If fbq is already defined, resolve immediately
const existing = window.fbq as QueuedFBQ | undefined;
if (existing) {
// If the existing stub is present but hasn't set these flags yet, set
// them now so the loaded library (if it inspects them) sees intended
// behavior. Setting these is a no-op if initialization already
// completed.
if (disablePushState) existing.disablePushState = true;
if (allowDuplicatePageViews) existing.allowDuplicatePageViews = true;
const existingScript = getExistingScript();
if (existingScript) {
return new Promise((resolve, reject) => {
attachToScript(existingScript, resolve, reject);
});
}
log.debug(
'Meta Pixel fbq already present, skipping injection',
existing.version,
existing.queue
);
return Promise.resolve();
}
// Configure fbq to queue commands until Meta takes over
const q = function (...args: unknown[]) {
if (q.callMethod) {
q.callMethod(...args);
} else {
if (!q.queue) q.queue = [];
q.queue.push(args);
}
} as QueuedFBQ;
q.queue = [];
q.push = q;
q.loaded = true;
q.version = '2.0';
// set control flags on the stub before the meta script runs
if (disablePushState) q.disablePushState = true;
if (allowDuplicatePageViews) q.allowDuplicatePageViews = true;
window.fbq = q;
window._fbq = q;
return new Promise((resolve, reject) => {
// Avoid adding the same script twice
const existingScript = getExistingScript();
if (existingScript) {
attachToScript(existingScript, resolve, reject);
log.debug('Meta Pixel script already present, waiting for load');
return;
}
// Otherwise, create the script element
const script = document.createElement('script');
script.src = SCRIPT_SRC;
script.async = true;
attachToScript(script, resolve, reject);
document.head.appendChild(script);
log.debug('Meta Pixel script added to document');
});
};
const getExistingScript = (): HTMLScriptElement | null => {
return document.querySelector(
`script[src*="connect.facebook.net"][src*="fbevents.js"]`
) as HTMLScriptElement | null;
};
const attachToScript = (
el: HTMLScriptElement,
resolve: () => void,
reject: (err: Error) => void
) => {
el.addEventListener('load', () => resolve());
el.addEventListener('error', () => reject(new Error('Failed to load Meta Pixel script')));
};