Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
commit a174a3c
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Sun Jul 28 15:15:56 2024 -0700

    Adds a unit test for multiple payloads. Generates types

commit fa0131d
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Sun Jul 28 14:58:38 2024 -0700

    Updates generateData to be a shared function so it can be exported and unit tested directly. Fixes minor issues and the first unit test is passing

commit 8ec6462
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Sun Jul 28 14:14:55 2024 -0700

    Major implementation of data & schema generation algorithm. The schema is hardcoded to always have the same order, irrespective to whether the payloads contain any of the keys in the hardcoded schema, this is to avoid having to generate a schema based on the present keys across all payloads. This is the method the classic ADS version of this destination takes as well. To generate the 2d data array all payloads are iterated thru, and each present value is inserted into the correct index of a row array which has the same length as the pre-defined schemas array. Each of these row arrays is sequentially appended to the data array. Includes 1 unit test, untested

commit 051cc65
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 25 15:35:55 2024 -0700

    A broken unit test, which I will fix with the next commit

commit 0e98649
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 25 14:59:13 2024 -0700

    Implements delete operation

commit f747dfd
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 25 14:50:07 2024 -0700

    Style updates and console.log removals

commit 48ff851
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 25 14:46:41 2024 -0700

    Fixes URL when syncing audience. Removes console.logs

commit 2e1f7ac
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 25 14:37:31 2024 -0700

    Updates usage of adAccountId setting to retlAdAccountId. Fixes type error

commit 8c6111a
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 18 17:03:00 2024 -0700

    WIP on syncing users - request likely failing due to bad permissions

commit 2cf86bf
Merge: 1bac8c3 bfc9b86
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 25 12:07:22 2024 -0700

    Merge remote-tracking branch 'origin/main' into fbca-retl-hook

commit 1bac8c3
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 25 12:06:03 2024 -0700

    Removes stray change to aws destination

commit 185d425
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 25 10:59:42 2024 -0700

    Updates hook logic to consider the operation the user selected, if create is selected we will attempt to create even if an existing audience ID exists on the mapping

commit a54e845
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Wed Jul 24 14:05:05 2024 -0700

    Generates types

commit 8a7a08e
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Tue Jul 23 13:11:32 2024 -0700

    Introduces a selector for users to choose whether they want to create a new rule or select an existing one

commit d7b8c1e
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Tue Jul 23 10:39:58 2024 -0700

    Fixes unit tests

commit b9f0391
Merge: 95b4593 b6fc3f2
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Tue Jul 23 10:27:04 2024 -0700

    Merge branch 'main' into fbca-retl-hook

