refactor conversion API to avoid facebook-nodejs-business-sdk
uses direct meta graph endpoints instead.
This commit is contained in:
@@ -1,26 +1,29 @@
|
||||
import { getFbpFbc } from '../metapixel/fbc.ts';
|
||||
import type { TrackingManager } from '$lib/tracking.svelte';
|
||||
import type {
|
||||
ConversionErrorResponseBody,
|
||||
ConversionEventParams,
|
||||
ConversionRequestBody,
|
||||
ConversionResponseBody,
|
||||
ConversionUserData
|
||||
} from '$lib/types/conversion.js';
|
||||
import type { StandardEventName } from '$lib/types/fbq.js';
|
||||
import {
|
||||
capiErrorBodySchema,
|
||||
capiResponseBodySchema,
|
||||
type CAPIErrorBody,
|
||||
type CAPIRequestBody,
|
||||
type CAPIResponseBody
|
||||
} from './handle.ts';
|
||||
import type { CAPICustomerInfoParams, CAPIEvent } from './event.ts';
|
||||
import * as v from 'valibot';
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
/**
|
||||
* Client for sending conversion events to a server endpoint.
|
||||
* Client abstracts HTTP communication with a spectator server endpoint for
|
||||
* sending conversion events to Meta Conversions API.
|
||||
*/
|
||||
export class ConversionClient {
|
||||
export class CAPIClient {
|
||||
private _href: string;
|
||||
private _trackingManager: TrackingManager;
|
||||
|
||||
/**
|
||||
* Creates a new ConversionClient.
|
||||
* Creates a new CAPIClient.
|
||||
*
|
||||
* @param serverHref - The server endpoint URL.
|
||||
* @param trackingManager - The tracking manager instance.
|
||||
* @param serverHref - The spectator server endpoint URL.
|
||||
* @param trackingManager - The tracking manager instance, used for consent status.
|
||||
*/
|
||||
constructor(serverHref: string, trackingManager: TrackingManager) {
|
||||
this._href = serverHref;
|
||||
@@ -28,65 +31,64 @@ export class ConversionClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a conversion event to the server.
|
||||
*
|
||||
* @param eventName - The name of the standard event to send.
|
||||
* @param options - Additional options for the event.
|
||||
* @returns A promise that resolves to the event response or error.
|
||||
* Sends CAPIEvents to the server endpoint.
|
||||
* @param events - The array of CAPIEvents to send.
|
||||
* @returns A promise that resolves to the server response.
|
||||
* @throws Will throw an error if input or response shape validation fails.
|
||||
*/
|
||||
async trackEvent(
|
||||
eventName: StandardEventName,
|
||||
options: {
|
||||
eventID: string;
|
||||
user?: Omit<ConversionUserData, 'ip' | 'fbp' | 'fbc' | 'ua'>;
|
||||
customData?: ConversionEventParams;
|
||||
}
|
||||
): Promise<ConversionResponseBody | ConversionErrorResponseBody> {
|
||||
// Extract user data
|
||||
const { fbp, fbc } = getFbpFbc();
|
||||
|
||||
const user: ConversionUserData = {
|
||||
...options.user,
|
||||
fbp,
|
||||
fbc
|
||||
};
|
||||
|
||||
// Get event source URL & extract UTM params if present
|
||||
const eventSourceURL = window.location.href;
|
||||
const url = new URL(eventSourceURL);
|
||||
const utms: Record<string, string> = {};
|
||||
url.searchParams.forEach((value, key) => {
|
||||
if (key.startsWith('utm_')) {
|
||||
utms[key] = value;
|
||||
async sendEvents(events: CAPIEvent[]): Promise<CAPIResponseBody | CAPIErrorBody> {
|
||||
// Respond with an empty response if consent is not given
|
||||
if (!this._trackingManager.haveUserConsent()) {
|
||||
if (dev) {
|
||||
console.warn(`[CAPIClient] Consent not given. Skipping sending ${events.length} event(s).`);
|
||||
}
|
||||
});
|
||||
return {
|
||||
pixelID: '',
|
||||
fbTraceID: '',
|
||||
receivedEvents: 0,
|
||||
processedEvents: 0,
|
||||
messages: []
|
||||
};
|
||||
}
|
||||
|
||||
// Attempt to build enriched user data
|
||||
const { fbp, fbc } = getFbpFbc();
|
||||
const enrichedUserData: Partial<CAPICustomerInfoParams> = { fbp, fbc };
|
||||
|
||||
// Build request body
|
||||
const requestBody: ConversionRequestBody = {
|
||||
consent: this._trackingManager.consent === true,
|
||||
eventName,
|
||||
eventID: options.eventID,
|
||||
user,
|
||||
eventSourceURL,
|
||||
utms,
|
||||
customData: options.customData
|
||||
const body: CAPIRequestBody = {
|
||||
events: events.map((e) => {
|
||||
e.enrichUserData(enrichedUserData);
|
||||
return e.toObject();
|
||||
})
|
||||
};
|
||||
v.parse(v.object({ events: v.array(v.any()) }), body); // Validate body shape
|
||||
|
||||
// Send request to server
|
||||
const response = await fetch(this._href, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const json = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as ConversionResponseBody;
|
||||
return data;
|
||||
const parsed = v.parse(capiResponseBodySchema, json);
|
||||
return parsed as CAPIResponseBody;
|
||||
} else {
|
||||
const errorData = (await response.json()) as ConversionErrorResponseBody;
|
||||
return errorData;
|
||||
const parsed = v.parse(capiErrorBodySchema, json);
|
||||
return parsed as CAPIErrorBody;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand for sending a single CAPIEvent to the server endpoint.
|
||||
* @param event - The CAPIEvent to send.
|
||||
* @returns A promise that resolves to the server response.
|
||||
* @throws Will throw an error if input or response shape validation fails.
|
||||
*/
|
||||
async trackEvent(event: CAPIEvent): Promise<CAPIResponseBody | CAPIErrorBody> {
|
||||
return this.sendEvents([event]);
|
||||
}
|
||||
}
|
||||
|
||||
97
src/lib/conversion/connector.ts
Normal file
97
src/lib/conversion/connector.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { dev } from '$app/environment';
|
||||
import log from 'loglevel';
|
||||
import type { CAPIEvent } from './event.ts';
|
||||
|
||||
const GRAPH_VERSION = 'v24.0';
|
||||
|
||||
/**
|
||||
* Response body from Meta Conversion API after sending events.
|
||||
*/
|
||||
export type CAPIRequestResponseBody = {
|
||||
/** Dataset or Pixel ID to which the event successfully posted. */
|
||||
pixelID: string;
|
||||
/** fbtrace_id for debugging purposes. */
|
||||
fbtrace_id: string;
|
||||
/** Number of events received that were sent by the request. */
|
||||
receivedEvents: number;
|
||||
/** Number of events successfully posted by the request. */
|
||||
processedEvents: number;
|
||||
/** Messages returned by the server. */
|
||||
messages: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Connector class for Meta Conversion API (CAPI). Abstraction over direct HTTP
|
||||
* requests to Meta's CAPI endpoint.
|
||||
*
|
||||
* See https://developers.facebook.com/docs/marketing-api/conversions-api/get-started
|
||||
* for more information.
|
||||
*/
|
||||
export class CAPIConnector {
|
||||
private _accessToken: string;
|
||||
private _pixelID: string;
|
||||
private _testEventCode?: string;
|
||||
|
||||
/**
|
||||
* Creates a new MCAPIControl instance.
|
||||
*
|
||||
* @param accessToken - Your Meta Pixel Conversion API access token.
|
||||
* @param pixelID - Your Meta Pixel ID.
|
||||
* @param testEventCode - Optional test event code used for all events if provided.
|
||||
*/
|
||||
constructor(accessToken: string, pixelID: string, testEventCode?: string) {
|
||||
this._accessToken = accessToken;
|
||||
this._pixelID = pixelID;
|
||||
this._testEventCode = testEventCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends conversion events to the Meta Conversion API.
|
||||
*
|
||||
* @param events - Array of CAPIEvent instances to send.
|
||||
* @returns The response from the Meta CAPI.
|
||||
* @throws Will throw an error if the request fails or the API returns an error.
|
||||
*/
|
||||
async sendEvents(events: CAPIEvent[]): Promise<CAPIRequestResponseBody> {
|
||||
if (dev && !this._testEventCode) {
|
||||
log.warn(
|
||||
`[CAPIConnector] Sending ${events.length} event(s) in dev mode without a test event code. ` +
|
||||
'Consider providing a test event code to avoid affecting real data.'
|
||||
);
|
||||
}
|
||||
|
||||
const url = `https://graph.facebook.com/${GRAPH_VERSION}/${this._pixelID}/events?access_token=${this._accessToken}`;
|
||||
|
||||
const body = {
|
||||
data: events.map((e) => e.toObject()),
|
||||
test_event_code: this._testEventCode
|
||||
};
|
||||
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const json = await resp.json();
|
||||
console.log('CAPI response:', json);
|
||||
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Meta CAPI error ${resp.status}: ${JSON.stringify(json, null, 2)}`);
|
||||
}
|
||||
|
||||
return {} as CAPIRequestResponseBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand for sending a single event to the Meta Conversion API.
|
||||
*
|
||||
* @param event - The CAPIEvent instance to send.
|
||||
* @returns The response from the Meta CAPI.
|
||||
* @throws Will throw an error if the request fails or the API returns an error.
|
||||
*/
|
||||
async trackEvent(event: CAPIEvent): Promise<CAPIRequestResponseBody> {
|
||||
return this.sendEvents([event]);
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
import {
|
||||
Content,
|
||||
CustomData,
|
||||
EventRequest,
|
||||
ServerEvent,
|
||||
UserData
|
||||
} from 'facebook-nodejs-business-sdk';
|
||||
import type { StandardEventName } from '../types/fbq.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.
|
||||
*
|
||||
* @param data - The user data to include.
|
||||
* @returns The constructed UserData object.
|
||||
*/
|
||||
const buildUserData = (data: ConversionUserData): UserData => {
|
||||
const userData = new UserData();
|
||||
if (data.email) userData.setEmail(data.email);
|
||||
if (data.phone) userData.setPhone(data.phone);
|
||||
if (data.firstName) userData.setFirstName(data.firstName);
|
||||
if (data.lastName) userData.setLastName(data.lastName);
|
||||
if (data.ip) userData.setClientIpAddress(data.ip);
|
||||
if (data.fbp) userData.setFbp(data.fbp);
|
||||
if (data.fbc) userData.setFbc(data.fbc);
|
||||
if (data.ua) userData.setClientUserAgent(data.ua);
|
||||
if (data.externalId) userData.setExternalId(data.externalId);
|
||||
return userData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds CustomData for conversion events.
|
||||
*
|
||||
* @param params - The custom data parameters.
|
||||
* @returns The constructed CustomData object.
|
||||
*/
|
||||
const buildCustomData = (params: ConversionEventParams): CustomData => {
|
||||
const c = new CustomData();
|
||||
if (params.value) c.setValue(params.value);
|
||||
if (params.net_revenue) c.setNetRevenue(params.net_revenue);
|
||||
if (params.currency) c.setCurrency(params.currency);
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Control class for sending Conversion API events to Meta Pixel.
|
||||
*/
|
||||
export class ConversionControl {
|
||||
private _accessToken: string;
|
||||
private _pixelID: string;
|
||||
private _testEventCode?: string;
|
||||
|
||||
/**
|
||||
* Creates a new ConversionControl instance.
|
||||
*
|
||||
* @param accessToken - Your Meta Pixel Conversion API access token.
|
||||
* @param pixelID - Your Meta Pixel ID.
|
||||
* @param testEventCode - Optional test event code for testing events.
|
||||
*/
|
||||
constructor(accessToken: string, pixelID: string, testEventCode?: string) {
|
||||
this._accessToken = accessToken;
|
||||
this._pixelID = pixelID;
|
||||
this._testEventCode = testEventCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @returns A promise that resolves to the event response.
|
||||
*/
|
||||
async trackEvent(
|
||||
eventName: StandardEventName,
|
||||
details: ConversionEventDetails,
|
||||
params?: ConversionEventParams
|
||||
): Promise<ConversionResponseBody> {
|
||||
const event = new ServerEvent()
|
||||
.setEventName(eventName)
|
||||
.setEventTime(Math.floor(Date.now() / 1000))
|
||||
.setUserData(buildUserData(details.userData))
|
||||
.setActionSource(details.actionSource);
|
||||
|
||||
if (details.eventID) event.setEventId(details.eventID);
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
329
src/lib/conversion/event.ts
Normal file
329
src/lib/conversion/event.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
import type { StandardEventName } from '../types/fbq.js';
|
||||
import * as v from 'valibot';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const sha256 = (value: string): string => {
|
||||
return crypto.createHash('sha256').update(value).digest('hex');
|
||||
};
|
||||
|
||||
const normPhone = (s?: string | null) => {
|
||||
// E.164-ish: keep digits, include leading + if present
|
||||
if (!s) return undefined;
|
||||
const plus = s.trim().startsWith('+');
|
||||
const digits = s.replace(/[^\d]/g, '');
|
||||
return plus ? `+${digits}` : digits;
|
||||
};
|
||||
|
||||
const trimPunctuation = (s: string) => {
|
||||
return s.replace(/^[\p{P}\p{S}]+|[\p{P}\p{S}]+$/gu, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Supported user data fields for conversion events.
|
||||
*
|
||||
* See https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters
|
||||
* for an exhaustive list of Facebook's supported user data parameters. Mappings
|
||||
* to Meta's expected keys are handled under the hood by CAPIEvent.
|
||||
*/
|
||||
export type CAPICustomerInfoParams = {
|
||||
/** Email address (maps to `em`). */
|
||||
email?: string;
|
||||
/** Phone number (maps to `ph`). */
|
||||
phone?: string;
|
||||
/** First name (maps to `fn`). */
|
||||
firstName?: string;
|
||||
/** Last name (maps to `ln`). */
|
||||
lastName?: string;
|
||||
/** Client IP address (maps to `client_ip_address`). */
|
||||
clientIP?: string;
|
||||
/** Client user agent (maps to `client_user_agent`). */
|
||||
clientUserAgent: string;
|
||||
/** Facebook browser pixel ID (_fbp cookie). */
|
||||
fbp?: string;
|
||||
/** Facebook click ID (_fbc cookie). */
|
||||
fbc?: string;
|
||||
/** External ID (maps to `external_id`). */
|
||||
externalID?: string;
|
||||
};
|
||||
|
||||
/** valibot schema for meta's customer information parameters */
|
||||
const metaCustomerInfoParamsSchema = v.object({
|
||||
em: v.optional(v.string()),
|
||||
ph: v.optional(v.string()),
|
||||
fn: v.optional(v.string()),
|
||||
ln: v.optional(v.string()),
|
||||
client_ip_address: v.optional(v.string()),
|
||||
client_user_agent: v.string(),
|
||||
fbp: v.optional(v.string()),
|
||||
fbc: v.optional(v.string()),
|
||||
external_id: v.optional(v.string())
|
||||
});
|
||||
|
||||
/** Meta's customer information parameters with annoyingly abbreviated keys. */
|
||||
type meta_customerInfoParams = v.InferOutput<typeof metaCustomerInfoParamsSchema>;
|
||||
|
||||
/** Maps our customer information parameters to Meta's abbreviated keys. */
|
||||
const _meta_customInfoParams_Map: Record<
|
||||
keyof CAPICustomerInfoParams,
|
||||
keyof meta_customerInfoParams
|
||||
> = {
|
||||
email: 'em',
|
||||
phone: 'ph',
|
||||
firstName: 'fn',
|
||||
lastName: 'ln',
|
||||
clientIP: 'client_ip_address',
|
||||
clientUserAgent: 'client_user_agent',
|
||||
fbp: 'fbp',
|
||||
fbc: 'fbc',
|
||||
externalID: 'external_id'
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps CAPICustomerInfoParams to meta_customerInfoParams by transforming keys.
|
||||
* Transforms values as needed (e.g. hashing email/phone).
|
||||
*
|
||||
* WARNING: This function is unsafe and does not perform shape validation; ensure
|
||||
* input shape is valid before consuming.
|
||||
*
|
||||
* @param data - The CAPICustomerInfoParams to map.
|
||||
* @returns The mapped meta_customerInfoParams.
|
||||
*/
|
||||
const mapCustomerInfoToMeta = (data: Partial<CAPICustomerInfoParams>) => {
|
||||
const dst = {} as meta_customerInfoParams; // unsafe
|
||||
for (const key in data) {
|
||||
const shortKey = _meta_customInfoParams_Map[key as keyof CAPICustomerInfoParams];
|
||||
if (shortKey && data[key as keyof CAPICustomerInfoParams]) {
|
||||
dst[shortKey] = data[key as keyof CAPICustomerInfoParams] as string;
|
||||
}
|
||||
}
|
||||
|
||||
// Transform values as needed
|
||||
|
||||
if (dst.em) {
|
||||
dst.em = sha256(dst.em.trim().toLowerCase());
|
||||
}
|
||||
if (dst.ph) {
|
||||
const normed = normPhone(dst.ph);
|
||||
dst.ph = normed ? sha256(normed) : normed;
|
||||
}
|
||||
if (dst.fn) {
|
||||
dst.fn = sha256(trimPunctuation(dst.fn.trim().toLowerCase()));
|
||||
}
|
||||
if (dst.ln) {
|
||||
dst.ln = sha256(trimPunctuation(dst.ln.trim().toLowerCase()));
|
||||
}
|
||||
|
||||
return dst;
|
||||
};
|
||||
|
||||
/** valibot schema for standard parameters / custom data */
|
||||
const standardParamsSchema = v.object({
|
||||
value: v.optional(v.number()),
|
||||
net_revenue: v.optional(v.number()),
|
||||
currency: v.optional(v.string()),
|
||||
content_name: v.optional(v.string()),
|
||||
content_category: v.optional(v.string()),
|
||||
content_ids: v.optional(v.array(v.string())),
|
||||
contents: v.optional(
|
||||
v.array(
|
||||
v.object({
|
||||
id: v.optional(v.string()),
|
||||
quantity: v.optional(v.number()),
|
||||
item_price: v.optional(v.number()),
|
||||
title: v.optional(v.string()),
|
||||
description: v.optional(v.string()),
|
||||
category: v.optional(v.string()),
|
||||
brand: v.optional(v.string()),
|
||||
delivery_category: v.optional(v.string())
|
||||
})
|
||||
)
|
||||
),
|
||||
content_type: v.optional(v.string()),
|
||||
order_id: v.optional(v.string()),
|
||||
predicted_ltv: v.optional(v.number()),
|
||||
num_items: v.optional(v.number()),
|
||||
search_string: v.optional(v.string()),
|
||||
status: v.optional(v.string()),
|
||||
item_number: v.optional(v.string()),
|
||||
delivery_category: v.optional(v.string()),
|
||||
custom_properties: v.optional(v.record(v.string(), v.unknown()))
|
||||
});
|
||||
|
||||
/**
|
||||
* Supported standard data fields typically used as custom data parameters
|
||||
* for CAPI/Pixel events.
|
||||
*
|
||||
* See https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/custom-data
|
||||
* for an exhaustive list of Facebook's supported standard event parameters.
|
||||
*/
|
||||
export type CAPIStandardParams = v.InferOutput<typeof standardParamsSchema>;
|
||||
|
||||
/** Supported action sources for conversion events. */
|
||||
export enum ActionSource {
|
||||
Email = 'email',
|
||||
Website = 'website',
|
||||
App = 'app',
|
||||
PhoneCall = 'phone_call',
|
||||
Chat = 'chat',
|
||||
PhysicalStore = 'physical_store',
|
||||
SystemGenerated = 'system_generated',
|
||||
BusinessMessaging = 'business_messaging',
|
||||
Other = 'other'
|
||||
}
|
||||
|
||||
/** valibot schema for server event parameters, sent directly as request */
|
||||
export const MetaServerEventParamsSchema = v.object({
|
||||
event_name: v.string(),
|
||||
event_time: v.number(),
|
||||
user_data: metaCustomerInfoParamsSchema,
|
||||
custom_data: v.optional(standardParamsSchema),
|
||||
event_source_url: v.optional(v.string()),
|
||||
opt_out: v.optional(v.boolean()),
|
||||
event_id: v.optional(v.string()),
|
||||
action_source: v.enum(ActionSource)
|
||||
});
|
||||
|
||||
/**
|
||||
* Internal type representing validated server event parameters, suitable to
|
||||
* POST directly to the Meta Conversions API.
|
||||
*
|
||||
* See https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/server-event
|
||||
* for an exhaustive list of Facebook's supported server event parameters.
|
||||
*/
|
||||
export type MetaServerEventParams = v.InferOutput<typeof MetaServerEventParamsSchema>;
|
||||
|
||||
/**
|
||||
* Options for creating a CAPIEvent. All parameters are validated and
|
||||
* transformed to Meta's expected keys by CAPIEvent.
|
||||
*/
|
||||
export type CAPIEventOptions = {
|
||||
eventName: StandardEventName | string;
|
||||
/** Will be set to the current date and time if not provided */
|
||||
eventTime?: Date;
|
||||
userData: CAPICustomerInfoParams;
|
||||
customData?: CAPIStandardParams;
|
||||
/** Required if actionSource is set to 'website' */
|
||||
eventSourceURL?: string;
|
||||
optOut?: boolean;
|
||||
eventID?: string;
|
||||
actionSource: ActionSource;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a Meta Conversions API event with properly mapped parameters
|
||||
* and input validation.
|
||||
*/
|
||||
export class CAPIEvent {
|
||||
private _params?: MetaServerEventParams;
|
||||
/** Returns Meta-compliant server event object. */
|
||||
get params() {
|
||||
return this._params;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Creates a new CAPIEvent instance with the given options and generates a
|
||||
* Meta-compliant event object.
|
||||
*
|
||||
* @param opts - The options for creating the CAPIEvent.
|
||||
* @return The CAPIEvent instance.
|
||||
* @throws Will throw an error if option shape validation fails.
|
||||
*/
|
||||
static fromOpts(opts: CAPIEventOptions) {
|
||||
const event = new CAPIEvent();
|
||||
|
||||
// Transform our customer info params to Meta's expected keys
|
||||
const meta_customerInfo = mapCustomerInfoToMeta(opts.userData);
|
||||
|
||||
// Build event params & validate
|
||||
event._params = {
|
||||
event_name: opts.eventName,
|
||||
event_time: Math.floor((opts.eventTime ?? new Date()).getTime() / 1000),
|
||||
user_data: meta_customerInfo,
|
||||
custom_data: opts.customData,
|
||||
event_source_url: opts.eventSourceURL,
|
||||
opt_out: opts.optOut,
|
||||
event_id: opts.eventID,
|
||||
action_source: opts.actionSource
|
||||
};
|
||||
|
||||
event._params = v.parse(MetaServerEventParamsSchema, event._params);
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmashals a JSON string or object into a CAPIEvent instance and parses
|
||||
* its parameters to match Meta's expected shape.
|
||||
*
|
||||
* @param src - The JSON string to unmarshal.
|
||||
* @returns The CAPIEvent instance.
|
||||
* @throws Will throw an error if the JSON is invalid or fails validation.
|
||||
*/
|
||||
private static fromSrc(src: string | object): CAPIEvent {
|
||||
const obj = typeof src === 'string' ? JSON.parse(src) : src;
|
||||
const parsed = v.parse(MetaServerEventParamsSchema, obj);
|
||||
const event = new CAPIEvent();
|
||||
event._params = parsed;
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmarshals a JSON string into a CAPIEvent instance and parses its
|
||||
* parameters to match Meta's expected shape.
|
||||
*
|
||||
* @param str - The JSON string to unmarshal.
|
||||
* @returns The CAPIEvent instance.
|
||||
* @throws Will throw an error if the JSON is invalid or fails validation.
|
||||
*/
|
||||
static fromJSON(str: string): CAPIEvent {
|
||||
return CAPIEvent.fromSrc(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmarshals a plain object into a CAPIEvent instance and parses its
|
||||
* parameters to match Meta's expected shape.
|
||||
*
|
||||
* @param obj - The object to unmarshal.
|
||||
* @returns The CAPIEvent instance.
|
||||
* @throws Will throw an error if the object fails validation.
|
||||
*/
|
||||
static fromObject(obj: object): CAPIEvent {
|
||||
return CAPIEvent.fromSrc(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshals the CAPIEvent to a JSON string suitable for sending to the Meta CAPI.
|
||||
* @returns The JSON string representation of the CAPIEvent.
|
||||
*/
|
||||
toJSON(): string {
|
||||
return JSON.stringify(this._params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshals the CAPIEvent to a plain object suitable for sending to the Meta CAPI.
|
||||
* @returns The object representation of the CAPIEvent.
|
||||
*/
|
||||
toObject(): MetaServerEventParams {
|
||||
return this._params as MetaServerEventParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enriches the CAPIEvent with additional user data.
|
||||
* @param additionalData - Additional user data to merge.
|
||||
* @throws Will throw an error if the enriched data fails validation.
|
||||
*/
|
||||
enrichUserData(additionalData: Partial<CAPICustomerInfoParams>) {
|
||||
if (!this._params) return;
|
||||
const additionalMetaData = mapCustomerInfoToMeta(additionalData);
|
||||
|
||||
// Merge additional data, only overwriting if no previous value exists
|
||||
this._params.user_data = {
|
||||
...additionalMetaData,
|
||||
...this._params.user_data
|
||||
};
|
||||
|
||||
// Re-validate after enrichment
|
||||
this._params = v.parse(MetaServerEventParamsSchema, this._params);
|
||||
}
|
||||
}
|
||||
76
src/lib/conversion/handle.ts
Normal file
76
src/lib/conversion/handle.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
import { getFbpFbcFromCookies } from '../metapixel/fbc.ts';
|
||||
import type { CAPIConnector } from './connector.ts';
|
||||
import { getRequestIP } from '../util/ip.ts';
|
||||
import * as v from 'valibot';
|
||||
import { CAPIEvent, MetaServerEventParamsSchema, type CAPICustomerInfoParams } from './event.ts';
|
||||
|
||||
export const capiRequestBodySchema = v.object({
|
||||
events: v.array(MetaServerEventParamsSchema)
|
||||
});
|
||||
|
||||
/** Request body for conversion events */
|
||||
export type CAPIRequestBody = v.InferOutput<typeof capiRequestBodySchema>;
|
||||
|
||||
export const capiErrorBodySchema = v.object({
|
||||
error: v.string()
|
||||
});
|
||||
|
||||
/** Returned by the conversion request handler in case of an error */
|
||||
export type CAPIErrorBody = v.InferOutput<typeof capiErrorBodySchema>;
|
||||
|
||||
export const capiResponseBodySchema = v.object({
|
||||
pixelID: v.string(),
|
||||
fbTraceID: v.string(),
|
||||
receivedEvents: v.number(),
|
||||
processedEvents: v.number(),
|
||||
messages: v.array(v.string())
|
||||
});
|
||||
|
||||
/** Returned by the conversion request handler in case of a successful response */
|
||||
export type CAPIResponseBody = v.InferOutput<typeof capiResponseBodySchema>;
|
||||
|
||||
/**
|
||||
* Creates a SvelteKit request handler for processing conversion events.
|
||||
*
|
||||
* @param connector - The CAPIConnector instance to send events through.
|
||||
* @returns A SvelteKit RequestHandler function.
|
||||
*/
|
||||
export const createCAPIHandler: (connector: CAPIConnector) => RequestHandler = (connector) => {
|
||||
const handle: RequestHandler = async ({ request, getClientAddress, cookies }) => {
|
||||
try {
|
||||
const json = await request.json();
|
||||
const parsed = v.parse(capiRequestBodySchema, json);
|
||||
|
||||
// Build enriched user data with IP, user agent, and fbp/fbc from cookies
|
||||
const ip = getRequestIP(request, getClientAddress);
|
||||
const ua = request.headers.get('user-agent') ?? undefined;
|
||||
const { fbp, fbc } = getFbpFbcFromCookies(cookies);
|
||||
|
||||
const enrichedUserData: Partial<CAPICustomerInfoParams> = {
|
||||
clientIP: ip,
|
||||
clientUserAgent: ua,
|
||||
fbp,
|
||||
fbc
|
||||
};
|
||||
|
||||
// Enrich each event's user data
|
||||
const events: CAPIEvent[] = parsed.events.map((eventParams) => {
|
||||
const event = CAPIEvent.fromObject(eventParams);
|
||||
event.enrichUserData(enrichedUserData);
|
||||
return event;
|
||||
});
|
||||
|
||||
// Send the event via the control
|
||||
const response = await connector.sendEvents(events);
|
||||
return json(response, { status: StatusCodes.OK });
|
||||
} catch (e) {
|
||||
const response: CAPIErrorBody = { error: e instanceof Error ? e.message : String(e) };
|
||||
return json(response, {
|
||||
status: StatusCodes.INTERNAL_SERVER_ERROR
|
||||
});
|
||||
}
|
||||
};
|
||||
return handle;
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './event.ts';
|
||||
export * from './connector.ts';
|
||||
export * from './handle.ts';
|
||||
export * from './client.ts';
|
||||
export * from './server.ts';
|
||||
export * from './control.ts';
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { ConversionControl } from './control.ts';
|
||||
import type {
|
||||
ConversionErrorResponseBody,
|
||||
ConversionEventParams,
|
||||
ConversionRequestBody,
|
||||
ConversionUserData
|
||||
} from '$lib/types/conversion.js';
|
||||
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
import { getFbpFbcFromCookies } from '../metapixel/fbc.ts';
|
||||
|
||||
export const getRequestIP = (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
|
||||
) => {
|
||||
const handle: RequestHandler = async ({ request, getClientAddress, cookies }) => {
|
||||
try {
|
||||
const body = (await request.json()) as ConversionRequestBody;
|
||||
|
||||
// Build user data with IP and user agent
|
||||
const ip = getRequestIP(request, getClientAddress);
|
||||
const ua = request.headers.get('user-agent');
|
||||
const { fbp, fbc } = getFbpFbcFromCookies(cookies);
|
||||
|
||||
const userData: ConversionUserData = {
|
||||
...body.user,
|
||||
ip,
|
||||
ua: ua ?? body.user?.ua ?? undefined,
|
||||
fbc: body.user?.fbc ?? fbc,
|
||||
fbp: body.user?.fbp ?? fbp
|
||||
};
|
||||
|
||||
// Build custom data with UTM params if applicable
|
||||
let customData: ConversionEventParams = body.customData ?? {};
|
||||
if (body.eventName === 'PageView' && body.utms) {
|
||||
// For PageView events, automatically include UTM params if provided
|
||||
customData = {
|
||||
...customData,
|
||||
...body.utms
|
||||
};
|
||||
}
|
||||
|
||||
// Send the event via the control
|
||||
const response = await control.trackEvent(
|
||||
body.eventName,
|
||||
{
|
||||
eventID: body.eventID,
|
||||
eventSourceURL: body.eventSourceURL,
|
||||
actionSource: 'website',
|
||||
userData
|
||||
},
|
||||
customData
|
||||
);
|
||||
|
||||
return json(response, { status: StatusCodes.OK });
|
||||
} catch (e) {
|
||||
return json(
|
||||
{ error: e instanceof Error ? e.message : String(e) } as ConversionErrorResponseBody,
|
||||
{ status: StatusCodes.INTERNAL_SERVER_ERROR }
|
||||
);
|
||||
}
|
||||
};
|
||||
return handle;
|
||||
};
|
||||
@@ -11,18 +11,18 @@ 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';
|
||||
import { CAPIClient } from '../conversion/client.ts';
|
||||
import { ActionSource, CAPIEvent, type CAPIStandardParams } from '../conversion/event.ts';
|
||||
|
||||
const pixelParamsToCustomData = (params: CommonParams & CustomParams): ConversionEventParams => {
|
||||
const customData: ConversionEventParams = {};
|
||||
const pixelParamsToCustomData = (params: CommonParams & CustomParams): CAPIStandardParams => {
|
||||
const customData: CAPIStandardParams = {};
|
||||
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'] = [];
|
||||
const acc: CAPIStandardParams['contents'] = [];
|
||||
customData.contents = params.contents.reduce((acc, content) => {
|
||||
acc.push({
|
||||
id: content.id.toString(),
|
||||
@@ -66,7 +66,7 @@ export class PixelControl {
|
||||
private _pixelID: string;
|
||||
private _testEventCode?: string = undefined;
|
||||
private _trackingManager: MaybeGetter<TrackingManager | undefined>;
|
||||
private _conversionClient?: ConversionClient = undefined;
|
||||
private _conversionClient?: CAPIClient = undefined;
|
||||
|
||||
private static _baseLoaded: boolean = false;
|
||||
private static _registeredPixels: Record<string, PixelControl> = {};
|
||||
@@ -98,10 +98,7 @@ export class PixelControl {
|
||||
|
||||
const resolvedTrackingManager = resolveGetter(trackingManager);
|
||||
if (options?.conversionHref && resolvedTrackingManager) {
|
||||
this._conversionClient = new ConversionClient(
|
||||
options.conversionHref,
|
||||
resolvedTrackingManager
|
||||
);
|
||||
this._conversionClient = new CAPIClient(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.`
|
||||
@@ -196,48 +193,61 @@ export class PixelControl {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a PageView event
|
||||
* Shorthand utility to send a PageView event
|
||||
* @param disableCAPI If true, disables sending this event to the Conversion API
|
||||
* @throws Error if the Meta Pixel is not initialized.
|
||||
*/
|
||||
pageView(disableCAPI: boolean = false) {
|
||||
if (!this.consentGuard()) return;
|
||||
this.track('PageView', undefined, undefined, disableCAPI);
|
||||
}
|
||||
|
||||
let eventID: string | undefined = undefined;
|
||||
// Optionally, send to conversion API endpoint if configured
|
||||
if (this._conversionClient && !disableCAPI) {
|
||||
/**
|
||||
* Forwards an event to the Conversion API client if configured.
|
||||
*
|
||||
* @param event - The event name.
|
||||
* @param params - The event parameters.
|
||||
* @param eventID - Optional event ID for deduplication.
|
||||
* @returns The event ID used, either provided or generated.
|
||||
*/
|
||||
private forwardToCAPI(
|
||||
event: StandardEventName | string,
|
||||
params?: CommonParams & CustomParams,
|
||||
eventID?: string
|
||||
): string | undefined {
|
||||
if (!this._conversionClient) return eventID;
|
||||
|
||||
if (!eventID) {
|
||||
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
|
||||
this._conversionClient
|
||||
.trackEvent(
|
||||
CAPIEvent.fromOpts({
|
||||
eventName: event,
|
||||
eventID: eventID,
|
||||
actionSource: ActionSource.Website,
|
||||
eventTime: new Date(),
|
||||
userData: {
|
||||
clientUserAgent: navigator.userAgent
|
||||
},
|
||||
customData: params ? pixelParamsToCustomData(params) : undefined
|
||||
})
|
||||
)
|
||||
.then((response) => {
|
||||
log.debug(
|
||||
`[PixelControl] [${this._pixelID}] ${event} event sent to Conversion API with Event ID: ${eventID}, Response: ${JSON.stringify(
|
||||
response,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error(
|
||||
`[PixelControl] [${this._pixelID}] Failed to send ${event} event to Conversion API with Event ID: ${eventID}`,
|
||||
error
|
||||
);
|
||||
});
|
||||
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.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -256,27 +266,12 @@ export class PixelControl {
|
||||
) {
|
||||
if (!this.consentGuard()) return;
|
||||
|
||||
// Optionally, send to conversion API endpoint if configured
|
||||
if (this._conversionClient && !disableCAPI) {
|
||||
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
|
||||
);
|
||||
});
|
||||
// Optionally, send to conversion API endpoint
|
||||
if (!disableCAPI) {
|
||||
eventID = this.forwardToCAPI(event, params, eventID);
|
||||
}
|
||||
|
||||
// Send the PageView event to Meta
|
||||
// Send the event to Meta via the pixel
|
||||
if (!dev || this._testEventCode) {
|
||||
window.fbq('trackSingle', this._pixelID, event, params, {
|
||||
eventID,
|
||||
@@ -294,10 +289,26 @@ export class PixelControl {
|
||||
|
||||
/**
|
||||
* Tracks a custom event for this pixel (uses `trackSingleCustom` under the hood)
|
||||
* @param event Custom event name
|
||||
* @param params Event parameters
|
||||
* @param eventID Optional event ID for deduplication with Conversion API
|
||||
* @param disableCAPI If true, disables sending this event to the Conversion API
|
||||
* @throws Error if the Meta Pixel is not initialized.
|
||||
*/
|
||||
trackCustom(event: string, params?: CommonParams & CustomParams, eventID?: string) {
|
||||
trackCustom(
|
||||
event: string,
|
||||
params?: CommonParams & CustomParams,
|
||||
eventID?: string,
|
||||
disableCAPI: boolean = false
|
||||
) {
|
||||
if (!this.consentGuard()) return;
|
||||
|
||||
// Optionally, send to conversdion API endpoint
|
||||
if (!disableCAPI) {
|
||||
eventID = this.forwardToCAPI(event, params, eventID);
|
||||
}
|
||||
|
||||
// Send the event to Meta via the pixel
|
||||
if (!dev || this._testEventCode) {
|
||||
window.fbq('trackSingleCustom', this._pixelID, event, params, {
|
||||
eventID,
|
||||
|
||||
94
src/lib/types/conversion.d.ts
vendored
94
src/lib/types/conversion.d.ts
vendored
@@ -1,94 +0,0 @@
|
||||
import type { StandardEventName } from '../types/fbq.js';
|
||||
|
||||
/**
|
||||
* Supported user data fields for conversion events.
|
||||
*/
|
||||
export type ConversionUserData = {
|
||||
email?: string;
|
||||
phone?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
ip?: string;
|
||||
fbp?: string;
|
||||
fbc?: string;
|
||||
/** user agent */
|
||||
ua?: string;
|
||||
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.
|
||||
*/
|
||||
export type ConversionRequestBody = {
|
||||
consent: boolean;
|
||||
eventName: StandardEventName;
|
||||
eventID: string;
|
||||
eventSourceURL?: string;
|
||||
utms?: Partial<
|
||||
Record<'utm_source' | 'utm_medium' | 'utm_campaign' | 'utm_content' | 'utm_term', string>
|
||||
>;
|
||||
user?: Omit<ConversionUserData, 'ip'>;
|
||||
customData?: ConversionEventParams;
|
||||
};
|
||||
|
||||
/**
|
||||
* Response body for conversion event tracking.
|
||||
*/
|
||||
export type ConversionResponseBody = {
|
||||
/** Dataset or Pixel ID to which the event successfully posted. */
|
||||
pixelID: string;
|
||||
/** fbtrace_id for debugging purposes. */
|
||||
fbtrace_id: string;
|
||||
/** Number of events received that were sent by the request. */
|
||||
receivedEvents: number;
|
||||
/** Number of events successfully posted by the request. */
|
||||
processedEvents: number;
|
||||
/** Messages returned by the server. */
|
||||
messages: string[];
|
||||
};
|
||||
|
||||
/** Error response body for conversion event tracking. */
|
||||
export type ConversionErrorResponseBody = {
|
||||
error: string;
|
||||
};
|
||||
18
src/lib/types/fbq.d.ts
vendored
18
src/lib/types/fbq.d.ts
vendored
@@ -230,27 +230,27 @@ export type StandardEventName =
|
||||
*/
|
||||
export type AdvancedMatching = {
|
||||
/** Primary contact email (or hashed email) */
|
||||
email?: string;
|
||||
em?: string;
|
||||
/** Phone number (E.164 or local) */
|
||||
phone?: string;
|
||||
ph?: string;
|
||||
/** First name */
|
||||
first_name?: string;
|
||||
fn?: string;
|
||||
/** Last name */
|
||||
last_name?: string;
|
||||
ln?: string;
|
||||
/** City */
|
||||
city?: string;
|
||||
ct?: string;
|
||||
/** State/region */
|
||||
state?: string;
|
||||
st?: string;
|
||||
/** Postal / ZIP code */
|
||||
zip?: string;
|
||||
zp?: string;
|
||||
/** Country code */
|
||||
country?: string;
|
||||
/** External id to match users (optional) */
|
||||
external_id?: string;
|
||||
/** Gender */
|
||||
gender?: string;
|
||||
ge?: string;
|
||||
/** Date of birth (ISO-like or YYYY-MM-DD) */
|
||||
date_of_birth?: string;
|
||||
db?: string;
|
||||
// allow additional provider-specific keys
|
||||
[key: string]: string | undefined;
|
||||
};
|
||||
|
||||
20
src/lib/util/ip.ts
Normal file
20
src/lib/util/ip.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Extracts the client's IP address from the request headers or falls back to a provided function.
|
||||
*
|
||||
* @param request - The incoming Request object.
|
||||
* @param getClientAddress - A function that returns the client's IP address as a fallback.
|
||||
* @returns The client's IP address as a string.
|
||||
*/
|
||||
export const getRequestIP = (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()
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user