add meta pixel integration

This commit is contained in:
Elijah Duffy
2025-12-16 17:21:52 -08:00
parent 1ead8431a7
commit 49c74410a9
4 changed files with 624 additions and 0 deletions

190
src/lib/MetaPixel.svelte Normal file
View File

@@ -0,0 +1,190 @@
<script lang="ts" module>
export class PixelControl {
private _pixelID: string;
private _testEventCode?: string = undefined;
private _trackingManager: MaybeGetter<TrackingManager | undefined>;
private static _baseLoaded: boolean = false;
static get baseLoaded(): boolean {
return this._baseLoaded;
}
/**
* Ensures that the Meta Pixel base has been loaded before
* allowing further operations.
* @throws Error if the Meta Pixel API is not loaded.
*/
static loadGuard(): void {
if (!this._baseLoaded || !window._fbq) {
throw new Error('Meta Pixel API has not been loaded. Call PixelControl.load() first.');
}
}
private constructor(
trackingManager: MaybeGetter<TrackingManager | undefined>,
pixelID: string,
testEventCode?: string
) {
this._trackingManager = trackingManager;
this._pixelID = pixelID;
this._testEventCode = testEventCode;
}
/** Loads the Meta Pixel base script. */
static load() {
if (this._baseLoaded && window._fbq) return;
if (!window._fbq) {
PixelControl.revokeConsent(); // Initialize without consent
loadMetaPixel(); // Load the Meta Pixel script
}
this._baseLoaded = true;
}
/** Tells the Meta pixel that the user has given consent for tracking. */
static grantConsent() {
this.loadGuard();
window.fbq?.('consent', 'grant');
}
/** Tells the Meta pixel that the user has revoked consent for tracking. */
static revokeConsent() {
this.loadGuard();
window.fbq?.('consent', 'revoke');
}
/**
* Returns a PixelControl instance for the given Meta Pixel ID. If
* the base Meta Pixel script has not been loaded yet, it will be
* loaded automatically. Optionally sets a test event code for the Pixel.
* @param pixelID Meta Pixel ID
* @param testEventCode Optional test event code
* @returns PixelControl instance
*/
static for(
trackingManager: MaybeGetter<TrackingManager | undefined>,
pixelID: string,
options?: {
testEventCode?: string;
advancedMatching?: AdvancedMatching;
initOptions?: InitOptions;
}
): PixelControl {
PixelControl.load();
window.fbq('init', pixelID);
return new PixelControl(trackingManager, pixelID, options?.testEventCode);
}
/**
* Checks if the Meta Pixel has consent to track user data
* and if the Pixel has been loaded.
* @returns true if tracking is allowed, false otherwise.
* @throws Error if the Meta Pixel is not loaded.
*/
consentGuard(): boolean {
PixelControl.loadGuard();
const trackingManager = resolveGetter(this._trackingManager);
return trackingManager?.haveUserConsent() ?? false;
}
/**
* Sends a PageView event
* @throws Error if the Meta Pixel is not initialized.
*/
pageView() {
if (!this.consentGuard()) return;
window.fbq('track', 'PageView', undefined, { test_event_code: this._testEventCode });
}
/**
* Tracks a standard event for this pixel (uses `trackSingle` under the hood)
* @throws Error if the Meta Pixel is not initialized.
*/
track<K extends StandardEventName>(event: K, params?: EventParamsByName[K], eventID?: string) {
if (!this.consentGuard()) return;
window.fbq('trackSingle', this._pixelID, event, params, {
eventID,
test_event_code: this._testEventCode
});
}
/**
* Tracks a custom event for this pixel (uses `trackSingleCustom` under the hood)
* @throws Error if the Meta Pixel is not initialized.
*/
trackCustom(event: string, params?: CommonParams & CustomParams, eventID?: string) {
if (!this.consentGuard()) return;
window.fbq('trackSingleCustom', this._pixelID, event, params, {
eventID,
test_event_code: this._testEventCode
});
}
}
</script>
<script lang="ts">
import { onMount } from 'svelte';
import type { TrackingManager } from './tracking.svelte.ts';
import type {
EventParamsByName,
StandardEventName,
CommonParams,
CustomParams,
AdvancedMatching,
InitOptions
} from './types/fbq.js';
import { loadMetaPixel } from './util/meta-pixel-loader.ts';
import { onNavigate } from '$app/navigation';
import { resolveGetter, type MaybeGetter } from './util/getter.ts';
interface Props {
/** Meta Pixel ID */
pixelID: string;
/**
* If a test event code is available, events fired will always have this
* code attached to prevent them from polluting real analytics data.
*/
testEventCode?: string;
/**
* Controls whether page views are automatically tracked by this
* component (default: true).
*/
autoPageView?: boolean;
/**
* Tracking manager to handle user consent for tracking. If omitted
* tracking is disabled by default until consent is granted via
* PixelControl.grantConsent().
*/
trackingManager?: TrackingManager;
}
let { pixelID, testEventCode, autoPageView = true, trackingManager }: Props = $props();
let pixel = $state<PixelControl | null>(null);
onMount(() => {
if (!trackingManager) {
throw new Error('MetaPixel component requires a TrackingManager to manage consent.');
}
PixelControl.load();
pixel = PixelControl.for(trackingManager, pixelID, { testEventCode });
trackingManager.runWithConsent(() => {
if (autoPageView && pixel) {
pixel.pageView();
}
});
});
onNavigate(() => {
trackingManager?.runWithConsent(() => {
if (autoPageView && pixel) {
pixel.pageView();
}
});
});
</script>

