-
Notifications
You must be signed in to change notification settings - Fork 283
Fix batching for editCustomerMatchMembers action in first party DV360 #3232
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,27 @@ const event = createTestEvent({ | |
} | ||
}) | ||
|
||
const event2 = createTestEvent({ | ||
event: 'Audience Entered', | ||
type: 'track', | ||
properties: { | ||
audience_key: 'personas_test_audience' | ||
}, | ||
context: { | ||
device: { | ||
advertisingId: '456' | ||
}, | ||
traits: { | ||
email: '[email protected]', | ||
phoneNumbers: '+0987654321', | ||
zipCodes: '54321', | ||
firstName: 'Jane', | ||
lastName: 'Smith', | ||
countryCode: 'CA' | ||
} | ||
} | ||
}) | ||
|
||
describe('First-Party-dv360.addToAudContactInfo', () => { | ||
it('should hash pii data if not already hashed', async () => { | ||
nock('https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences') | ||
|
@@ -77,4 +98,188 @@ describe('First-Party-dv360.addToAudContactInfo', () => { | |
'"{\\"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\\"}}}"' | ||
) | ||
}) | ||
|
||
it('should handle batch requests with multiple payloads', async () => { | ||
nock('https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences') | ||
.post('/1234567890:editCustomerMatchMembers') | ||
.reply(200, { success: true }) | ||
|
||
const responses = await testDestination.testBatchAction('addToAudContactInfo', { | ||
events: [event, event2], | ||
mapping: { | ||
emails: { | ||
'@arrayPath': [ | ||
'$.context.traits.email' | ||
] | ||
}, | ||
phoneNumbers: { | ||
'@arrayPath': [ | ||
'$.context.traits.phoneNumbers' | ||
] | ||
}, | ||
zipCodes: { | ||
'@arrayPath': [ | ||
'$.context.traits.zipCodes' | ||
] | ||
}, | ||
firstName: { | ||
'@path': '$.context.traits.firstName' | ||
}, | ||
lastName: { | ||
'@path': '$.context.traits.lastName' | ||
}, | ||
countryCode: { | ||
'@path': '$.context.traits.countryCode' | ||
}, | ||
external_id: '1234567890', | ||
advertiser_id: '1234567890', | ||
enable_batching: true, | ||
batch_size: 10 | ||
} | ||
}) | ||
|
||
expect(responses.length).toBe(1) | ||
expect(responses[0].status).toBe(200) | ||
|
||
// Parse the request body to verify it contains both contact infos | ||
const requestBody = JSON.parse(responses[0].options.body as string) | ||
expect(requestBody.addedContactInfoList.contactInfos).toHaveLength(2) | ||
|
||
// Verify first contact info | ||
expect(requestBody.addedContactInfoList.contactInfos[0]).toMatchObject({ | ||
hashedEmails: '584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777', | ||
hashedPhoneNumbers: '422ce82c6fc1724ac878042f7d055653ab5e983d186e616826a72d4384b68af8', | ||
zipCodes: '12345', | ||
hashedFirstName: '96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a', | ||
hashedLastName: '799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f', | ||
countryCode: 'US' | ||
}) | ||
|
||
// Verify second contact info | ||
expect(requestBody.addedContactInfoList.contactInfos[1]).toMatchObject({ | ||
hashedEmails: 'f9b0f73e2d723f122e24fddfebf37978c09a31b8530be10dccf51e6a4c49cbfa', | ||
hashedPhoneNumbers: '75bfc57aed345daba0e4394b604a334c87ab5f7b1c04dfdb649bcc457c182fa9', | ||
zipCodes: '54321', | ||
hashedFirstName: '81f8f6dde88365f3928796ec7aa53f72820b06db8664f5fe76a7eb13e24546a2', | ||
hashedLastName: '6627835f988e2c5e50533d491163072d3f4f41f5c8b04630150debb3722ca2dd', | ||
countryCode: 'CA' | ||
}) | ||
}) | ||
|
||
it('should filter out payloads without required identifiers in batch', async () => { | ||
nock('https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences') | ||
.post('/1234567890:editCustomerMatchMembers') | ||
.reply(200, { success: true }) | ||
|
||
const eventWithoutIdentifiers = createTestEvent({ | ||
event: 'Audience Entered', | ||
type: 'track', | ||
properties: { | ||
audience_key: 'personas_test_audience' | ||
}, | ||
context: { | ||
traits: { | ||
// No email, phone, firstName, or lastName | ||
zipCodes: '99999', | ||
countryCode: 'FR' | ||
} | ||
} | ||
}) | ||
|
||
const responses = await testDestination.testBatchAction('addToAudContactInfo', { | ||
events: [event, eventWithoutIdentifiers, event2], | ||
mapping: { | ||
emails: { | ||
'@arrayPath': [ | ||
'$.context.traits.email' | ||
] | ||
}, | ||
phoneNumbers: { | ||
'@arrayPath': [ | ||
'$.context.traits.phoneNumbers' | ||
] | ||
}, | ||
zipCodes: { | ||
'@arrayPath': [ | ||
'$.context.traits.zipCodes' | ||
] | ||
}, | ||
firstName: { | ||
'@path': '$.context.traits.firstName' | ||
}, | ||
lastName: { | ||
'@path': '$.context.traits.lastName' | ||
}, | ||
countryCode: { | ||
'@path': '$.context.traits.countryCode' | ||
}, | ||
external_id: '1234567890', | ||
advertiser_id: '1234567890', | ||
enable_batching: true, | ||
batch_size: 10 | ||
} | ||
}) | ||
|
||
expect(responses.length).toBe(1) | ||
expect(responses[0].status).toBe(200) | ||
|
||
// Parse the request body to verify it only contains 2 contact infos (filtered out the invalid one) | ||
const requestBody = JSON.parse(responses[0].options.body as string) | ||
expect(requestBody.addedContactInfoList.contactInfos).toHaveLength(2) | ||
}) | ||
|
||
it('should handle single payload in batch properly', async () => { | ||
nock('https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences') | ||
.post('/1234567890:editCustomerMatchMembers') | ||
.reply(200, { success: true }) | ||
|
||
const responses = await testDestination.testBatchAction('addToAudContactInfo', { | ||
events: [event], | ||
mapping: { | ||
emails: { | ||
'@arrayPath': [ | ||
'$.context.traits.email' | ||
] | ||
}, | ||
phoneNumbers: { | ||
'@arrayPath': [ | ||
'$.context.traits.phoneNumbers' | ||
] | ||
}, | ||
zipCodes: { | ||
'@arrayPath': [ | ||
'$.context.traits.zipCodes' | ||
] | ||
}, | ||
firstName: { | ||
'@path': '$.context.traits.firstName' | ||
}, | ||
lastName: { | ||
'@path': '$.context.traits.lastName' | ||
}, | ||
countryCode: { | ||
'@path': '$.context.traits.countryCode' | ||
}, | ||
external_id: '1234567890', | ||
advertiser_id: '1234567890', | ||
enable_batching: true, | ||
batch_size: 10 | ||
} | ||
}) | ||
|
||
expect(responses.length).toBe(1) | ||
expect(responses[0].status).toBe(200) | ||
|
||
// Parse the request body to verify it contains 1 contact info | ||
const requestBody = JSON.parse(responses[0].options.body as string) | ||
expect(requestBody.addedContactInfoList.contactInfos).toHaveLength(1) | ||
expect(requestBody.addedContactInfoList.contactInfos[0]).toMatchObject({ | ||
hashedEmails: '584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777', | ||
hashedPhoneNumbers: '422ce82c6fc1724ac878042f7d055653ab5e983d186e616826a72d4384b68af8', | ||
zipCodes: '12345', | ||
hashedFirstName: '96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a', | ||
hashedLastName: '799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f', | ||
countryCode: 'US' | ||
}) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -79,20 +79,30 @@ export async function editDeviceMobileIds( | |
operation: 'add' | 'remove', | ||
statsContext?: StatsContext // Adjust type based on actual stats context | ||
) { | ||
const payload = payloads[0] | ||
const audienceId = payload.external_id | ||
if (payloads.length === 0) { | ||
return | ||
} | ||
|
||
const firstPayload = payloads[0] | ||
const audienceId = firstPayload.external_id | ||
const advertiserId = firstPayload.advertiser_id | ||
|
||
//Check if mobile device id exists otherwise drop the event | ||
if (payload.mobileDeviceIds === undefined) { | ||
// Filter out payloads that don't have mobile device ids | ||
const validPayloads = payloads.filter(payload => payload.mobileDeviceIds !== undefined) | ||
|
||
if (validPayloads.length === 0) { | ||
return | ||
} | ||
|
||
//Format the endpoint | ||
const endpoint = DV360API + '/' + audienceId + ':editCustomerMatchMembers' | ||
|
||
// Collect all mobile device ids from valid payloads | ||
const mobileDeviceIds = validPayloads.map(payload => payload.mobileDeviceIds).filter(Boolean) | ||
|
||
// Prepare the request payload | ||
const mobileDeviceIdList = { | ||
mobileDeviceIds: [payload.mobileDeviceIds], | ||
mobileDeviceIds: mobileDeviceIds, | ||
consent: { | ||
adUserData: CONSENT_STATUS_GRANTED, | ||
adPersonalization: CONSENT_STATUS_GRANTED | ||
|
@@ -101,7 +111,7 @@ export async function editDeviceMobileIds( | |
|
||
// Convert the payload to string if needed | ||
const requestPayload = JSON.stringify({ | ||
advertiserId: payload.advertiser_id, | ||
advertiserId: advertiserId, | ||
...(operation === 'add' ? { addedMobileDeviceIdList: mobileDeviceIdList } : {}), | ||
...(operation === 'remove' ? { removedMobileDeviceIdList: mobileDeviceIdList } : {}) | ||
}) | ||
|
@@ -131,25 +141,35 @@ export async function editContactInfo( | |
operation: 'add' | 'remove', | ||
statsContext?: StatsContext | ||
) { | ||
const payload = payloads[0] | ||
const audienceId = payloads[0].external_id | ||
|
||
//Check if one of the required identifiers exists otherwise drop the event | ||
if ( | ||
payload.emails === undefined && | ||
payload.phoneNumbers === undefined && | ||
payload.firstName === undefined && | ||
payload.lastName === undefined | ||
) { | ||
if (payloads.length === 0) { | ||
return | ||
} | ||
|
||
const firstPayload = payloads[0] | ||
const audienceId = firstPayload.external_id | ||
const advertiserId = firstPayload.advertiser_id | ||
|
||
// Filter out payloads that don't have any required identifiers | ||
const validPayloads = payloads.filter(payload => | ||
payload.emails !== undefined || | ||
payload.phoneNumbers !== undefined || | ||
payload.firstName !== undefined || | ||
payload.lastName !== undefined | ||
) | ||
|
||
|
||
if (validPayloads.length === 0) { | ||
return | ||
} | ||
|
||
//Format the endpoint | ||
const endpoint = DV360API + '/' + audienceId + ':editCustomerMatchMembers' | ||
|
||
// Process all valid payloads into contact infos | ||
const contactInfos = validPayloads.map(payload => processPayload(payload)) | ||
|
||
// Prepare the request payload | ||
const contactInfoList = { | ||
contactInfos: [processPayload(payload)], | ||
contactInfos: contactInfos, | ||
consent: { | ||
adUserData: CONSENT_STATUS_GRANTED, | ||
adPersonalization: CONSENT_STATUS_GRANTED | ||
|
@@ -158,7 +178,7 @@ export async function editContactInfo( | |
|
||
// Convert the payload to string if needed | ||
const requestPayload = JSON.stringify({ | ||
advertiserId: payload.advertiser_id, | ||
advertiserId: advertiserId, | ||
...(operation === 'add' ? { addedContactInfoList: contactInfoList } : {}), | ||
...(operation === 'remove' ? { removedContactInfoList: contactInfoList } : {}) | ||
}) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot this is not required, you can assume that the payloads are already filtered
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're absolutely right. I've removed the unnecessary filtering from both
editDeviceMobileIds
andeditContactInfo
functions since the actions framework already validates payloads before calling these functions. The code is now cleaner and more efficient. Changes committed in 7425747.