Compare commits
7 Commits
2e12d281ef
...
0cd3f10da6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cd3f10da6 | ||
|
|
f2d389ee64 | ||
|
|
f5ec7b3812 | ||
|
|
fc07bb057c | ||
|
|
9400e81aaa | ||
|
|
824fd262ed | ||
|
|
82cce84a4e |
@@ -1,285 +0,0 @@
|
||||
<!-- @component
|
||||
MetaPixel integrates the Meta (Facebook) Pixel into your Svelte application,
|
||||
allowing you to track page views and custom events while respecting user consent
|
||||
for tracking. The component manages the lifecycle of the Meta Pixel script and
|
||||
PixelControl interface.
|
||||
|
||||
The PixelControl class also allows you to directly manage multiple Pixel
|
||||
instances and handle event tracking with optional test event codes without
|
||||
using the MetaPixel component.
|
||||
-->
|
||||
|
||||
<script lang="ts" module>
|
||||
export type PixelControlOptions = {
|
||||
/**
|
||||
* if provided, events fired will always have this code attached
|
||||
* to prevent them from polluting real analytics data.
|
||||
*/
|
||||
testEventCode?: string;
|
||||
/** Advanced matching data */
|
||||
advancedMatching?: AdvancedMatching;
|
||||
/** Initialization options */
|
||||
initOptions?: InitOptions;
|
||||
};
|
||||
|
||||
export class PixelControl {
|
||||
private _pixelID: string;
|
||||
private _testEventCode?: string = undefined;
|
||||
private _trackingManager: MaybeGetter<TrackingManager | 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;
|
||||
}
|
||||
|
||||
/** 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<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(`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;
|
||||
// Send the PageView event
|
||||
if (!dev || this._testEventCode) {
|
||||
window.fbq('track', 'PageView', undefined, { test_event_code: this._testEventCode });
|
||||
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<K extends StandardEventName>(event: K, params?: EventParamsByName[K], eventID?: string) {
|
||||
if (!this.consentGuard()) return;
|
||||
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.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
</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';
|
||||
import log from 'loglevel';
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Tracking manager to handle user consent for tracking. If omitted
|
||||
* tracking is disabled by default until consent is granted via
|
||||
* PixelControl.grantConsent().
|
||||
*/
|
||||
trackingManager?: TrackingManager;
|
||||
|
||||
/** Meta Pixel ID */
|
||||
pixelID: string;
|
||||
|
||||
/** Meta Pixel Options */
|
||||
pixelOptions?: PixelControlOptions;
|
||||
|
||||
/**
|
||||
* Controls whether page views are automatically tracked by this
|
||||
* component (default: true).
|
||||
*/
|
||||
autoPageView?: boolean;
|
||||
}
|
||||
|
||||
let { pixelID, pixelOptions, 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.initialize(trackingManager, pixelID, pixelOptions);
|
||||
|
||||
trackingManager.runWithConsent(() => {
|
||||
if (autoPageView && pixel) {
|
||||
pixel.pageView();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onNavigate(() => {
|
||||
trackingManager?.runWithConsent(() => {
|
||||
if (autoPageView && pixel) {
|
||||
pixel.pageView();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export const getPixelControl = (): PixelControl => {
|
||||
if (!pixel) {
|
||||
throw new Error('MetaPixel component has not been initialized yet, wait for onMount.');
|
||||
}
|
||||
return pixel;
|
||||
};
|
||||
</script>
|
||||
@@ -25,7 +25,9 @@
|
||||
const consentGranted = $derived(trackingManager ? trackingManager.consent === true : true);
|
||||
|
||||
// Development overrides to prevent dirty analytics
|
||||
const devConsoleTag = $derived(`[dev][consent: ${consentGranted ? 'granted' : 'revoked'}]`);
|
||||
const devConsoleTag = $derived(
|
||||
`[Umami] [dev][consent: ${consentGranted ? 'granted' : 'revoked'}]`
|
||||
);
|
||||
const devOverride = {
|
||||
track: (...args: unknown[]): Promise<string> | undefined => {
|
||||
log.debug(`${devConsoleTag}: Track called with:`, ...args);
|
||||
@@ -62,7 +64,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
if (dev) log.debug('[dev]: Umami tracking disabled');
|
||||
if (dev) log.info('[Umami] [dev]: tracking disabled');
|
||||
|
||||
onMount(() => {
|
||||
if (dev) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { getFbpFbc } from '../metapixel/fbc.ts';
|
||||
import type { TrackingManager } from '$lib/tracking.svelte';
|
||||
import type {
|
||||
ConversionErrorResponseBody,
|
||||
ConversionEventParams,
|
||||
ConversionRequestBody,
|
||||
ConversionResponseBody,
|
||||
ConversionUserData
|
||||
@@ -37,17 +39,16 @@ export class ConversionClient {
|
||||
options: {
|
||||
eventID: string;
|
||||
user?: Omit<ConversionUserData, 'ip' | 'fbp' | 'fbc' | 'ua'>;
|
||||
customData?: Record<string, string>;
|
||||
customData?: ConversionEventParams;
|
||||
}
|
||||
): Promise<ConversionResponseBody | ConversionErrorResponseBody> {
|
||||
// Extract user data
|
||||
const fbp = await cookieStore.get('_fbp');
|
||||
const fbc = await cookieStore.get('_fbc');
|
||||
const { fbp, fbc } = getFbpFbc();
|
||||
|
||||
const user: ConversionUserData = {
|
||||
...options.user,
|
||||
fbp: fbp?.value,
|
||||
fbc: fbc?.value
|
||||
fbp,
|
||||
fbc
|
||||
};
|
||||
|
||||
// Get event source URL & extract UTM params if present
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import {
|
||||
Content,
|
||||
CustomData,
|
||||
EventRequest,
|
||||
EventResponse,
|
||||
ServerEvent,
|
||||
UserData
|
||||
} from 'facebook-nodejs-business-sdk';
|
||||
import type { StandardEventName } from '../types/fbq.js';
|
||||
import type { ConversionUserData } from '$lib/types/conversion.js';
|
||||
import type {
|
||||
ConversionEventDetails,
|
||||
ConversionEventParams,
|
||||
ConversionResponseBody,
|
||||
ConversionUserData
|
||||
} from '$lib/types/conversion.js';
|
||||
import { dev } from '$app/environment';
|
||||
import log from 'loglevel';
|
||||
|
||||
/**
|
||||
* Builds UserData for conversion events.
|
||||
@@ -14,7 +21,7 @@ import type { ConversionUserData } from '$lib/types/conversion.js';
|
||||
* @param data - The user data to include.
|
||||
* @returns The constructed UserData object.
|
||||
*/
|
||||
export const buildConversionUserData = (data: ConversionUserData): UserData => {
|
||||
const buildUserData = (data: ConversionUserData): UserData => {
|
||||
const userData = new UserData();
|
||||
if (data.email) userData.setEmail(data.email);
|
||||
if (data.phone) userData.setPhone(data.phone);
|
||||
@@ -34,38 +41,39 @@ export const buildConversionUserData = (data: ConversionUserData): UserData => {
|
||||
* @param params - The custom data parameters.
|
||||
* @returns The constructed CustomData object.
|
||||
*/
|
||||
export const buildCustomData = (params: Record<string, string>): CustomData => {
|
||||
const buildCustomData = (params: ConversionEventParams): CustomData => {
|
||||
const c = new CustomData();
|
||||
// Map known fields safely
|
||||
if (params.value) c.setValue(params.value);
|
||||
if (params.net_revenue) c.setNetRevenue(params.net_revenue);
|
||||
if (params.currency) c.setCurrency(params.currency);
|
||||
if (typeof params.value === 'number') c.setValue(params.value);
|
||||
if (Array.isArray(params.contents)) c.setContents(params.contents);
|
||||
if (params.content_type) c.setContentType(params.content_type);
|
||||
if (Array.isArray(params.content_ids)) c.setContentIds(params.content_ids);
|
||||
if (typeof params.num_items === 'number') c.setNumItems(params.num_items);
|
||||
if (params.search_string) c.setSearchString(params.search_string);
|
||||
// Attach anything else as custom_properties
|
||||
const extras = { ...params };
|
||||
delete extras.currency;
|
||||
delete extras.value;
|
||||
delete extras.contents;
|
||||
delete extras.content_type;
|
||||
delete extras.content_ids;
|
||||
delete extras.num_items;
|
||||
delete extras.search_string;
|
||||
if (Object.keys(extras).length) c.setCustomProperties(extras);
|
||||
if (params.content_name) c.setContentName(params.content_name);
|
||||
if (params.content_category) c.setContentCategory(params.content_category);
|
||||
if (params.content_ids) c.setContentIds(params.content_ids);
|
||||
if (params.contents) {
|
||||
const contents: Content[] = params.contents.map((content) => {
|
||||
const c = new Content();
|
||||
if (content.id) c.setId(content.id.toString());
|
||||
if (content.quantity) c.setQuantity(content.quantity);
|
||||
if (content.item_price) c.setItemPrice(content.item_price);
|
||||
if (content.title) c.setTitle(content.title);
|
||||
if (content.description) c.setDescription(content.description);
|
||||
if (content.category) c.setCategory(content.category);
|
||||
if (content.brand) c.setBrand(content.brand);
|
||||
if (content.delivery_category) c.setDeliveryCategory(content.delivery_category);
|
||||
return c;
|
||||
});
|
||||
c.setContents(contents);
|
||||
}
|
||||
if (params.content_type) c.setContentType(params.content_type);
|
||||
if (params.order_id) c.setOrderId(params.order_id);
|
||||
if (params.predicted_ltv) c.setPredictedLtv(params.predicted_ltv);
|
||||
if (params.num_items) c.setNumItems(params.num_items);
|
||||
if (params.search_string) c.setSearchString(params.search_string);
|
||||
if (params.status) c.setStatus(params.status);
|
||||
if (params.item_number) c.setItemNumber(params.item_number);
|
||||
if (params.delivery_category) c.setDeliveryCategory(params.delivery_category);
|
||||
if (params.custom_properties) c.setCustomProperties(params.custom_properties);
|
||||
return c;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parameters for sending a conversion event to Meta Pixel.
|
||||
*/
|
||||
export type ConversionEventOptions = {
|
||||
eventID: string;
|
||||
eventSourceURL?: string;
|
||||
actionSource: 'website' | 'app' | 'offline' | 'other';
|
||||
userData: UserData;
|
||||
customData?: CustomData;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -90,7 +98,8 @@ export class ConversionControl {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a conversion event to Meta Pixel.
|
||||
* Sends a conversion event to Meta Pixel. Requires a test event code for
|
||||
* events to be sent to Meta when in development mode.
|
||||
*
|
||||
* @param eventName - The name of the standard event to send.
|
||||
* @param options - Additional options for the event.
|
||||
@@ -98,20 +107,51 @@ export class ConversionControl {
|
||||
*/
|
||||
async trackEvent(
|
||||
eventName: StandardEventName,
|
||||
options: ConversionEventOptions
|
||||
): Promise<EventResponse> {
|
||||
details: ConversionEventDetails,
|
||||
params?: ConversionEventParams
|
||||
): Promise<ConversionResponseBody> {
|
||||
const event = new ServerEvent()
|
||||
.setEventName(eventName)
|
||||
.setEventTime(Math.floor(Date.now() / 1000))
|
||||
.setEventId(options.eventID)
|
||||
.setUserData(options.userData)
|
||||
.setActionSource(options.actionSource);
|
||||
.setEventId(details.eventID)
|
||||
.setUserData(buildUserData(details.userData))
|
||||
.setActionSource(details.actionSource);
|
||||
|
||||
if (options?.eventSourceURL) event.setEventSourceUrl(options.eventSourceURL);
|
||||
if (options?.customData) event.setCustomData(options.customData);
|
||||
if (details?.eventSourceURL) event.setEventSourceUrl(details.eventSourceURL);
|
||||
if (params) {
|
||||
const customData = buildCustomData(params);
|
||||
event.setCustomData(customData);
|
||||
}
|
||||
|
||||
// If we're in dev mode and missing a test event code, log and exit
|
||||
if (dev && !this._testEventCode) {
|
||||
log.debug(
|
||||
`[ConversionControl] ${eventName} event not sent - missing test event code in dev mode.`
|
||||
);
|
||||
return Promise.resolve({
|
||||
pixelID: this._pixelID,
|
||||
fbtrace_id: 'dev-mode-no-test-event-code',
|
||||
receivedEvents: 1,
|
||||
processedEvents: 0,
|
||||
messages: ['Event not sent - missing test event code in dev mode.']
|
||||
});
|
||||
}
|
||||
|
||||
const req = new EventRequest(this._accessToken, this._pixelID).setEvents([event]);
|
||||
if (this._testEventCode) req.setTestEventCode(this._testEventCode);
|
||||
return req.execute();
|
||||
const response = await req.execute();
|
||||
|
||||
const structuredResponse: ConversionResponseBody = {
|
||||
pixelID: response.id,
|
||||
fbtrace_id: response.fbtrace_id,
|
||||
receivedEvents: response.events_received,
|
||||
processedEvents: response.num_processed_entries,
|
||||
messages: response.messages
|
||||
};
|
||||
|
||||
log.debug(
|
||||
`[ConversionControl] Sent ${eventName}, Event response: ${JSON.stringify(structuredResponse)}`
|
||||
);
|
||||
return structuredResponse;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { buildConversionUserData, buildCustomData, ConversionControl } from './control.ts';
|
||||
import { ConversionControl } from './control.ts';
|
||||
import type {
|
||||
ConversionErrorResponseBody,
|
||||
ConversionEventParams,
|
||||
ConversionRequestBody,
|
||||
ConversionResponseBody
|
||||
ConversionUserData
|
||||
} from '$lib/types/conversion.js';
|
||||
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
|
||||
const getEventIP = (request: Request, getClientAddress: () => string) => {
|
||||
return (
|
||||
request.headers.get('x-forwarded-for') ||
|
||||
request.headers.get('cf-connecting-ip') ||
|
||||
request.headers.get('x-real-ip') ||
|
||||
request.headers.get('x-client-ip') ||
|
||||
request.headers.get('x-cluster-client-ip') ||
|
||||
request.headers.get('x-original-forwarded-for') ||
|
||||
request.headers.get('forwarded-for') ||
|
||||
request.headers.get('forwarded') ||
|
||||
getClientAddress()
|
||||
);
|
||||
};
|
||||
|
||||
export const createConversionRequestHandler: (control: ConversionControl) => RequestHandler = (
|
||||
control
|
||||
) => {
|
||||
@@ -16,46 +31,38 @@ export const createConversionRequestHandler: (control: ConversionControl) => Req
|
||||
const body = (await request.json()) as ConversionRequestBody;
|
||||
|
||||
// Build user data with IP and user agent
|
||||
const ip = getClientAddress();
|
||||
const ip = getEventIP(request, getClientAddress);
|
||||
const ua = request.headers.get('user-agent');
|
||||
|
||||
const userData = buildConversionUserData({
|
||||
const userData: ConversionUserData = {
|
||||
...body.user,
|
||||
ip,
|
||||
ua: ua ?? body.user?.ua ?? undefined
|
||||
});
|
||||
};
|
||||
|
||||
// Build custom data with UTM params if applicable
|
||||
let rawCustomData = body.customData ?? {};
|
||||
let customData: ConversionEventParams = body.customData ?? {};
|
||||
if (body.eventName === 'PageView' && body.utms) {
|
||||
// For PageView events, automatically include UTM params if provided
|
||||
rawCustomData = {
|
||||
...rawCustomData,
|
||||
customData = {
|
||||
...customData,
|
||||
...body.utms
|
||||
};
|
||||
}
|
||||
|
||||
const customData = buildCustomData(rawCustomData);
|
||||
|
||||
// Send the event via the control
|
||||
const response = await control.trackEvent(body.eventName, {
|
||||
const response = await control.trackEvent(
|
||||
body.eventName,
|
||||
{
|
||||
eventID: body.eventID,
|
||||
eventSourceURL: body.eventSourceURL,
|
||||
actionSource: 'website',
|
||||
userData,
|
||||
userData
|
||||
},
|
||||
customData
|
||||
});
|
||||
);
|
||||
|
||||
// Structure the response
|
||||
const structuredResponse: ConversionResponseBody = {
|
||||
pixelID: response.id,
|
||||
fbtrace_id: response.fbtrace_id,
|
||||
receivedEvents: response.events_received,
|
||||
processedEvents: response.num_processed_entries,
|
||||
messages: response.messages
|
||||
};
|
||||
|
||||
return json(structuredResponse, { status: StatusCodes.OK });
|
||||
return json(response, { status: StatusCodes.OK });
|
||||
} catch (e) {
|
||||
return json(
|
||||
{ error: e instanceof Error ? e.message : String(e) } as ConversionErrorResponseBody,
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
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';
|
||||
|
||||
// set log level to debug if we're in dev mode
|
||||
if (dev) {
|
||||
|
||||
76
src/lib/metapixel/MetaPixel.svelte
Normal file
76
src/lib/metapixel/MetaPixel.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<!-- @component
|
||||
MetaPixel integrates the Meta (Facebook) Pixel into your Svelte application,
|
||||
allowing you to track page views and custom events while respecting user consent
|
||||
for tracking. The component manages the lifecycle of the Meta Pixel script and
|
||||
PixelControl interface.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import type { TrackingManager } from '../tracking.svelte.ts';
|
||||
import { onNavigate } from '$app/navigation';
|
||||
import { PixelControl, type PixelControlOptions } from './pixel-control.ts';
|
||||
import { ensureFbc, type EnsureFbcOptions } from './fbc.ts';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Tracking manager to handle user consent for tracking. If omitted
|
||||
* tracking is disabled by default until consent is granted via
|
||||
* PixelControl.grantConsent().
|
||||
*/
|
||||
trackingManager?: TrackingManager;
|
||||
|
||||
/** Meta Pixel ID */
|
||||
pixelID: string;
|
||||
|
||||
/** Meta Pixel Options */
|
||||
pixelOptions?: PixelControlOptions;
|
||||
|
||||
/**
|
||||
* Controls whether page views are automatically tracked by this
|
||||
* component (default: true).
|
||||
*/
|
||||
autoPageView?: boolean;
|
||||
}
|
||||
|
||||
let { pixelID, pixelOptions, autoPageView = true, trackingManager }: Props = $props();
|
||||
|
||||
let pixel = $state<PixelControl | null>(null);
|
||||
|
||||
const ensureFbcOptions: EnsureFbcOptions = $derived({
|
||||
sameSite: 'Lax',
|
||||
pixelLoaded: PixelControl.baseLoaded && !PixelControl.baseFailed
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (!trackingManager) {
|
||||
throw new Error('MetaPixel component requires a TrackingManager to manage consent.');
|
||||
}
|
||||
PixelControl.load();
|
||||
pixel = PixelControl.initialize(trackingManager, pixelID, pixelOptions);
|
||||
|
||||
trackingManager.runWithConsent(() => {
|
||||
if (autoPageView && pixel) {
|
||||
pixel.pageView();
|
||||
ensureFbc(ensureFbcOptions);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onNavigate(() => {
|
||||
trackingManager?.runWithConsent(() => {
|
||||
if (autoPageView && pixel) {
|
||||
pixel.pageView();
|
||||
ensureFbc(ensureFbcOptions);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export const getPixelControl = (): PixelControl => {
|
||||
if (!pixel) {
|
||||
throw new Error('MetaPixel component has not been initialized yet, wait for onMount.');
|
||||
}
|
||||
return pixel;
|
||||
};
|
||||
</script>
|
||||
88
src/lib/metapixel/fbc.ts
Normal file
88
src/lib/metapixel/fbc.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import log from 'loglevel';
|
||||
|
||||
export type EnsureFbcOptions = {
|
||||
days?: number; // cookie lifetime, default 180
|
||||
domain?: string; // optional cookie domain
|
||||
sameSite?: 'Lax' | 'Strict' | 'None'; // default Lax
|
||||
secure?: boolean; // default inferred from location.protocol === 'https:'
|
||||
pixelLoaded?: boolean; // if true, skip manual set and let Pixel handle it
|
||||
};
|
||||
|
||||
function getParam(name: string): string | undefined {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const v = params.get(name);
|
||||
return v || undefined;
|
||||
}
|
||||
|
||||
function getCookie(name: string): string | undefined {
|
||||
return document.cookie
|
||||
.split('; ')
|
||||
.find((c) => c.startsWith(name + '='))
|
||||
?.split('=')[1];
|
||||
}
|
||||
|
||||
function setCookie(
|
||||
name: string,
|
||||
value: string,
|
||||
{
|
||||
days = 180,
|
||||
domain,
|
||||
sameSite = 'Lax',
|
||||
secure
|
||||
}: Pick<EnsureFbcOptions, 'days' | 'domain' | 'sameSite' | 'secure'> = {}
|
||||
) {
|
||||
const d = new Date();
|
||||
d.setTime(d.getTime() + days * 864e5);
|
||||
const parts = [
|
||||
`${name}=${encodeURIComponent(value)}`,
|
||||
`expires=${d.toUTCString()}`,
|
||||
'path=/',
|
||||
`SameSite=${sameSite}`
|
||||
];
|
||||
if (domain) parts.push(`domain=${domain}`);
|
||||
const isSecure = secure ?? location.protocol === 'https:';
|
||||
if (isSecure) parts.push('Secure');
|
||||
document.cookie = parts.join('; ');
|
||||
}
|
||||
|
||||
function isValidFbc(value: string | undefined): boolean {
|
||||
if (!value) return false;
|
||||
// Expect "fb.1.<unix>.<fbclid>"
|
||||
if (!value.startsWith('fb.1.')) return false;
|
||||
const parts = value.split('.');
|
||||
return parts.length >= 4 && /^\d+$/.test(parts[2]) && parts[3].length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure _fbc cookie exists when landing URL contains fbclid.
|
||||
* Call after consent. If pixelLoaded is true, it skips manual setting.
|
||||
*/
|
||||
export function ensureFbc(options: EnsureFbcOptions = {}) {
|
||||
try {
|
||||
const { pixelLoaded = false, ...cookieOpts } = options;
|
||||
|
||||
if (pixelLoaded) throw new Error('Pixel loaded, skipping manual _fbc set'); // Let the Pixel set _fbc if it’s active
|
||||
|
||||
const fbclid = getParam('fbclid');
|
||||
if (!fbclid) throw new Error('No fbclid param present');
|
||||
|
||||
const existing = getCookie('_fbc');
|
||||
if (isValidFbc(existing)) throw new Error('_fbc cookie already present and valid');
|
||||
|
||||
const ts = Math.floor(Date.now() / 1000);
|
||||
const fbc = `fb.1.${ts}.${fbclid}`;
|
||||
setCookie('_fbc', fbc, cookieOpts);
|
||||
log.debug('[ensureFbc] Set _fbc cookie:', fbc);
|
||||
} catch (e) {
|
||||
log.debug('[ensureFbc]', (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to read both _fbp and _fbc for your CAPI payload.
|
||||
*/
|
||||
export function getFbpFbc(): { fbp?: string; fbc?: string } {
|
||||
const fbp = getCookie('_fbp');
|
||||
const fbc = getCookie('_fbc');
|
||||
return { fbp, fbc };
|
||||
}
|
||||
4
src/lib/metapixel/index.ts
Normal file
4
src/lib/metapixel/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as MetaPixel } from './MetaPixel.svelte';
|
||||
export { PixelControl, type PixelControlOptions } from './pixel-control.ts';
|
||||
export * from './fbc.ts';
|
||||
export * from './pixel-control.ts';
|
||||
320
src/lib/metapixel/pixel-control.ts
Normal file
320
src/lib/metapixel/pixel-control.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
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 = $state(false);
|
||||
private static _baseFailed: boolean = $state(false);
|
||||
private static _registeredPixels: Record<string, PixelControl> = {};
|
||||
|
||||
/** Indicates whether the Meta Pixel base script has been loaded. */
|
||||
static get baseLoaded(): boolean {
|
||||
return this._baseLoaded;
|
||||
}
|
||||
|
||||
/** Indicates whether the Meta Pixel base script has failed to load. */
|
||||
static get baseFailed(): boolean {
|
||||
return this._baseFailed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 async load() {
|
||||
if (this._baseLoaded && !this.baseFailed && !!window.fbq) return;
|
||||
if (!window.fbq) {
|
||||
try {
|
||||
await loadMetaPixel(); // Load the Meta Pixel script
|
||||
} catch (e) {
|
||||
log.warn('[PixelControl] Failed to load Meta Pixel script, all events will be queued.', e);
|
||||
this._baseFailed = true;
|
||||
return;
|
||||
} finally {
|
||||
this._baseLoaded = true;
|
||||
}
|
||||
}
|
||||
log.debug('[PixelControl] 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('[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.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/lib/types/conversion.d.ts
vendored
43
src/lib/types/conversion.d.ts
vendored
@@ -16,6 +16,47 @@ export type ConversionUserData = {
|
||||
externalId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Supported custom data fields for conversion events.
|
||||
*/
|
||||
export type ConversionEventParams = {
|
||||
value?: number;
|
||||
net_revenue?: number;
|
||||
currency?: string;
|
||||
content_name?: string;
|
||||
content_category?: string;
|
||||
content_ids?: string[];
|
||||
contents?: {
|
||||
id?: string;
|
||||
quantity?: number;
|
||||
item_price?: number;
|
||||
title?: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
brand?: string;
|
||||
delivery_category?: string;
|
||||
}[];
|
||||
content_type?: string;
|
||||
order_id?: string;
|
||||
predicted_ltv?: number;
|
||||
num_items?: number;
|
||||
search_string?: string;
|
||||
status?: string;
|
||||
item_number?: string;
|
||||
delivery_category?: string;
|
||||
custom_properties?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parameters for sending a conversion event to Meta Pixel.
|
||||
*/
|
||||
export type ConversionEventDetails = {
|
||||
eventID: string;
|
||||
actionSource: 'website' | 'app' | 'offline' | 'other';
|
||||
userData: ConversionUserData;
|
||||
eventSourceURL?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Request body for conversion event tracking.
|
||||
*/
|
||||
@@ -28,7 +69,7 @@ export type ConversionRequestBody = {
|
||||
Record<'utm_source' | 'utm_medium' | 'utm_campaign' | 'utm_content' | 'utm_term', string>
|
||||
>;
|
||||
user?: Omit<ConversionUserData, 'ip'>;
|
||||
customData?: Record<string, string>;
|
||||
customData?: ConversionEventParams;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
2
src/lib/types/fbq.d.ts
vendored
2
src/lib/types/fbq.d.ts
vendored
@@ -39,7 +39,7 @@ export type CommonParams = Partial<{
|
||||
/**
|
||||
* Product IDs associated with the event, such as SKUs (e.g. ["ABC123", "XYZ456"]).
|
||||
*/
|
||||
content_ids: (string | number)[];
|
||||
content_ids: string[];
|
||||
|
||||
/**
|
||||
* Name of the page/product.
|
||||
|
||||
Reference in New Issue
Block a user