Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const destination: BrowserDestinationDefinition<Settings, FBClient> = {
pixelId: {
description: 'The Pixel ID associated with your Facebook Pixel.',
label: 'Pixel ID',
minimum: 15,
maximum: 15,
type: 'string',
required: true
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export const value: InputField = {
label: 'Value',
description: 'A numeric value associated with this event. This could be a monetary value or a value in some other metric.',
type: 'number',
default: { '@path': '$.properties.currency' },
default: { '@path': '$.properties.total' },
depends_on: getDependenciesFor('value'),
required: {
match: 'all',
Expand All @@ -268,7 +268,7 @@ export const custom_data: InputField = {

export const eventID: InputField = {
label: 'Event ID',
description: 'This ID can be any unique string. Event ID is used to deduplicate events sent by both Facebook Pixel and Conversions API.',
description: 'This ID can be any unique string. Event ID is used to deduplicate events sent both the server side Conversions API and the browser Pixel.',
type: 'string',
default: { '@path': '$.messageId' }
}
Expand Down Expand Up @@ -315,7 +315,7 @@ export const userData: InputField = {
},
ph: {
label: 'Phone Number',
description: 'Phone number of the user',
description: 'Phone number of the user. Make sure to include the country code. For example, "15551234567" for a US number.',
type: 'string'
},
fn: {
Expand Down Expand Up @@ -354,7 +354,7 @@ export const userData: InputField = {
},
zp: {
label: 'ZIP/Postal Code',
description: 'ZIP or postal code of the user. For example, "94025" for Menlo Park, CA, or "10001" for New York City.',
description: 'ZIP or postal code of the user. For example, U.S zip code: 94035, Australia zip code: 1987, France zip code: 75018, UK zip code: m11ae',
type: 'string'
},
country: {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,17 @@ import type { BrowserActionDefinition } from '@segment/browser-destination-runti
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
import { AllFields } from './fields'
import type { FBClient, FBStandardEventType, FBNonStandardEventType } from '../types'
import { buildOptions } from './utils'
import { getNotVisibleForEvent } from './depends-on'
import { validate } from './validate'
import type { FBClient } from '../types'
import { send } from './utils'

const action: BrowserActionDefinition<Settings, FBClient, Payload> = {
title: 'Send Event',
description: 'Send a Standard or Custom Event to Facebook Conversions API.',
platform: 'web',
defaultSubscription: 'type = "track"',
fields: AllFields,
perform: (client, { payload, settings }) => {
const { pixelId } = settings
const {
event_config: { custom_event_name, show_fields, event_name } = {},
eventID,
eventSourceUrl,
actionSource,
userData,
...rest
} = payload

const isCustom = event_name === 'CustomEvent' ? true : false

if(isCustom){
validate(payload)
}

if(show_fields === false){
// If show_fields is false we delete values for fields which are hidden in the UI.
const fieldsToDelete = getNotVisibleForEvent(event_name as FBStandardEventType | FBNonStandardEventType)
fieldsToDelete.forEach(field => {
if (field in rest) {
delete rest[field as keyof typeof rest]
}
})
}

const options = buildOptions(payload)

if(isCustom){
client(
'trackSingleCustom',
pixelId,
custom_event_name as string,
{ ...rest },
options
)
} else {
client(
'trackSingle',
pixelId,
event_name as FBStandardEventType,
{ ...rest },
options
)
}
send(client, payload, settings)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,70 @@
import { LDU, UserData, Options, ActionSource } from '../types'
import { LDU, UserData, Options, ActionSource, FBClient, FBStandardEventType, FBNonStandardEventType } from '../types'
import { US_STATE_CODES, COUNTRY_CODES} from './constants'
import { Payload } from './generated-types'
import { Settings } from '../generated-types'
import { getNotVisibleForEvent } from './depends-on'
import { omit } from '@segment/actions-core'

export function send(client: FBClient, payload: Payload, settings: Settings) {
const { pixelId } = settings
const {
event_config: { custom_event_name, show_fields, event_name } = {},
...rest
} = omit(payload, ['eventID', 'eventSourceUrl', 'actionSource', 'userData'])

const isCustom = event_name === 'CustomEvent' ? true : false

if(isCustom && !validate(payload)){
return
}

if(show_fields === false){
// If show_fields is false we delete values for fields which are hidden in the UI.
const fieldsToDelete = getNotVisibleForEvent(event_name as FBStandardEventType | FBNonStandardEventType)
fieldsToDelete.forEach(field => {
if (field in rest) {
delete rest[field as keyof typeof rest]
}
})
}

const options = buildOptions(payload)

if(isCustom){
client(
'trackSingleCustom',
pixelId,
custom_event_name as string,
{ ...rest },
options
)
} else {
client(
'trackSingle',
pixelId,
event_name as FBStandardEventType,
{ ...rest },
options
)
}
}

export function validate(payload: Payload): boolean {
const {
event_config: { event_name },
content_ids,
contents
} = payload

if(['AddToCart', 'Purchase', 'ViewContent'].includes(event_name)){
if(content_ids?.length === 0 || contents?.length === 0) {
console.warn(`content_ids or contents are required for event ${event_name}`)
return false
}
}

return true
}

export function getLDU(ldu: keyof typeof LDU) {
const lduObj = LDU[ldu]
Expand All @@ -10,11 +74,13 @@ export function getLDU(ldu: keyof typeof LDU) {
export function buildOptions(payload: Payload): Options | undefined {
const { eventID, eventSourceUrl, actionSource, userData } = payload

const sanitizedUserData = santizeUserData(userData as UserData)

const options: Options = {
eventID,
eventSourceUrl,
actionSource: actionSource as ActionSource | undefined,
userData: santizeUserData(userData as UserData)
...(actionSource ? { actionSource: actionSource as ActionSource } : {}),
...(sanitizedUserData ? { userData: sanitizedUserData } : {})
}

return Object.values(options).some(Boolean) ? options : undefined
Expand All @@ -24,17 +90,36 @@ export function santizeUserData(userData: UserData): UserData | undefined {
if(!userData){
return undefined
}
userData.em = typeof userData.em === 'string' ? userData.em.toLowerCase().trim() : undefined // lowercase and trim whitespace
userData.ph = userData.ph ? userData.ph.replace(/\D/g, '') : undefined // remove non-numeric characters
userData.fn = userData.fn ? userData.fn.toLowerCase().trim() : undefined // lowercase and trim whitespace
userData.ln = userData.ln ? userData.ln.toLowerCase().trim() : undefined // lowercase and trim whitespace
userData.ge = userData.ge ? userData.ge.toLowerCase().trim() : undefined // lowercase and trim whitespace
userData.db = formatDate(userData.db) // format date to YYYYMMDD
userData.ct = userData.ct ? userData.ct.toLowerCase().replace(/\s+/g, '') : undefined // lowercase and replace any whitespace
userData.st = sanitizeState(userData.st) // lowercase 2 character state code
userData.zp = userData.zp ? userData.zp.trim() : undefined
userData.country = sanitizeCountry(userData.country) // lowercase 2 character country code
return userData

const { external_id, em, ph, fn, ln, ge, db, ct, st, zp, country } = userData

const formattedExternalId = external_id ?? undefined
const formattedEm = typeof em === 'string' ? em.toLowerCase().trim() : undefined
const formattedPh = ph ? ph.replace(/\D/g, '') : undefined
const formattedFn = fn ? sanitiseUtf8String(fn) : undefined
const formattedLn = ln ? sanitiseUtf8String(ln) : undefined
const formattedGe = ge === 'f' || ge === 'm' ? ge : undefined
const formattedDb = formatDate(db)
const formattedCt = ct ? sanitiseUtf8String(ct) : undefined
const formattedSt = st ? sanitizeState(st) : undefined
const formattedZp = zp ? sanitiseUtf8String(zp) : undefined
const formattedCountry = country ? sanitizeCountry(country) : undefined

const ud: UserData = {
...(formattedExternalId ? {external_id: formattedExternalId} : {}),
...(formattedEm ? {em: formattedEm} : {}),
...(formattedPh ? {ph: formattedPh} : {}),
...(formattedFn ? {fn: formattedFn} : {}),
...(formattedLn ? {ln: formattedLn} : {}),
...(formattedGe ? {ge: formattedGe} : {}),
...(formattedDb ? {db: formattedDb} : {}),
...(formattedCt ? {ct: formattedCt} : {}),
...(formattedSt ? {st: formattedSt} : {}),
...(formattedZp ? {zp: formattedZp} : {}),
...(formattedCountry ? {country: formattedCountry} : {})
}

return ud
}

function formatDate(isoDate?: string): string | undefined {
Expand All @@ -56,35 +141,46 @@ function sanitizeState(state?: string): string | undefined {
return undefined
}

const normalized = state.replace(/\s+/g, '').toLowerCase()
const normalized = sanitiseUtf8String(state)

if (!normalized) {
return undefined
}

const abbreviation = US_STATE_CODES.get(normalized)
if (abbreviation) {
return abbreviation
} else
{
return normalized
}

if (/^[a-z]{2}$/i.test(normalized)) {
return normalized.toLowerCase()
}

return undefined
}

function sanitizeCountry(country?: string): string | undefined {
if (typeof country !== 'string' || !country.trim()) {
return undefined
}

const normalized = country.replace(/\s+/g, '').toUpperCase()
const normalized = sanitiseUtf8String(country)

if(!normalized){
return undefined
}

const abbreviation = COUNTRY_CODES.get(normalized)
if (abbreviation) {
return abbreviation
} else {
return normalized
}
}

if (/^[A-Z]{2}$/.test(normalized)) {
return normalized.toLowerCase()
function sanitiseUtf8String(stringValue?: string): string | undefined {
if (!stringValue) {
return undefined
}

return undefined
// Remove all non-letter characters using Unicode property escapes
return stringValue
.toLowerCase()
.replace(/[^\p{L}]/gu, '')
}
Original file line number Diff line number Diff line change
@@ -1,15 +1 @@
import { Payload } from './generated-types'

export function validate(payload: Payload) {
const {
event_config: { event_name },
content_ids,
contents
} = payload

if(['AddToCart', 'Purchase', 'ViewContent'].includes(event_name)){
if(content_ids?.length === 0 || contents?.length === 0) {
throw new Error(`content_ids or contents are required for event ${event_name}`)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export type UserData = {
ph?: string // Phone number (SHA-256)
fn?: string // First name (SHA-256)
ln?: string // Last name (SHA-256)
ge?: string // Gender (SHA-256)
ge?: 'f' | 'm' // Gender (SHA-256)
db?: string // Date of birth (SHA-256) - format: YYYYMMDD
ct?: string // City (SHA-256)
st?: string // State (SHA-256)
Expand Down
Loading