From 49c74410a9275b72b413295107476ac60dce33e9 Mon Sep 17 00:00:00 2001 From: Elijah Duffy Date: Tue, 16 Dec 2025 17:21:52 -0800 Subject: [PATCH] add meta pixel integration --- src/lib/MetaPixel.svelte | 190 ++++++++++++++++ src/lib/types/fbq.d.ts | 351 ++++++++++++++++++++++++++++++ src/lib/util/getter.ts | 17 ++ src/lib/util/meta-pixel-loader.ts | 66 ++++++ 4 files changed, 624 insertions(+) create mode 100644 src/lib/MetaPixel.svelte create mode 100644 src/lib/types/fbq.d.ts create mode 100644 src/lib/util/getter.ts create mode 100644 src/lib/util/meta-pixel-loader.ts diff --git a/src/lib/MetaPixel.svelte b/src/lib/MetaPixel.svelte new file mode 100644 index 0000000..4326498 --- /dev/null +++ b/src/lib/MetaPixel.svelte @@ -0,0 +1,190 @@ + + + diff --git a/src/lib/types/fbq.d.ts b/src/lib/types/fbq.d.ts new file mode 100644 index 0000000..01bb570 --- /dev/null +++ b/src/lib/types/fbq.d.ts @@ -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 +>; + +/** + * When a product is added to the shopping cart. + */ +export type AddToCartParams = Partial< + Pick +>; + +/** + * When a product is added to a wishlist. + */ +export type AddToWishlistParams = Partial< + Pick +>; + +/** + * When a registration form is completed. + */ +export type CompleteRegistrationParams = Partial> & { + status?: boolean; +}; + +/** + * When a person initiates contact with your business. + */ +export type ContactParams = Partial; + +/** + * When a person customizes a product. + */ +export type CustomizeProductParams = Partial; + +/** + * When a person donates funds to your organization or cause. + */ +export type DonateParams = Partial; + +/** + * 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; + +/** + * When a person enters the checkout flow prior to completing a purchase. + */ +export type InitiateCheckoutParams = Partial< + Pick +>; + +/** + * When a sign up is completed. + */ +export type LeadParams = Partial>; + +/** + * When a purchase is made or checkout flow is completed. + */ +export type PurchaseParams = { + currency: string; + value: number; +} & Partial>; + +/** + * When a person books an appointment to visit one of your locations. + */ +export type ScheduleParams = Partial; + +/** + * 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>; + +/** + * When a person applies for a product, service, or program you offer. + */ +export type SubmitApplicationParams = Partial; + +/** + * When a person subscribes to a paid product or service you offer. + */ +export type SubscribeParams = Partial>; + +/** + * A visit to a web page you care about (for example, a product page or landing page). + */ +export type ViewContentParams = Partial< + Pick +>; + +/** 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; + +/** Additional custom params for custom events. */ +export type CustomParams = Record; + +/** 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; +}; + +/** 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 */ + >( + 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. */ + ( + 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): 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 {}; diff --git a/src/lib/util/getter.ts b/src/lib/util/getter.ts new file mode 100644 index 0000000..a259fb5 --- /dev/null +++ b/src/lib/util/getter.ts @@ -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); + +/** + * ResolveGetter returns the underlying value stored by a MaybeGetter type. + * @returns Raw value T or function return T. + */ +export const resolveGetter = (getter: MaybeGetter): T => { + if (typeof getter === 'function') { + return (getter as () => T)(); + } + return getter; +}; diff --git a/src/lib/util/meta-pixel-loader.ts b/src/lib/util/meta-pixel-loader.ts new file mode 100644 index 0000000..c5485de --- /dev/null +++ b/src/lib/util/meta-pixel-loader.ts @@ -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 => { + // 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); + }); +};