conversion api wrapper
This commit is contained in:
91
src/lib/conversion/client.ts
Normal file
91
src/lib/conversion/client.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { TrackingManager } from '$lib/tracking.svelte';
|
||||
import type {
|
||||
ConversionErrorResponseBody,
|
||||
ConversionRequestBody,
|
||||
ConversionResponseBody,
|
||||
ConversionUserData
|
||||
} from '$lib/types/conversion.js';
|
||||
import type { StandardEventName } from '$lib/types/fbq.js';
|
||||
|
||||
/**
|
||||
* Client for sending conversion events to a server endpoint.
|
||||
*/
|
||||
export class ConversionClient {
|
||||
private _href: string;
|
||||
private _trackingManager: TrackingManager;
|
||||
|
||||
/**
|
||||
* Creates a new ConversionClient.
|
||||
*
|
||||
* @param serverHref - The server endpoint URL.
|
||||
* @param trackingManager - The tracking manager instance.
|
||||
*/
|
||||
constructor(serverHref: string, trackingManager: TrackingManager) {
|
||||
this._href = serverHref;
|
||||
this._trackingManager = trackingManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
async trackEvent(
|
||||
eventName: StandardEventName,
|
||||
options: {
|
||||
eventID: string;
|
||||
user?: Omit<ConversionUserData, 'ip' | 'fbp' | 'fbc' | 'ua'>;
|
||||
customData?: Record<string, string>;
|
||||
}
|
||||
): Promise<ConversionResponseBody | ConversionErrorResponseBody> {
|
||||
// Extract user data
|
||||
const fbp = await cookieStore.get('_fbp');
|
||||
const fbc = await cookieStore.get('_fbc');
|
||||
|
||||
const user: ConversionUserData = {
|
||||
...options.user,
|
||||
fbp: fbp?.value,
|
||||
fbc: fbc?.value
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
|
||||
// Build request body
|
||||
const requestBody: ConversionRequestBody = {
|
||||
consent: this._trackingManager.consent === true,
|
||||
eventName,
|
||||
eventID: options.eventID,
|
||||
user,
|
||||
eventSourceURL,
|
||||
utms,
|
||||
customData: options.customData
|
||||
};
|
||||
|
||||
// Send request to server
|
||||
const response = await fetch(this._href, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as ConversionResponseBody;
|
||||
return data;
|
||||
} else {
|
||||
const errorData = (await response.json()) as ConversionErrorResponseBody;
|
||||
return errorData;
|
||||
}
|
||||
}
|
||||
}
|
||||
117
src/lib/conversion/control.ts
Normal file
117
src/lib/conversion/control.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
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';
|
||||
|
||||
/**
|
||||
* Builds UserData for conversion events.
|
||||
*
|
||||
* @param data - The user data to include.
|
||||
* @returns The constructed UserData object.
|
||||
*/
|
||||
export const buildConversionUserData = (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.
|
||||
*/
|
||||
export const buildCustomData = (params: Record<string, string>): CustomData => {
|
||||
const c = new CustomData();
|
||||
// Map known fields safely
|
||||
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);
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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,
|
||||
options: ConversionEventOptions
|
||||
): Promise<EventResponse> {
|
||||
const event = new ServerEvent()
|
||||
.setEventName(eventName)
|
||||
.setEventTime(Math.floor(Date.now() / 1000))
|
||||
.setEventId(options.eventID)
|
||||
.setUserData(options.userData)
|
||||
.setActionSource(options.actionSource);
|
||||
|
||||
if (options?.eventSourceURL) event.setEventSourceUrl(options.eventSourceURL);
|
||||
if (options?.customData) event.setCustomData(options.customData);
|
||||
|
||||
const req = new EventRequest(this._accessToken, this._pixelID).setEvents([event]);
|
||||
if (this._testEventCode) req.setTestEventCode(this._testEventCode);
|
||||
return req.execute();
|
||||
}
|
||||
}
|
||||
3
src/lib/conversion/index.ts
Normal file
3
src/lib/conversion/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './client.ts';
|
||||
export * from './server.ts';
|
||||
export * from './control.ts';
|
||||
67
src/lib/conversion/server.ts
Normal file
67
src/lib/conversion/server.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { buildConversionUserData, buildCustomData, ConversionControl } from './control.ts';
|
||||
import type {
|
||||
ConversionErrorResponseBody,
|
||||
ConversionRequestBody,
|
||||
ConversionResponseBody
|
||||
} from '$lib/types/conversion.js';
|
||||
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
|
||||
export const createConversionRequestHandler: (control: ConversionControl) => RequestHandler = (
|
||||
control
|
||||
) => {
|
||||
const handle: RequestHandler = async ({ request, getClientAddress }) => {
|
||||
try {
|
||||
const body = (await request.json()) as ConversionRequestBody;
|
||||
|
||||
// Build user data with IP and user agent
|
||||
const ip = getClientAddress();
|
||||
const ua = request.headers.get('user-agent');
|
||||
|
||||
const userData = buildConversionUserData({
|
||||
...body.user,
|
||||
ip,
|
||||
ua: ua ?? body.user?.ua ?? undefined
|
||||
});
|
||||
|
||||
// Build custom data with UTM params if applicable
|
||||
let rawCustomData = body.customData ?? {};
|
||||
if (body.eventName === 'PageView' && body.utms) {
|
||||
// For PageView events, automatically include UTM params if provided
|
||||
rawCustomData = {
|
||||
...rawCustomData,
|
||||
...body.utms
|
||||
};
|
||||
}
|
||||
|
||||
const customData = buildCustomData(rawCustomData);
|
||||
|
||||
// Send the event via the control
|
||||
const response = await control.trackEvent(body.eventName, {
|
||||
eventID: body.eventID,
|
||||
eventSourceURL: body.eventSourceURL,
|
||||
actionSource: 'website',
|
||||
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 });
|
||||
} catch (e) {
|
||||
return json(
|
||||
{ error: e instanceof Error ? e.message : String(e) } as ConversionErrorResponseBody,
|
||||
{ status: StatusCodes.INTERNAL_SERVER_ERROR }
|
||||
);
|
||||
}
|
||||
};
|
||||
return handle;
|
||||
};
|
||||
53
src/lib/types/conversion.d.ts
vendored
Normal file
53
src/lib/types/conversion.d.ts
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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?: Record<string, string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
Reference in New Issue
Block a user