From c35434546694a05eea7427a12485b3f24ded6a02 Mon Sep 17 00:00:00 2001 From: Prithviraj-rathore-segment Date: Tue, 2 Sep 2025 16:36:58 +0530 Subject: [PATCH] STRATCONN-6153 upgrade version from v3 to v4 for first party DV360 action destination --- .../first-party-dv360/_tests_/index.test.ts | 34 ++--- .../addToAudContactInfo/_tests_/index.test.ts | 76 +++++++++- .../first-party-dv360/functions.ts | 130 ++++++++++++------ .../destinations/first-party-dv360/index.ts | 4 +- .../_tests_/index.test.ts | 4 +- 5 files changed, 184 insertions(+), 64 deletions(-) diff --git a/packages/destination-actions/src/destinations/first-party-dv360/_tests_/index.test.ts b/packages/destination-actions/src/destinations/first-party-dv360/_tests_/index.test.ts index 95730518a18..58e058ac976 100644 --- a/packages/destination-actions/src/destinations/first-party-dv360/_tests_/index.test.ts +++ b/packages/destination-actions/src/destinations/first-party-dv360/_tests_/index.test.ts @@ -55,16 +55,16 @@ describe('Audience Destination', () => { describe('createAudience', () => { it('creates an audience successfully', async () => { nock('https://displayvideo.googleapis.com') - .post('/v3/firstAndThirdPartyAudiences?advertiserId=12345', { + .post('/v4/firstPartyAndPartnerAudiences?advertiserId=12345', { displayName: audienceName, audienceType: 'CUSTOMER_MATCH_CONTACT_INFO', membershipDurationDays: '30', description: 'Test description', audienceSource: 'AUDIENCE_SOURCE_UNSPECIFIED', - firstAndThirdPartyAudienceType: 'FIRST_AND_THIRD_PARTY_AUDIENCE_TYPE_FIRST_PARTY' + firstPartyAndPartnerAudienceType: 'TYPE_FIRST_PARTY' }) .matchHeader('Authorization', 'Bearer temp-token') - .reply(200, { firstAndThirdPartyAudienceId: 'audience-id-123' }) + .reply(200, { firstPartyAndPartnerAudienceId: 'audience-id-123' }) const result = await testDestination.createAudience(createAudienceInput) expect(result).toEqual({ externalId: 'audience-id-123' }) @@ -81,10 +81,10 @@ describe('Audience Destination', () => { describe('getAudience', () => { it('should succeed when provided with a valid audience ID', async () => { nock('https://displayvideo.googleapis.com') - .get(`/v3/firstAndThirdPartyAudiences/audience-id-123?advertiserId=12345`) + .get(`/v4/firstPartyAndPartnerAudiences/audience-id-123?advertiserId=12345`) .matchHeader('Authorization', 'Bearer temp-token') .reply(200, { - firstAndThirdPartyAudienceId: 'audience-id-123' + firstPartyAndPartnerAudienceId: 'audience-id-123' }) const result = await testDestination.getAudience(getAudienceInput) @@ -132,7 +132,7 @@ describe('Audience Destination', () => { it('should add customer match members successfully', async () => { nock('https://displayvideo.googleapis.com') - .post('/v3/firstAndThirdPartyAudiences/audience-id-123:editCustomerMatchMembers', { + .post('/v4/firstPartyAndPartnerAudiences/audience-id-123:editCustomerMatchMembers', { advertiserId: '12345', addedContactInfoList: { contactInfos: [ @@ -151,7 +151,7 @@ describe('Audience Destination', () => { } } }) - .reply(200, { firstAndThirdPartyAudienceId: 'audience-id-123' }) + .reply(200, { firstPartyAndPartnerAudienceId: 'audience-id-123' }) const result = await testDestination.testAction('addToAudContactInfo', { event, @@ -160,7 +160,7 @@ describe('Audience Destination', () => { expect(result).toContainEqual( expect.objectContaining({ data: expect.objectContaining({ - firstAndThirdPartyAudienceId: 'audience-id-123' + firstPartyAndPartnerAudienceId: 'audience-id-123' }) }) ) @@ -168,7 +168,7 @@ describe('Audience Destination', () => { it('should remove customer match members successfully', async () => { nock('https://displayvideo.googleapis.com') - .post('/v3/firstAndThirdPartyAudiences/audience-id-123:editCustomerMatchMembers', { + .post('/v4/firstPartyAndPartnerAudiences/audience-id-123:editCustomerMatchMembers', { advertiserId: '12345', removedContactInfoList: { contactInfos: [ @@ -187,7 +187,7 @@ describe('Audience Destination', () => { } } }) - .reply(200, { firstAndThirdPartyAudienceId: 'audience-id-123' }) + .reply(200, { firstPartyAndPartnerAudienceId: 'audience-id-123' }) const result = await testDestination.testAction('removeFromAudContactInfo', { event, @@ -196,7 +196,7 @@ describe('Audience Destination', () => { expect(result).toContainEqual( expect.objectContaining({ data: expect.objectContaining({ - firstAndThirdPartyAudienceId: 'audience-id-123' + firstPartyAndPartnerAudienceId: 'audience-id-123' }) }) ) @@ -227,7 +227,7 @@ describe('Audience Destination', () => { it('should add customer match members successfully', async () => { nock('https://displayvideo.googleapis.com') - .post('/v3/firstAndThirdPartyAudiences/audience-id-123:editCustomerMatchMembers', { + .post('/v4/firstPartyAndPartnerAudiences/audience-id-123:editCustomerMatchMembers', { advertiserId: '12345', addedMobileDeviceIdList: { mobileDeviceIds: ['123'], @@ -237,7 +237,7 @@ describe('Audience Destination', () => { } } }) - .reply(200, { firstAndThirdPartyAudienceId: 'audience-id-123' }) + .reply(200, { firstPartyAndPartnerAudienceId: 'audience-id-123' }) const result = await testDestination.testAction('addToAudMobileDeviceId', { event, @@ -246,7 +246,7 @@ describe('Audience Destination', () => { expect(result).toContainEqual( expect.objectContaining({ data: expect.objectContaining({ - firstAndThirdPartyAudienceId: 'audience-id-123' + firstPartyAndPartnerAudienceId: 'audience-id-123' }) }) ) @@ -254,7 +254,7 @@ describe('Audience Destination', () => { it('should remove customer match members successfully', async () => { nock('https://displayvideo.googleapis.com') - .post('/v3/firstAndThirdPartyAudiences/audience-id-123:editCustomerMatchMembers', { + .post('/v4/firstPartyAndPartnerAudiences/audience-id-123:editCustomerMatchMembers', { advertiserId: '12345', removedMobileDeviceIdList: { mobileDeviceIds: ['123'], @@ -264,7 +264,7 @@ describe('Audience Destination', () => { } } }) - .reply(200, { firstAndThirdPartyAudienceId: 'audience-id-123' }) + .reply(200, { firstPartyAndPartnerAudienceId: 'audience-id-123' }) const result = await testDestination.testAction('removeFromAudMobileDeviceId', { event, @@ -273,7 +273,7 @@ describe('Audience Destination', () => { expect(result).toContainEqual( expect.objectContaining({ data: expect.objectContaining({ - firstAndThirdPartyAudienceId: 'audience-id-123' + firstPartyAndPartnerAudienceId: 'audience-id-123' }) }) ) diff --git a/packages/destination-actions/src/destinations/first-party-dv360/addToAudContactInfo/_tests_/index.test.ts b/packages/destination-actions/src/destinations/first-party-dv360/addToAudContactInfo/_tests_/index.test.ts index 2fe4bfe6a5f..26c2fca0cc4 100644 --- a/packages/destination-actions/src/destinations/first-party-dv360/addToAudContactInfo/_tests_/index.test.ts +++ b/packages/destination-actions/src/destinations/first-party-dv360/addToAudContactInfo/_tests_/index.test.ts @@ -27,7 +27,7 @@ const event = createTestEvent({ describe('First-Party-dv360.addToAudContactInfo', () => { it('should hash pii data if not already hashed', async () => { - nock('https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences') + nock('https://displayvideo.googleapis.com/v4/firstPartyAndPartnerAudiences') .post('/1234567890:editCustomerMatchMembers') .reply(200, { success: true }) @@ -53,7 +53,7 @@ describe('First-Party-dv360.addToAudContactInfo', () => { }) it('should not hash pii data if already hashed', async () => { - nock('https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences') + nock('https://displayvideo.googleapis.com/v4/firstPartyAndPartnerAudiences') .post('/1234567890:editCustomerMatchMembers') .reply(200, { success: true }) @@ -77,4 +77,76 @@ 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 batch multiple payloads into a single request when enable_batching is true', async () => { + nock('https://displayvideo.googleapis.com/v4/firstPartyAndPartnerAudiences') + .post('/1234567890:editCustomerMatchMembers') + .reply(200, { success: true }) + + const events = createBatchTestEvents(createContactList) + const responses = await testDestination.testBatchAction('addToAudContactInfo', { + events: events, + mapping: { + emails: ['584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777'], + phoneNumbers: ['422ce82c6fc1724ac878042f7d055653ab5e983d186e616826a72d4384b68af8'], + zipCodes: ['12345'], + firstName: '96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a', + lastName: '799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f', + countryCode: 'US', + external_id: '1234567890', + advertiser_id: '1234567890', + enable_batching: true, + batch_size: 2 + } + }) + + const requestBody = JSON.parse(String(responses[0].options.body)) + expect(requestBody.addedContactInfoList.contactInfos.length).toBe(2) + expect(requestBody.addedContactInfoList.contactInfos[0].hashedEmails).toBeDefined() + expect(requestBody.addedContactInfoList.contactInfos[1].hashedEmails).toBeDefined() + // Optionally, check that the emails are correctly hashed and correspond to the input + }) }) + +export type BatchContactListItem = { + id?: string + email: string + firstname: string + lastname: string +} + +export const createBatchTestEvents = (batchContactList: BatchContactListItem[]) => + batchContactList.map((contact) => + createTestEvent({ + type: 'identify', + traits: { + email: contact.email, + firstname: contact.firstname, + lastname: contact.lastname, + address: { + city: 'San Francisco', + country: 'USA', + postal_code: '600001', + state: 'California', + street: 'Vancover st' + }, + graduation_date: 1664533942262, + company: 'Some Company', + phone: '+13134561129', + website: 'somecompany.com' + } + }) + ) + +const createContactList: BatchContactListItem[] = [ + { + email: 'userone@somecompany.com', + firstname: 'User', + lastname: 'One' + }, + { + email: 'usertwo@somecompany.com', + firstname: 'User', + lastname: 'Two' + } +] diff --git a/packages/destination-actions/src/destinations/first-party-dv360/functions.ts b/packages/destination-actions/src/destinations/first-party-dv360/functions.ts index 21be8a11430..8d299f76086 100644 --- a/packages/destination-actions/src/destinations/first-party-dv360/functions.ts +++ b/packages/destination-actions/src/destinations/first-party-dv360/functions.ts @@ -3,7 +3,7 @@ import { Payload } from './addToAudContactInfo/generated-types' import { Payload as DeviceIdPayload } from './addToAudMobileDeviceId/generated-types' import { processHashing } from '../../lib/hashing-utils' -const DV360API = `https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences` +const DV360API = `https://displayvideo.googleapis.com/v4/firstPartyAndPartnerAudiences` const CONSENT_STATUS_GRANTED = 'CONSENT_STATUS_GRANTED' // Define consent status interface createAudienceRequestParams { @@ -23,7 +23,7 @@ interface getAudienceParams { } interface DV360editCustomerMatchResponse { - firstAndThirdPartyAudienceId: string + firstPartyAndPartnerAudienceId: string error: [ { code: string @@ -53,7 +53,7 @@ export const createAudienceRequest = ( membershipDurationDays: membershipDurationDays, description: description, audienceSource: 'AUDIENCE_SOURCE_UNSPECIFIED', - firstAndThirdPartyAudienceType: 'FIRST_AND_THIRD_PARTY_AUDIENCE_TYPE_FIRST_PARTY', + firstPartyAndPartnerAudienceType: 'TYPE_FIRST_PARTY', appId: appId } }) @@ -112,7 +112,7 @@ export async function editDeviceMobileIds( }, body: requestPayload }) - if (!response.data || !response.data.firstAndThirdPartyAudienceId) { + if (!response.data || !response.data.firstPartyAndPartnerAudienceId) { statsContext?.statsClient?.incr('addCustomerMatchMembers.error', 1, statsContext?.tags) throw new IntegrationError( `API returned error: ${response.data?.error || 'Unknown error'}`, @@ -125,54 +125,102 @@ export async function editDeviceMobileIds( return response.data } -export async function editContactInfo( - request: RequestClient, - payloads: Payload[], - 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 - ) { - return - } - - //Format the endpoint - const endpoint = DV360API + '/' + audienceId + ':editCustomerMatchMembers' - - // Prepare the request payload - const contactInfoList = { - contactInfos: [processPayload(payload)], +// Helper to build contactInfoList +function buildContactInfoList(contactInfos: Record[]): { + contactInfos: Record[] + consent: { adUserData: string; adPersonalization: string } +} { + return { + contactInfos, consent: { adUserData: CONSENT_STATUS_GRANTED, adPersonalization: CONSENT_STATUS_GRANTED } } +} - // Convert the payload to string if needed - const requestPayload = JSON.stringify({ - advertiserId: payload.advertiser_id, +// Helper to build request payload +function buildRequestPayload( + advertiserId: string, + contactInfoList: { + contactInfos: Record[] + consent: { adUserData: string; adPersonalization: string } + }, + operation: 'add' | 'remove' +) { + return JSON.stringify({ + advertiserId, ...(operation === 'add' ? { addedContactInfoList: contactInfoList } : {}), ...(operation === 'remove' ? { removedContactInfoList: contactInfoList } : {}) }) +} - const response = await request(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=utf-8' - }, - body: requestPayload - }) +export async function editContactInfo( + request: RequestClient, + payloads: Payload[], + operation: 'add' | 'remove', + statsContext?: StatsContext +) { + if (!payloads || payloads.length === 0) return - statsContext?.statsClient?.incr('addCustomerMatchMembers.success', 1, statsContext?.tags) - return response.data + const batchingEnabled = payloads.some((p) => p.enable_batching) + + // Filter valid payloads + const validPayloads = payloads.filter( + (payload) => + payload.emails !== undefined || + payload.phoneNumbers !== undefined || + payload.firstName !== undefined || + payload.lastName !== undefined + ) + if (validPayloads.length === 0) return + + if (batchingEnabled) { + // Assume all payloads are for the same audience/advertiser (use first) + const { external_id: audienceId, advertiser_id: advertiserId } = validPayloads[0] + if (!audienceId || !advertiserId) { + throw new IntegrationError( + 'Missing required audience or advertiser ID for batch request', + 'MISSING_REQUIRED_FIELD', + 400 + ) + } + const contactInfos = validPayloads.map(processPayload) + const contactInfoList = buildContactInfoList(contactInfos) + const requestPayload = buildRequestPayload(advertiserId, contactInfoList, operation) + const endpoint = DV360API + '/' + audienceId + ':editCustomerMatchMembers' + const response = await request(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + body: requestPayload + }) + statsContext?.statsClient?.incr('addCustomerMatchMembers.success', contactInfos.length, statsContext?.tags) + return response.data + } + + // Non-batching: process each payload individually + const results = [] + for (const payload of validPayloads) { + const { external_id: audienceId, advertiser_id: advertiserId } = payload + if (!audienceId || !advertiserId) { + throw new IntegrationError( + 'Missing required audience or advertiser ID for request', + 'MISSING_REQUIRED_FIELD', + 400 + ) + } + const contactInfoList = buildContactInfoList([processPayload(payload)]) + const requestPayload = buildRequestPayload(advertiserId, contactInfoList, operation) + const endpoint = DV360API + '/' + audienceId + ':editCustomerMatchMembers' + const response = await request(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + body: requestPayload + }) + statsContext?.statsClient?.incr('addCustomerMatchMembers.success', 1, statsContext?.tags) + results.push(response.data) + } + return results } function normalizeAndHash(data: string) { diff --git a/packages/destination-actions/src/destinations/first-party-dv360/index.ts b/packages/destination-actions/src/destinations/first-party-dv360/index.ts index 9b5d2c7b3a1..1fca59d0759 100644 --- a/packages/destination-actions/src/destinations/first-party-dv360/index.ts +++ b/packages/destination-actions/src/destinations/first-party-dv360/index.ts @@ -172,7 +172,7 @@ const destination: AudienceDestinationDefinition = { const r = await response.json() statsClient?.incr(`${statsName}.success`, 1, statsTags) return { - externalId: r.firstAndThirdPartyAudienceId + externalId: r.firstPartyAndPartnerAudienceId } }, @@ -236,7 +236,7 @@ const destination: AudienceDestinationDefinition = { const audienceData = await response.json() statsClient?.incr(`${statsName}.success`, 1, statsTags) return { - externalId: audienceData.firstAndThirdPartyAudienceId + externalId: audienceData.firstPartyAndPartnerAudienceId } } }, diff --git a/packages/destination-actions/src/destinations/first-party-dv360/removeFromAudContactInfo/_tests_/index.test.ts b/packages/destination-actions/src/destinations/first-party-dv360/removeFromAudContactInfo/_tests_/index.test.ts index 8dfbfec41e3..ef32e8c25a5 100644 --- a/packages/destination-actions/src/destinations/first-party-dv360/removeFromAudContactInfo/_tests_/index.test.ts +++ b/packages/destination-actions/src/destinations/first-party-dv360/removeFromAudContactInfo/_tests_/index.test.ts @@ -27,7 +27,7 @@ const event = createTestEvent({ describe('First-Party-dv360.removeToAudContactInfo', () => { it('should hash pii data if not already hashed', async () => { - nock('https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences') + nock('https://displayvideo.googleapis.com/v4/firstPartyAndPartnerAudiences') .post('/1234567890:editCustomerMatchMembers') .reply(200, { success: true }) @@ -53,7 +53,7 @@ describe('First-Party-dv360.removeToAudContactInfo', () => { }) it('should not hash pii data if already hashed', async () => { - nock('https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences') + nock('https://displayvideo.googleapis.com/v4/firstPartyAndPartnerAudiences') .post('/1234567890:editCustomerMatchMembers') .reply(200, { success: true })