commit 95b4593
Author: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com>
Date:   Fri Jul 19 16:19:18 2024 +0200

    aws s3 bug fixes (#2200)

commit 945c7eb
Author: Pooya Jaferian <pooya.j@gmail.com>
Date:   Thu Jul 18 17:02:38 2024 -0700

    Adding support for FieldDisplayModes & disabling certain input methods (#2187)

    * update types

    * added a note on default displayMode

commit 3ea1444
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Mon Jul 22 17:15:11 2024 -0700

    Splits off engage and retl AdAccountId settings, fun fact: these cannot share a slug

commit 0a1285a
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 18 15:08:29 2024 -0700

    Generates types

commit 8f5f5ed
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 18 15:04:14 2024 -0700

    WIP on an inexplicably not passing unit test

commit fc4f5ed
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 18 14:05:48 2024 -0700

    Checks for existence of user selected audience. Saves audience name and ID when returning from hook

commit a2a642c
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 18 12:58:03 2024 -0700

    WIP - verifying selected audience exists and saving it's ID as output

commit a022704
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 18 12:47:32 2024 -0700

    Removes description param from creating an audience per PRD

commit 49add30
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 18 12:44:07 2024 -0700

    Pulls down 200 most recent audiences in dynamic field response

commit c2f3e32
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 18 12:35:50 2024 -0700

    Removes pagination related code - that work will be saved for later :)

commit 6078690
Merge: b831f7b c9ecf66
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Thu Jul 18 12:31:29 2024 -0700

    Merge branch 'main' into fbca-retl-hook

commit b831f7b
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Wed Jul 17 15:14:15 2024 -0700

    Sets paging return such that the previous page was the current page

commit 365690e
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Wed Jul 17 15:09:14 2024 -0700

    Can pass dynamicFieldContext & paging into local server request

commit 037534c
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Wed Jul 17 14:37:23 2024 -0700

    Fixes build

commit 1e9d616
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Tue Jul 16 16:55:05 2024 -0700

    WIP on a pagination mechanism for dynamic fields

commit 151d26c
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Tue Jul 16 16:14:03 2024 -0700

    Adds a dynamic audience field, pulls down all customaudiences as choices

commit 27e4903
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Tue Jul 16 15:29:46 2024 -0700

    Adds required customer_file_source field

commit 60d2291
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Tue Jul 16 12:26:51 2024 -0700

    WIP - first draft of a FacebookClient class, including an untested createAudience hook for retl

commit 5ba6c09
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Tue Jul 16 12:05:23 2024 -0700

    Introduces shared adAccountId top-level setting and audienceSetting

commit 0a20b76
Author: Nick Aguilar <nicholas.aguilar@segment.com>
Date:   Tue Jul 16 12:02:16 2024 -0700

    Removes delete action, add action. Introduces single sync action
  • Loading branch information
nick-Ag committed Jul 29, 2024
1 parent d29c23b commit 261fe1a
Show file tree
Hide file tree
Showing 15 changed files with 806 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import createRequestClient from '../../../../../core/src/request-client'
import FacebookClient, { BASE_URL, generateData } from '../fbca-operations'
import { Settings } from '../generated-types'
import nock from 'nock'
import { Payload } from '../sync/generated-types'
import { createHash } from 'crypto'

const requestClient = createRequestClient()
const settings: Settings = {
retlAdAccountId: 'act_123456'
}
const EMPTY = ''

// clone of the hash function in fbca-operations.ts since it's a private method
const hash = (value: string): string => {
return createHash('sha256').update(value).digest('hex')
}

describe('Facebook Custom Audiences', () => {
const facebookClient = new FacebookClient(requestClient, settings.retlAdAccountId)
describe('retlOnMappingSave hook', () => {
const hookInputs = {
audienceName: 'test-audience'
}

it('should create a custom audience in facebook', async () => {
nock(`${BASE_URL}`)
.post(`/${settings.retlAdAccountId}/customaudiences`, {
name: hookInputs.audienceName,
subtype: 'CUSTOM',
customer_file_source: 'BOTH_USER_AND_PARTNER_PROVIDED'
})
.reply(201, { id: '123' })

await facebookClient.createAudience(hookInputs.audienceName)
})
})

describe('generateData', () => {
it('should generate data correctly for a single user', async () => {
const payloads: Payload[] = [
{
email: 'haaron@braves.com',
phone: '555-555-5555',
name: {
first: 'Henry',
last: 'Aaron'
},
externalId: '5'
}
]

expect(generateData(payloads)).toEqual([
[
hash(payloads[0].externalId || ''), // external_id
hash(payloads[0].email || ''), // email
hash(payloads[0].phone || ''), // phone
EMPTY, // gender
EMPTY, // year
EMPTY, // month
EMPTY, // day
hash(payloads[0].name?.last || ''), // last_name
hash(payloads[0].name?.first || ''), // first_name
EMPTY, // first_initial
EMPTY, // city
EMPTY, // state
EMPTY, // zip
EMPTY, // mobile_advertiser_id,
EMPTY // country
]
])
})

it('should generate data correctly for multiple users', async () => {
const payloads: Payload[] = new Array(2)

payloads[0] = {
email: 'haaron@braves.com',
phone: '555-555-5555',
name: {
first: 'Henry',
last: 'Aaron'
},
externalId: '5'
}

payloads[1] = {
email: 'tony@padres.com',
gender: 'male',
birth: {
year: '1960',
month: 'May',
day: '9'
},
name: {
first: 'Tony',
last: 'Gwynn',
firstInitial: 'T'
},
address: {
city: 'San Diego',
state: 'CA',
zip: '92000',
country: 'US'
}
}

expect(generateData(payloads)).toEqual([
[
hash(payloads[0].externalId || ''), // external_id
hash(payloads[0].email || ''), // email
hash(payloads[0].phone || ''), // phone
EMPTY, // gender
EMPTY, // year
EMPTY, // month
EMPTY, // day
hash(payloads[0].name?.last || ''), // last_name
hash(payloads[0].name?.first || ''), // first_name
EMPTY, // first_initial
EMPTY, // city
EMPTY, // state
EMPTY, // zip
EMPTY, // mobile_advertiser_id,
EMPTY // country
],
[
EMPTY, // external_id
hash(payloads[1].email || ''), // email
EMPTY, // phone
hash(payloads[1].gender || ''), // gender
hash(payloads[1].birth?.year || ''), // year
hash(payloads[1].birth?.month || ''), // month
hash(payloads[1].birth?.day || ''), // day
hash(payloads[1].name?.last || ''), // last_name
hash(payloads[1].name?.first || ''), // first_name
hash(payloads[1].name?.firstInitial || ''), // first_initial
hash(payloads[1].address?.city || ''), // city
hash(payloads[1].address?.state || ''), // state
hash(payloads[1].address?.zip || ''), // zip
EMPTY, // mobile_advertiser_id,
hash(payloads[1].address?.country || '') // country
]
])
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ const getAudienceUrl = `https://graph.facebook.com/${FACEBOOK_API_VERSION}/`
const createAudienceUrl = `https://graph.facebook.com/${FACEBOOK_API_VERSION}/act_${adAccountId}`

const createAudienceInput = {
settings: {},
settings: {
retlAdAccountId: '123'
},
audienceName: '',
audienceSettings: {
adAccountId: adAccountId,
engageAdAccountId: adAccountId,
audienceDescription: 'We are the Mario Brothers and plumbing is our game.'
}
}
const getAudienceInput = {
externalId: audienceId,
settings: {}
settings: {
retlAdAccountId: '123'
}
}

describe('Facebook Custom Audiences', () => {
Expand All @@ -29,15 +33,15 @@ describe('Facebook Custom Audiences', () => {

it('should fail if no ad account ID is set', async () => {
createAudienceInput.audienceName = 'The Void'
createAudienceInput.audienceSettings.adAccountId = 0
createAudienceInput.audienceSettings.engageAdAccountId = 0
await expect(testDestination.createAudience(createAudienceInput)).rejects.toThrowError(IntegrationError)
})

it('should create a new Facebook Audience', async () => {
nock(createAudienceUrl).post('/customaudiences').reply(200, { id: '88888888888888888' })

createAudienceInput.audienceName = 'The Super Mario Brothers Fans'
createAudienceInput.audienceSettings.adAccountId = adAccountId
createAudienceInput.audienceSettings.engageAdAccountId = adAccountId

const r = await testDestination.createAudience(createAudienceInput)
expect(r).toEqual({ externalId: '88888888888888888' })
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { DynamicFieldItem, DynamicFieldError, RequestClient, IntegrationError } from '@segment/actions-core'
import { Payload } from './sync/generated-types'
import { createHash } from 'crypto'
import { segmentSchemaKeyToArrayIndex, SCHEMA_PROPERTIES } from './fbca-properties'

const FACEBOOK_API_VERSION = 'v20.0'
// exported for unit testing
export const BASE_URL = `https://graph.facebook.com/${FACEBOOK_API_VERSION}/`

interface AudienceCreationResponse {
id: string
}

interface GetAllAudienceResponse {
data: {
id: string
name: string
}[]
}

interface GetSingleAudienceResponse {
name: string
id: string
}

interface FacebookResponseError {
error: {
message: string
type: string
code: number
}
}

// exported for unit testing. Also why these are not members of the class
export const generateData = (payloads: Payload[]): (string | number)[][] => {
const data: (string | number)[][] = new Array(payloads.length)

payloads.forEach((payload, index) => {
const row: (string | number)[] = new Array(SCHEMA_PROPERTIES.length).fill('')

Object.entries(payload).forEach(([key, value]) => {
if (typeof value === 'object') {
Object.entries(value).forEach(([nestedKey, value]) => {
appendToDataRow(nestedKey, value as string | number, row)
})
} else {
appendToDataRow(key, value as string | number, row)
}
})

data[index] = row
})

return data
}

const appendToDataRow = (key: string, value: string | number, row: (string | number)[]) => {
const index = segmentSchemaKeyToArrayIndex.get(key)

if (index === undefined) {
throw new IntegrationError(`Invalid schema key: ${key}`, 'INVALID_SCHEMA_KEY', 500)
}

if (typeof value === 'number') {
row[index] = value
return
}

row[index] = hash(value)
}

const hash = (value: string): string => {
return createHash('sha256').update(value).digest('hex')
}

export default class FacebookClient {
request: RequestClient
adAccountId: string

constructor(request: RequestClient, adAccountId: string) {
this.request = request
this.adAccountId = this.formatAdAccount(adAccountId)
}

createAudience = async (name: string) => {
return await this.request<AudienceCreationResponse>(`${BASE_URL}${this.adAccountId}/customaudiences`, {
method: 'post',
json: {
name,
subtype: 'CUSTOM',
customer_file_source: 'BOTH_USER_AND_PARTNER_PROVIDED'
}
})
}

getSingleAudience = async (
audienceId: string
): Promise<{ data?: GetSingleAudienceResponse; error?: FacebookResponseError }> => {
try {
const fields = '?fields=id,name'
const { data } = await this.request<GetSingleAudienceResponse>(`${BASE_URL}${audienceId}${fields}`)
return { data, error: undefined }
} catch (error) {
return { data: undefined, error: error as FacebookResponseError }
}
}

getAllAudiences = async (): Promise<{ choices: DynamicFieldItem[]; error: DynamicFieldError | undefined }> => {
const { data } = await this.request<GetAllAudienceResponse>(
`${BASE_URL}${this.adAccountId}/customaudiences?fields=id,name&limit=200`
)

const choices = data.data.map(({ id, name }) => ({
value: id,
label: name
}))

return {
choices,
error: undefined
}
}

syncAudience = async (input: { audienceId: string; payloads: Payload[]; deleteUsers?: boolean }) => {
const data = generateData(input.payloads)

const params = {
payload: {
schema: SCHEMA_PROPERTIES,
data: data
}
}

try {
return await this.request(`${BASE_URL}${input.audienceId}/users`, {
method: input.deleteUsers === true ? 'delete' : 'post',
json: params
})
} catch (e) {
return
}
}

private formatAdAccount(adAccountId: string) {
if (adAccountId.startsWith('act_')) {
return adAccountId
}
return `act_${adAccountId}`
}
}
Loading

0 comments on commit 261fe1a

Please sign in to comment.