diff --git a/src/lib/MetaPixel.svelte b/src/lib/MetaPixel.svelte deleted file mode 100644 index e2d6106..0000000 --- a/src/lib/MetaPixel.svelte +++ /dev/null @@ -1,352 +0,0 @@ - - - - - diff --git a/src/lib/index.ts b/src/lib/index.ts index be123e9..5460a6b 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -3,8 +3,9 @@ import { dev } from '$app/environment'; import log from 'loglevel'; +export type * from './types/conversion.d.ts'; export type * as fbq from './types/fbq.d.ts'; -export { default as MetaPixel, PixelControl } from './MetaPixel.svelte'; +export * from './metapixel/index.ts'; export * from './tracking.svelte.ts'; export { default as Umami } from './Umami.svelte'; export * from './conversion/index.ts'; diff --git a/src/lib/metapixel/MetaPixel.svelte b/src/lib/metapixel/MetaPixel.svelte new file mode 100644 index 0000000..b27bb9a --- /dev/null +++ b/src/lib/metapixel/MetaPixel.svelte @@ -0,0 +1,68 @@ + + + diff --git a/src/lib/metapixel/index.ts b/src/lib/metapixel/index.ts new file mode 100644 index 0000000..654cbaa --- /dev/null +++ b/src/lib/metapixel/index.ts @@ -0,0 +1,2 @@ +export { default as MetaPixel } from './MetaPixel.svelte'; +export { PixelControl, type PixelControlOptions } from './pixel-control.ts'; diff --git a/src/lib/metapixel/pixel-control.ts b/src/lib/metapixel/pixel-control.ts new file mode 100644 index 0000000..59c2794 --- /dev/null +++ b/src/lib/metapixel/pixel-control.ts @@ -0,0 +1,284 @@ +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 { resolveGetter, type MaybeGetter } from '../util/getter.ts'; +import log from 'loglevel'; +import { dev } from '$app/environment'; +import { ConversionClient } from '../conversion/client.ts'; + +export type PixelControlOptions = { + /** + * if provided, events fired will always have this code attached + * to prevent them from polluting real analytics data. + */ + testEventCode?: string; + /** + * if provided, all events fired will be passed to the server endpoint + * at this URL to be sent via the Conversion API. Any events sent + * without a event ID will be assigned a random one. + */ + conversionHref?: string; + /** Advanced matching data */ + advancedMatching?: AdvancedMatching; + /** Initialization options */ + initOptions?: InitOptions; +}; + +/** + * Manages multiple Meta Pixel instances and provides methods to + * interact with them, including consent management and event tracking. + */ +export class PixelControl { + private _pixelID: string; + private _testEventCode?: string = undefined; + private _trackingManager: MaybeGetter; + private _conversionClient?: ConversionClient = undefined; + + private static _baseLoaded: boolean = false; + private static _registeredPixels: Record = {}; + + /** Indicates whether the Meta Pixel base script has been loaded. */ + 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, + pixelID: string, + options?: PixelControlOptions + ) { + this._trackingManager = trackingManager; + this._pixelID = pixelID; + this._testEventCode = options?.testEventCode; + + const resolvedTrackingManager = resolveGetter(trackingManager); + if (options?.conversionHref && resolvedTrackingManager) { + this._conversionClient = new ConversionClient( + options.conversionHref, + resolvedTrackingManager + ); + } else if (options?.conversionHref) { + log.warn( + `Conversion Client ${options.conversionHref} for Meta Pixel [${this._pixelID}] not initialized - TrackingManager is required for user consent.` + ); + } + } + + /** Loads the Meta Pixel base script. */ + static async load() { + if (this._baseLoaded && !!window.fbq) return; + if (!window.fbq) { + try { + await loadMetaPixel(); // Load the Meta Pixel script + } catch (e) { + log.warn('Failed to load Meta Pixel script, all events will be queued.', e); + } + } + this._baseLoaded = true; + log.debug('Meta Pixel base script loaded.'); + } + + /** Tells the Meta pixel that the user has given consent for tracking. */ + static grantConsent() { + this.loadGuard(); + window.fbq?.('consent', 'grant'); + log.debug('Meta Pixel consent granted.'); + } + + /** Tells the Meta pixel that the user has revoked consent for tracking. */ + static revokeConsent() { + this.loadGuard(); + window.fbq?.('consent', 'revoke'); + log.debug('Meta Pixel consent revoked.'); + } + + /** + * Registers 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. + * Should only be called once for each Pixel ID, use PixelControl.get() + * to retrieve existing instances. + * @param trackingManager Tracking manager to handle user consent for tracking + * @param pixelID Meta Pixel ID + * @param options Optional settings + * @returns PixelControl instance + */ + static initialize( + trackingManager: MaybeGetter, + pixelID: string, + options?: PixelControlOptions + ): PixelControl { + // Load the base script if not already loaded + PixelControl.load(); + + // Check for existing PixelControl instance + if (this._registeredPixels[pixelID]) { + log.warn( + `PixelControl instance for Meta Pixel ID: ${pixelID} already exists. Returning existing instance.` + ); + return this._registeredPixels[pixelID]; + } + + // Create and register the PixelControl instance + const pixel = new PixelControl(trackingManager, pixelID, options); + this._registeredPixels[pixelID] = pixel; + + // Fire initialization + window.fbq('init', pixel._pixelID, options?.advancedMatching, options?.initOptions); + log.debug(`Meta Pixel [${pixel._pixelID}] initialized.`); + + return pixel; + } + + /** + * Returns an existing PixelControl instance for the given Meta Pixel ID. + * @param pixelID Meta Pixel ID + * @returns PixelControl instance + * @throws Error if no PixelControl instance is found for the given ID. + */ + static get(pixelID: string): PixelControl { + const pixel = this._registeredPixels[pixelID]; + if (!pixel) { + throw new Error(`No PixelControl instance found for Meta Pixel ID: ${pixelID}`); + } + return pixel; + } + + /** + * 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; + + let eventID: string | undefined = undefined; + // Optionally, send to conversion API endpoint if configured + if (this._conversionClient) { + eventID = crypto.randomUUID(); + this._conversionClient + .trackEvent('PageView', { eventID }) + .then((response) => { + log.debug( + `Meta Pixel [${this._pixelID}] PageView event sent to Conversion API with Event ID: ${eventID}, Response: ${JSON.stringify( + response + )}` + ); + }) + .catch((error) => { + log.error( + `Meta Pixel [${this._pixelID}] Failed to send PageView event to Conversion API with Event ID: ${eventID}`, + error + ); + }); + } + + // Send the PageView event to Meta + if (!dev || this._testEventCode) { + window.fbq('track', 'PageView', undefined, { + test_event_code: this._testEventCode, + eventID + }); + log.debug( + `Meta Pixel [${this._pixelID}] PageView event sent${dev && ` (test code: ${this._testEventCode})`}.` + ); + } else { + log.info( + `Meta Pixel [${this._pixelID}] PageView event not sent in development mode without a test event code.` + ); + } + } + + /** + * Tracks a standard event for this pixel (uses `trackSingle` under the hood) + * @throws Error if the Meta Pixel is not initialized. + */ + track(event: K, params?: EventParamsByName[K], eventID?: string) { + if (!this.consentGuard()) return; + + // Optionally, send to conversion API endpoint if configured + if (this._conversionClient) { + eventID = eventID ?? crypto.randomUUID(); + this._conversionClient + .trackEvent(event, { eventID: eventID, customData: params as any }) + .then((response) => { + log.debug( + `Meta Pixel [${this._pixelID}] ${event} event sent to Conversion API with Event ID: ${eventID}, Response: ${JSON.stringify( + response + )}` + ); + }) + .catch((error) => { + log.error( + `Meta Pixel [${this._pixelID}] Failed to send ${event} event to Conversion API with Event ID: ${eventID}`, + error + ); + }); + } + + // Send the PageView event to Meta + if (!dev || this._testEventCode) { + window.fbq('trackSingle', this._pixelID, event, params, { + eventID, + test_event_code: this._testEventCode + }); + log.debug( + `Meta Pixel [${this._pixelID}] ${event} event sent${dev && ` (test code: ${this._testEventCode})`}.` + ); + } else { + log.info( + `Meta Pixel [${this._pixelID}] ${event} event not sent in development mode without a test event code.` + ); + } + } + + /** + * 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; + if (!dev || this._testEventCode) { + window.fbq('trackSingleCustom', this._pixelID, event, params, { + eventID, + test_event_code: this._testEventCode + }); + log.debug( + `Meta Pixel [${this._pixelID}] ${event} custom event sent (test code: ${this._testEventCode}).` + ); + } else { + log.info( + `Meta Pixel [${this._pixelID}] ${event} custom event not sent in development mode without a test event code.` + ); + } + } +}