conversion: explicit event params, removes any in PixelControl
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import type { TrackingManager } from '$lib/tracking.svelte';
|
import type { TrackingManager } from '$lib/tracking.svelte';
|
||||||
import type {
|
import type {
|
||||||
ConversionErrorResponseBody,
|
ConversionErrorResponseBody,
|
||||||
|
ConversionEventParams,
|
||||||
ConversionRequestBody,
|
ConversionRequestBody,
|
||||||
ConversionResponseBody,
|
ConversionResponseBody,
|
||||||
ConversionUserData
|
ConversionUserData
|
||||||
@@ -37,7 +38,7 @@ export class ConversionClient {
|
|||||||
options: {
|
options: {
|
||||||
eventID: string;
|
eventID: string;
|
||||||
user?: Omit<ConversionUserData, 'ip' | 'fbp' | 'fbc' | 'ua'>;
|
user?: Omit<ConversionUserData, 'ip' | 'fbp' | 'fbc' | 'ua'>;
|
||||||
customData?: Record<string, string>;
|
customData?: ConversionEventParams;
|
||||||
}
|
}
|
||||||
): Promise<ConversionResponseBody | ConversionErrorResponseBody> {
|
): Promise<ConversionResponseBody | ConversionErrorResponseBody> {
|
||||||
// Extract user data
|
// Extract user data
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import { CustomData, EventRequest, ServerEvent, UserData } from 'facebook-nodejs-business-sdk';
|
import {
|
||||||
|
Content,
|
||||||
|
CustomData,
|
||||||
|
EventRequest,
|
||||||
|
ServerEvent,
|
||||||
|
UserData
|
||||||
|
} from 'facebook-nodejs-business-sdk';
|
||||||
import type { StandardEventName } from '../types/fbq.js';
|
import type { StandardEventName } from '../types/fbq.js';
|
||||||
import type { ConversionResponseBody, ConversionUserData } from '$lib/types/conversion.js';
|
import type {
|
||||||
|
ConversionEventDetails,
|
||||||
|
ConversionEventParams,
|
||||||
|
ConversionResponseBody,
|
||||||
|
ConversionUserData
|
||||||
|
} from '$lib/types/conversion.js';
|
||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
|
|
||||||
@@ -10,7 +21,7 @@ import log from 'loglevel';
|
|||||||
* @param data - The user data to include.
|
* @param data - The user data to include.
|
||||||
* @returns The constructed UserData object.
|
* @returns The constructed UserData object.
|
||||||
*/
|
*/
|
||||||
export const buildConversionUserData = (data: ConversionUserData): UserData => {
|
const buildUserData = (data: ConversionUserData): UserData => {
|
||||||
const userData = new UserData();
|
const userData = new UserData();
|
||||||
if (data.email) userData.setEmail(data.email);
|
if (data.email) userData.setEmail(data.email);
|
||||||
if (data.phone) userData.setPhone(data.phone);
|
if (data.phone) userData.setPhone(data.phone);
|
||||||
@@ -30,38 +41,39 @@ export const buildConversionUserData = (data: ConversionUserData): UserData => {
|
|||||||
* @param params - The custom data parameters.
|
* @param params - The custom data parameters.
|
||||||
* @returns The constructed CustomData object.
|
* @returns The constructed CustomData object.
|
||||||
*/
|
*/
|
||||||
export const buildCustomData = (params: Record<string, string>): CustomData => {
|
const buildCustomData = (params: ConversionEventParams): CustomData => {
|
||||||
const c = new 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 (params.currency) c.setCurrency(params.currency);
|
||||||
if (typeof params.value === 'number') c.setValue(params.value);
|
if (params.content_name) c.setContentName(params.content_name);
|
||||||
if (Array.isArray(params.contents)) c.setContents(params.contents);
|
if (params.content_category) c.setContentCategory(params.content_category);
|
||||||
if (params.content_type) c.setContentType(params.content_type);
|
if (params.content_ids) c.setContentIds(params.content_ids);
|
||||||
if (Array.isArray(params.content_ids)) c.setContentIds(params.content_ids);
|
if (params.contents) {
|
||||||
if (typeof params.num_items === 'number') c.setNumItems(params.num_items);
|
const contents: Content[] = params.contents.map((content) => {
|
||||||
if (params.search_string) c.setSearchString(params.search_string);
|
const c = new Content();
|
||||||
// Attach anything else as custom_properties
|
if (content.id) c.setId(content.id.toString());
|
||||||
const extras = { ...params };
|
if (content.quantity) c.setQuantity(content.quantity);
|
||||||
delete extras.currency;
|
if (content.item_price) c.setItemPrice(content.item_price);
|
||||||
delete extras.value;
|
if (content.title) c.setTitle(content.title);
|
||||||
delete extras.contents;
|
if (content.description) c.setDescription(content.description);
|
||||||
delete extras.content_type;
|
if (content.category) c.setCategory(content.category);
|
||||||
delete extras.content_ids;
|
if (content.brand) c.setBrand(content.brand);
|
||||||
delete extras.num_items;
|
if (content.delivery_category) c.setDeliveryCategory(content.delivery_category);
|
||||||
delete extras.search_string;
|
return c;
|
||||||
if (Object.keys(extras).length) c.setCustomProperties(extras);
|
});
|
||||||
|
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;
|
return c;
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parameters for sending a conversion event to Meta Pixel.
|
|
||||||
*/
|
|
||||||
export type ConversionEventOptions = {
|
|
||||||
eventID: string;
|
|
||||||
actionSource: 'website' | 'app' | 'offline' | 'other';
|
|
||||||
userData: UserData;
|
|
||||||
customData?: CustomData;
|
|
||||||
eventSourceURL?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,17 +107,21 @@ export class ConversionControl {
|
|||||||
*/
|
*/
|
||||||
async trackEvent(
|
async trackEvent(
|
||||||
eventName: StandardEventName,
|
eventName: StandardEventName,
|
||||||
options: ConversionEventOptions
|
details: ConversionEventDetails,
|
||||||
|
params?: ConversionEventParams
|
||||||
): Promise<ConversionResponseBody> {
|
): Promise<ConversionResponseBody> {
|
||||||
const event = new ServerEvent()
|
const event = new ServerEvent()
|
||||||
.setEventName(eventName)
|
.setEventName(eventName)
|
||||||
.setEventTime(Math.floor(Date.now() / 1000))
|
.setEventTime(Math.floor(Date.now() / 1000))
|
||||||
.setEventId(options.eventID)
|
.setEventId(details.eventID)
|
||||||
.setUserData(options.userData)
|
.setUserData(buildUserData(details.userData))
|
||||||
.setActionSource(options.actionSource);
|
.setActionSource(details.actionSource);
|
||||||
|
|
||||||
if (options?.eventSourceURL) event.setEventSourceUrl(options.eventSourceURL);
|
if (details?.eventSourceURL) event.setEventSourceUrl(details.eventSourceURL);
|
||||||
if (options?.customData) event.setCustomData(options.customData);
|
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 we're in dev mode and missing a test event code, log and exit
|
||||||
if (dev && !this._testEventCode) {
|
if (dev && !this._testEventCode) {
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||||
import { buildConversionUserData, buildCustomData, ConversionControl } from './control.ts';
|
import { ConversionControl } from './control.ts';
|
||||||
import type { ConversionErrorResponseBody, ConversionRequestBody } from '$lib/types/conversion.js';
|
import type {
|
||||||
|
ConversionErrorResponseBody,
|
||||||
|
ConversionEventParams,
|
||||||
|
ConversionRequestBody,
|
||||||
|
ConversionUserData
|
||||||
|
} from '$lib/types/conversion.js';
|
||||||
|
|
||||||
import { StatusCodes } from 'http-status-codes';
|
import { StatusCodes } from 'http-status-codes';
|
||||||
|
|
||||||
@@ -29,32 +34,33 @@ export const createConversionRequestHandler: (control: ConversionControl) => Req
|
|||||||
const ip = getEventIP(request, getClientAddress);
|
const ip = getEventIP(request, getClientAddress);
|
||||||
const ua = request.headers.get('user-agent');
|
const ua = request.headers.get('user-agent');
|
||||||
|
|
||||||
const userData = buildConversionUserData({
|
const userData: ConversionUserData = {
|
||||||
...body.user,
|
...body.user,
|
||||||
ip,
|
ip,
|
||||||
ua: ua ?? body.user?.ua ?? undefined
|
ua: ua ?? body.user?.ua ?? undefined
|
||||||
});
|
};
|
||||||
|
|
||||||
// Build custom data with UTM params if applicable
|
// Build custom data with UTM params if applicable
|
||||||
let rawCustomData = body.customData ?? {};
|
let customData: ConversionEventParams = body.customData ?? {};
|
||||||
if (body.eventName === 'PageView' && body.utms) {
|
if (body.eventName === 'PageView' && body.utms) {
|
||||||
// For PageView events, automatically include UTM params if provided
|
// For PageView events, automatically include UTM params if provided
|
||||||
rawCustomData = {
|
customData = {
|
||||||
...rawCustomData,
|
...customData,
|
||||||
...body.utms
|
...body.utms
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const customData = buildCustomData(rawCustomData);
|
|
||||||
|
|
||||||
// Send the event via the control
|
// Send the event via the control
|
||||||
const response = await control.trackEvent(body.eventName, {
|
const response = await control.trackEvent(
|
||||||
|
body.eventName,
|
||||||
|
{
|
||||||
eventID: body.eventID,
|
eventID: body.eventID,
|
||||||
eventSourceURL: body.eventSourceURL,
|
eventSourceURL: body.eventSourceURL,
|
||||||
actionSource: 'website',
|
actionSource: 'website',
|
||||||
userData,
|
userData
|
||||||
|
},
|
||||||
customData
|
customData
|
||||||
});
|
);
|
||||||
|
|
||||||
return json(response, { status: StatusCodes.OK });
|
return json(response, { status: StatusCodes.OK });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -12,7 +12,34 @@ import { resolveGetter, type MaybeGetter } from '../util/getter.ts';
|
|||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
import { ConversionClient } from '../conversion/client.ts';
|
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 = {
|
export type PixelControlOptions = {
|
||||||
/**
|
/**
|
||||||
* if provided, events fired will always have this code attached
|
* if provided, events fired will always have this code attached
|
||||||
@@ -229,7 +256,7 @@ export class PixelControl {
|
|||||||
if (this._conversionClient) {
|
if (this._conversionClient) {
|
||||||
eventID = eventID ?? crypto.randomUUID();
|
eventID = eventID ?? crypto.randomUUID();
|
||||||
this._conversionClient
|
this._conversionClient
|
||||||
.trackEvent(event, { eventID: eventID, customData: params as any })
|
.trackEvent(event, { eventID: eventID, customData: pixelParamsToCustomData(params ?? {}) })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
log.debug(
|
log.debug(
|
||||||
`Meta Pixel [${this._pixelID}] ${event} event sent to Conversion API with Event ID: ${eventID}, Response: ${JSON.stringify(
|
`Meta Pixel [${this._pixelID}] ${event} event sent to Conversion API with Event ID: ${eventID}, Response: ${JSON.stringify(
|
||||||
|
|||||||
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;
|
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.
|
* 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>
|
Record<'utm_source' | 'utm_medium' | 'utm_campaign' | 'utm_content' | 'utm_term', string>
|
||||||
>;
|
>;
|
||||||
user?: Omit<ConversionUserData, 'ip'>;
|
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"]).
|
* 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.
|
* Name of the page/product.
|
||||||
|
|||||||
Reference in New Issue
Block a user