add meta pixel integration
This commit is contained in:
190
src/lib/MetaPixel.svelte
Normal file
190
src/lib/MetaPixel.svelte
Normal 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
351
src/lib/types/fbq.d.ts
vendored
Normal 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
17
src/lib/util/getter.ts
Normal 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;
|
||||
};
|
||||
66
src/lib/util/meta-pixel-loader.ts
Normal file
66
src/lib/util/meta-pixel-loader.ts
Normal 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);
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user