351
src/lib/types/fbq.d.ts vendored Normal file
View File

@@ -0,0 +1,351 @@
/**
* Meta Pixel (fbq) Type Definitions
*
* This file contains strongly-typed definitions for the Meta Pixel global
* (`fbq`) and its standard event parameter shapes. Exported types cover
* per-event params (e.g., `PurchaseParams`), the `FBQ` callable interface,
* `AdvancedMatching` for `fbq('init', ...)`, and related helper types.
*
* Example:
* ```ts
* import type { FBQ, PurchaseParams } from '$lib/types/fbq';
* // call the global pixel (browser only)
* window.fbq('track', 'Purchase', { currency: 'USD', value: 9.99 } as PurchaseParams);
* ```
*/
/** A Meta Pixel identifier string. */
export type PixelId = string;
/**
* Optional fourth argument for deduplication when sending events to Conversions
* API and test events.
*/
export type EventOptions = {
eventID?: string;
test_event_code?: string;
};
/**
* Common event parameter values shared across many standard events.
* Use `EventParamsByName` to map specific standard events to their params.
*/
export type CommonParams = Partial<{
/**
* Category of the page/product.
*/
content_category: string;
/**
* Product IDs associated with the event, such as SKUs (e.g. ["ABC123", "XYZ456"]).
*/
content_ids: (string | number)[];
/**
* Name of the page/product.
*/
content_name: string;
/**
* Type of content referenced in `content_ids`. If IDs refer to individual
* products, use 'product'. If they refer to groups of products, use 'product_group'.
*
* If empty, Meta will match the event to every item that has the same ID,
* independent of its type.
*/
content_type: 'product' | 'product_group';
/**
* Array of JSON objects that contains the quantity and the International
* Article Number (EAN) when applicable, or other product or content identifiers.
*
* Custom parameters can also be included in each object.
*/
contents: { id: string | number; quantity: number }[];
/**
* The currency for the `value` specified.
*/
currency: string;
/**
* The number of items when checkout was initiated (used with `InitiateCheckout` event).
*/
num_items: number;
/**
* Predicted lifetime value of a subscriber as defined by the advertiser
* and expressed as an exact value.
*/
predicted_ltv: number;
/**
* Search string used in a search event (used with `Search` event).
*/
search_string: string;
/**
* Shows status of a registration (used with `CompleteRegistration` event).
*/
status: boolean;
/**
* The value of a user performing this event to the business.
*/
value: number;
}>;
/**
* When payment information is added in the checkout flow.
*/
export type AddPaymentInfoParams = Partial<
Pick<CommonParams, 'content_ids' | 'contents' | 'currency' | 'value'>
>;
/**
* When a product is added to the shopping cart.
*/
export type AddToCartParams = Partial<
Pick<CommonParams, 'content_ids' | 'content_type' | 'contents' | 'currency' | 'value'>
>;
/**
* When a product is added to a wishlist.
*/
export type AddToWishlistParams = Partial<
Pick<CommonParams, 'content_ids' | 'contents' | 'currency' | 'value'>
>;
/**
* When a registration form is completed.
*/
export type CompleteRegistrationParams = Partial<Pick<CommonParams, 'currency' | 'value'>> & {
status?: boolean;
};
/**
* When a person initiates contact with your business.
*/
export type ContactParams = Partial<CommonParams>;
/**
* When a person customizes a product.
*/
export type CustomizeProductParams = Partial<CommonParams>;
/**
* When a person donates funds to your organization or cause.
*/
export type DonateParams = Partial<CommonParams>;
/**
* When a person searched for a location of your store via a website or app,
* with an intention to visit the physical location.
*/
export type FindLocationParams = Partial<CommonParams>;
/**
* When a person enters the checkout flow prior to completing a purchase.
*/
export type InitiateCheckoutParams = Partial<
Pick<CommonParams, 'content_ids' | 'contents' | 'currency' | 'num_items' | 'value'>
>;
/**
* When a sign up is completed.
*/
export type LeadParams = Partial<Pick<CommonParams, 'currency' | 'value'>>;
/**
* When a purchase is made or checkout flow is completed.
*/
export type PurchaseParams = {
currency: string;
value: number;
} & Partial<Pick<CommonParams, 'content_ids' | 'content_type' | 'contents' | 'num_items'>>;
/**
* When a person books an appointment to visit one of your locations.
*/
export type ScheduleParams = Partial<CommonParams>;
/**
* When a search is made.
*/
export type SearchParams = Partial<
Pick<
CommonParams,
'content_ids' | 'content_type' | 'contents' | 'currency' | 'value' | 'search_string'
>
>;
/**
* When a person starts a free trial of a product or service you offer.
*/
export type StartTrialParams = Partial<Pick<CommonParams, 'currency' | 'predicted_ltv' | 'value'>>;
/**
* When a person applies for a product, service, or program you offer.
*/
export type SubmitApplicationParams = Partial<CommonParams>;
/**
* When a person subscribes to a paid product or service you offer.
*/
export type SubscribeParams = Partial<Pick<CommonParams, 'currency' | 'predicted_ltv' | 'value'>>;
/**
* A visit to a web page you care about (for example, a product page or landing page).
*/
export type ViewContentParams = Partial<
Pick<CommonParams, 'content_ids' | 'content_type' | 'contents' | 'currency' | 'value'>
>;
/** Standard event names supported by Meta Pixel. */
export type StandardEventName =
| 'AddPaymentInfo'
| 'AddToCart'
| 'AddToWishlist'
| 'CompleteRegistration'
| 'Contact'
| 'CustomizeProduct'
| 'Donate'
| 'FindLocation'
| 'InitiateCheckout'
| 'Lead'
| 'Purchase'
| 'Schedule'
| 'Search'
| 'StartTrial'
| 'SubmitApplication'
| 'Subscribe'
| 'ViewContent'
| 'PageView';
/**
* Advanced matching fields accepted by the Pixel init call. These fields are
* used to help match site visitors to people on Meta for better attribution.
* See Meta's Advanced Matching docs for details; values should generally be
* raw strings (or hashed values if you pre-hash on the client/server).
*/
export type AdvancedMatching = {
/** Primary contact email (or hashed email) */
email?: string;
/** Phone number (E.164 or local) */
phone?: string;
/** First name */
first_name?: string;
/** Last name */
last_name?: string;
/** City */
city?: string;
/** State/region */
state?: string;
/** Postal / ZIP code */
zip?: string;
/** Country code */
country?: string;
/** External id to match users (optional) */
external_id?: string;
/** Gender */
gender?: string;
/** Date of birth (ISO-like or YYYY-MM-DD) */
date_of_birth?: string;
// allow additional provider-specific keys
[key: string]: string | undefined;
};
/** Arbitrary init/config options from the global pixel initialization API. */
export type InitOptions = Record<string, unknown>;
/** Additional custom params for custom events. */
export type CustomParams = Record<string, unknown>;
/** Map each standard event name to its allowed parameter shape. */
export type EventParamsByName = {
AddPaymentInfo: AddPaymentInfoParams;
AddToCart: AddToCartParams;
AddToWishlist: AddToWishlistParams;
CompleteRegistration: CompleteRegistrationParams;
Contact: ContactParams;
CustomizeProduct: CustomizeProductParams;
Donate: DonateParams;
FindLocation: FindLocationParams;
InitiateCheckout: InitiateCheckoutParams;
Lead: LeadParams;
Purchase: PurchaseParams;
Schedule: ScheduleParams;
Search: SearchParams;
StartTrial: StartTrialParams;
SubmitApplication: SubmitApplicationParams;
Subscribe: SubscribeParams;
ViewContent: ViewContentParams;
PageView: Record<string, never>;
};
/** Strongly-typed interface for the `fbq` global provided by Meta Pixel. */
export interface FBQ {
/**
* Initialize a pixel for this page.
* @param pixelId The pixel identifier to initialize.
* @param advancedMatching Optional advanced matching data (email/phone/etc.).
* @param options Additional init options.
*/
(cmd: 'init', pixelId: PixelId, advancedMatching?: AdvancedMatching, options?: InitOptions): void;
// Strongly-typed track overloads.
(cmd: 'track', event: 'Purchase', params: PurchaseParams, options?: EventOptions): void;
/** Generic overload for other standard events where params are optional */
<K extends Exclude<StandardEventName, 'Purchase'>>(
cmd: 'track',
event: K,
params?: EventParamsByName[K],
options?: EventOptions
): void;
/** Custom event variant — accepts arbitrary custom keys in addition to `CommonParams`. */
(
cmd: 'trackCustom',
event: string,
params?: CommonParams & CustomParams,
options?: EventOptions
): void;
/** Single-pixel variants that mirror the typed `track` API without duplicating event shapes. */
<K extends StandardEventName>(
cmd: 'trackSingle',
pixelId: PixelId,
event: K,
params?: EventParamsByName[K],
options?: EventOptions
): void;
(
cmd: 'trackSingleCustom',
pixelId: PixelId,
event: string,
params?: CommonParams & CustomParams,
options?: EventOptions
): void;
// Configuration helpers with typed keys and values.
(cmd: 'set', key: 'autoConfig', value: boolean, pixelId?: PixelId): void;
(
cmd: 'set',
key: 'test_event_code' | 'agent' | 'eventSourceUrl',
value: string,
pixelId?: PixelId
): void;
(cmd: 'set', config: Record<string, unknown>): void;
// consent and LDU
(cmd: 'consent', state: 'grant' | 'revoke'): void;
(cmd: 'dataProcessingOptions', options: string[], countryCode?: number, stateCode?: number): void;
}
declare global {
interface Window {
fbq: FBQ;
_fbq?: FBQ;
}
}
export {};

