Skip to content

Commit 56fc074

Browse files
authored
STRATCONN-6152: Fix batching support for editContactInfo function (#3211)
* STRATCONN-6152: Fix batching support for editContactInfo function * address review comment
1 parent dde001b commit 56fc074

File tree

2 files changed

+121
-33
lines changed

2 files changed

+121
-33
lines changed

packages/destination-actions/src/destinations/first-party-dv360/addToAudContactInfo/_tests_/index.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,76 @@ describe('First-Party-dv360.addToAudContactInfo', () => {
7777
'"{\\"advertiserId\\":\\"1234567890\\",\\"addedContactInfoList\\":{\\"contactInfos\\":[{\\"hashedEmails\\":\\"584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777\\",\\"hashedPhoneNumbers\\":\\"422ce82c6fc1724ac878042f7d055653ab5e983d186e616826a72d4384b68af8\\",\\"zipCodes\\":\\"12345\\",\\"hashedFirstName\\":\\"96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a\\",\\"hashedLastName\\":\\"799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f\\",\\"countryCode\\":\\"US\\"}],\\"consent\\":{\\"adUserData\\":\\"CONSENT_STATUS_GRANTED\\",\\"adPersonalization\\":\\"CONSENT_STATUS_GRANTED\\"}}}"'
7878
)
7979
})
80+
81+
it('should batch multiple payloads into a single request when enable_batching is true', async () => {
82+
nock('https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences')
83+
.post('/1234567890:editCustomerMatchMembers')
84+
.reply(200, { success: true })
85+
86+
const events = createBatchTestEvents(createContactList)
87+
const responses = await testDestination.testBatchAction('addToAudContactInfo', {
88+
events: events,
89+
mapping: {
90+
emails: ['584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777'],
91+
phoneNumbers: ['422ce82c6fc1724ac878042f7d055653ab5e983d186e616826a72d4384b68af8'],
92+
zipCodes: ['12345'],
93+
firstName: '96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a',
94+
lastName: '799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f',
95+
countryCode: 'US',
96+
external_id: '1234567890',
97+
advertiser_id: '1234567890',
98+
enable_batching: true,
99+
batch_size: 2
100+
}
101+
})
102+
103+
const requestBody = JSON.parse(String(responses[0].options.body))
104+
expect(requestBody.addedContactInfoList.contactInfos.length).toBe(2)
105+
expect(requestBody.addedContactInfoList.contactInfos[0].hashedEmails).toBeDefined()
106+
expect(requestBody.addedContactInfoList.contactInfos[1].hashedEmails).toBeDefined()
107+
// Optionally, check that the emails are correctly hashed and correspond to the input
108+
})
80109
})
110+
111+
export type BatchContactListItem = {
112+
id?: string
113+
email: string
114+
firstname: string
115+
lastname: string
116+
}
117+
118+
export const createBatchTestEvents = (batchContactList: BatchContactListItem[]) =>
119+
batchContactList.map((contact) =>
120+
createTestEvent({
121+
type: 'identify',
122+
traits: {
123+
email: contact.email,
124+
firstname: contact.firstname,
125+
lastname: contact.lastname,
126+
address: {
127+
city: 'San Francisco',
128+
country: 'USA',
129+
postal_code: '600001',
130+
state: 'California',
131+
street: 'Vancover st'
132+
},
133+
graduation_date: 1664533942262,
134+
company: 'Some Company',
135+
phone: '+13134561129',
136+
website: 'somecompany.com'
137+
}
138+
})
139+
)
140+
141+
const createContactList: BatchContactListItem[] = [
142+
{
143+
144+
firstname: 'User',
145+
lastname: 'One'
146+
},
147+
{
148+
149+
firstname: 'User',
150+
lastname: 'Two'
151+
}
152+
]

packages/destination-actions/src/destinations/first-party-dv360/functions.ts

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -125,53 +125,69 @@ export async function editDeviceMobileIds(
125125
return response.data
126126
}
127127

