pixel: fix $state usage, fix load ordering
This commit is contained in:
305
src/lib/metapixel/pixel-control.ts
Normal file
305
src/lib/metapixel/pixel-control.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
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';
|
||||
import type { ConversionEventParams } from '$lib/types/conversion.js';
|
||||
|
||||
const pixelParamsToCustomData = (params: CommonParams & CustomParams): ConversionEventParams => {
|
||||
const customData: ConversionEventParams = {};
|
||||
if (params.value) customData.value = params.value;
|
||||
if (params.currency) customData.currency = params.currency;
|
||||
if (params.content_name) customData.content_name = params.content_name;
|
||||
if (params.content_category) customData.content_category = params.content_category;
|
||||
if (params.content_ids) customData.content_ids = params.content_ids;
|
||||
if (params.contents) {
|
||||
const acc: ConversionEventParams['contents'] = [];
|
||||
customData.contents = params.contents.reduce((acc, content) => {
|
||||
acc.push({
|
||||
id: content.id.toString(),
|
||||
quantity: content.quantity
|
||||
});
|
||||
return acc;
|
||||
}, acc);
|
||||
}
|
||||
if (params.num_items) customData.num_items = params.num_items;
|
||||
if (params.search_string) customData.search_string = params.search_string;
|
||||
if (params.predicted_ltv) customData.predicted_ltv = params.predicted_ltv;
|
||||
return customData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for configuring a PixelControl instance.
|
||||
*/
|
||||
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<TrackingManager | undefined>;
|
||||
private _conversionClient?: ConversionClient = undefined;
|
||||
|
||||
private static _baseLoaded: boolean = false;
|
||||
private static _registeredPixels: Record<string, PixelControl> = {};
|
||||
|
||||
/** 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<TrackingManager | undefined>,
|
||||
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(
|
||||
`[PixelControl] Conversion Client ${options.conversionHref} for Meta Pixel [${this._pixelID}] not initialized, TrackingManager is required for user consent.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Loads the Meta Pixel base script. */
|
||||
static load() {
|
||||
if (this._baseLoaded && !!window.fbq) return;
|
||||
loadMetaPixel(); // Load the Meta Pixel script
|
||||
this._baseLoaded = true;
|
||||
log.debug('[PixelControl] Meta Pixel base script loaded.', this._baseLoaded);
|
||||
}
|
||||
|
||||
/** Tells the Meta pixel that the user has given consent for tracking. */
|
||||
static grantConsent() {
|
||||
this.loadGuard();
|
||||
window.fbq?.('consent', 'grant');
|
||||
log.debug('[PixelControl] 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('[PixelControl] 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<TrackingManager | undefined>,
|
||||
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(`[PixelControl] [${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(
|
||||
`[PixelControl] [${this._pixelID}] PageView event sent to Conversion API with Event ID: ${eventID}, Response: ${JSON.stringify(
|
||||
response
|
||||
)}`
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error(
|
||||
`[PixelControl] [${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(
|
||||
`[PixelControl] [${this._pixelID}] PageView event sent${dev && ` (test code: ${this._testEventCode})`}.`
|
||||
);
|
||||
} else {
|
||||
log.info(
|
||||
`[PixelControl] [${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<K extends StandardEventName>(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: pixelParamsToCustomData(params ?? {}) })
|
||||
.then((response) => {
|
||||
log.debug(
|
||||
`[PixelControl] [${this._pixelID}] ${event} event sent to Conversion API with Event ID: ${eventID}, Response: ${JSON.stringify(
|
||||
response
|
||||
)}`
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error(
|
||||
`[PixelControl] [${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(
|
||||
`[PixelControl] [${this._pixelID}] ${event} event sent${dev && ` (test code: ${this._testEventCode})`}.`
|
||||
);
|
||||
} else {
|
||||
log.info(
|
||||
`[PixelControl] [${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(
|
||||
`[PixelControl] [${this._pixelID}] ${event} custom event sent (test code: ${this._testEventCode}).`
|
||||
);
|
||||
} else {
|
||||
log.info(
|
||||
`[PixelControl] [${this._pixelID}] ${event} custom event not sent in development mode without a test event code.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user