17
src/lib/util/getter.ts Normal file
View File

@@ -0,0 +1,17 @@
/**
* MaybeGetter is a type that can either be a value of type T or a function that returns a value of type T.
* This is useful for cases where you might want to pass a value directly or a function that computes the
* value later, potentially taking advantage of reactivity.
*/
export type MaybeGetter<T> = T | (() => T);
/**
* ResolveGetter returns the underlying value stored by a MaybeGetter type.
* @returns Raw value T or function return T.
*/
export const resolveGetter = <T>(getter: MaybeGetter<T>): T => {
if (typeof getter === 'function') {
return (getter as () => T)();
}
return getter;
};

View File

@@ -0,0 +1,66 @@
import { browser } from '$app/environment';
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;
};
/**
* 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.
*/
export const loadMetaPixel = (): Promise<void> => {
// Make sure we're using the browser
if (!browser || !window) {
return Promise.reject(new Error('Window is undefined'));
}
// If fbq is already defined, resolve immediately
const existing = window.fbq as QueuedFBQ | undefined;
if (existing && existing.loaded) {
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';
window.fbq = q;
return new Promise((resolve, reject) => {
// Avoid adding the same script twice
const existingScript = document.querySelector(
`script[src="${SCRIPT_SRC}"]`
) as HTMLScriptElement | null;
if (existingScript) {
existingScript.addEventListener('load', () => resolve());
existingScript.addEventListener('error', () =>
reject(new Error('Failed to load Meta Pixel script'))
);
return;
}
// Otherwise, create the script element
const script = document.createElement('script');
script.src = SCRIPT_SRC;
script.async = true;
script.addEventListener('load', () => resolve());
script.addEventListener('error', () => reject(new Error('Failed to load Meta Pixel script')));
document.head.appendChild(script);
});
};