128-
export async function editContactInfo(
129-
request: RequestClient,
130-
payloads: Payload[],
131-
operation: 'add' | 'remove',
132-
statsContext?: StatsContext
133-
) {
134-
const payload = payloads[0]
135-
const audienceId = payloads[0].external_id
136-
137-
//Check if one of the required identifiers exists otherwise drop the event
138-
if (
139-
payload.emails === undefined &&
140-
payload.phoneNumbers === undefined &&
141-
payload.firstName === undefined &&
142-
payload.lastName === undefined
143-
) {
144-
return
145-
}
146-
147-
//Format the endpoint
148-
const endpoint = DV360API + '/' + audienceId + ':editCustomerMatchMembers'
149-
150-
// Prepare the request payload
151-
const contactInfoList = {
152-
contactInfos: [processPayload(payload)],
128+
// Helper to build contactInfoList
129+
function buildContactInfoList(contactInfos: Record<string, string>[]): {
130+
contactInfos: Record<string, string>[]
131+
consent: { adUserData: string; adPersonalization: string }
132+
} {
133+
return {
134+
contactInfos,
153135
consent: {
154136
adUserData: CONSENT_STATUS_GRANTED,
155137
adPersonalization: CONSENT_STATUS_GRANTED
156138
}
157139
}
140+
}
158141

159-
// Convert the payload to string if needed
160-
const requestPayload = JSON.stringify({
161-
advertiserId: payload.advertiser_id,
142+
// Helper to build request payload
143+
function buildRequestPayload(
144+
advertiserId: string,
145+
contactInfoList: {
146+
contactInfos: Record<string, string>[]
147+
consent: { adUserData: string; adPersonalization: string }
148+
},
149+
operation: 'add' | 'remove'
150+
) {
151+
return JSON.stringify({
152+
advertiserId,
162153
...(operation === 'add' ? { addedContactInfoList: contactInfoList } : {}),
163154
...(operation === 'remove' ? { removedContactInfoList: contactInfoList } : {})
164155
})
156+
}
165157

158+
export async function editContactInfo(
159+
request: RequestClient,
160+
payloads: Payload[],
161+
operation: 'add' | 'remove',
162+
statsContext?: StatsContext
163+
) {
164+
if (!payloads || payloads.length === 0) return
165+
166+
// TODO: remove this check, the framework should handle this
167+
const validPayloads = payloads.filter(
168+
(payload) =>
169+
payload.emails !== undefined ||
170+
payload.phoneNumbers !== undefined ||
171+
payload.firstName !== undefined ||
172+
payload.lastName !== undefined
173+
)
174+
if (validPayloads.length === 0) return
175+
176+
// Assume all payloads are for the same audience/advertiser (use first)
177+
const { external_id: audienceId, advertiser_id: advertiserId } = validPayloads[0]
178+
if (!audienceId || !advertiserId) {
179+
throw new IntegrationError('Missing required audience or advertiser ID', 'MISSING_REQUIRED_FIELD', 400)
180+
}
181+
const contactInfos = validPayloads.map(processPayload)
182+
const contactInfoList = buildContactInfoList(contactInfos)
183+
const requestPayload = buildRequestPayload(advertiserId, contactInfoList, operation)
184+
const endpoint = DV360API + '/' + audienceId + ':editCustomerMatchMembers'
166185
const response = await request<DV360editCustomerMatchResponse>(endpoint, {
167186
method: 'POST',
168-
headers: {
169-
'Content-Type': 'application/json; charset=utf-8'
170-
},
187+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
171188
body: requestPayload
172189
})
173-
174-
statsContext?.statsClient?.incr('addCustomerMatchMembers.success', 1, statsContext?.tags)
190+
statsContext?.statsClient?.incr('addCustomerMatchMembers.success', contactInfos.length, statsContext?.tags)
175191
return response.data
176192
}
177193

0 commit comments

Comments
 (0)