From ca5568e5d06ab6dccef1b0f9c369d78a44ae7a8a Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Tue, 18 Jun 2024 18:41:16 +0530 Subject: [PATCH 1/3] chore: upgrade packages --- package-lock.json | 18 +++++++++--------- package.json | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 066dac2c88..c1ca7a6ca6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,8 +20,8 @@ "@ndhoule/extend": "^2.0.0", "@pyroscope/nodejs": "^0.2.9", "@rudderstack/integrations-lib": "^0.2.10", - "@rudderstack/json-template-engine": "^0.13.2", - "@rudderstack/workflow-engine": "^0.8.2", + "@rudderstack/json-template-engine": "^0.13.8", + "@rudderstack/workflow-engine": "^0.8.6", "@shopify/jest-koa-mocks": "^5.1.1", "ajv": "^8.12.0", "ajv-draft-04": "^1.0.0", @@ -4526,17 +4526,17 @@ } }, "node_modules/@rudderstack/json-template-engine": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@rudderstack/json-template-engine/-/json-template-engine-0.13.2.tgz", - "integrity": "sha512-uEyMv/qjm/mP5V8EifJzolvFLtka/dacmvwo9Xk3+MnEbsNN0YLu7Z/qWeyXeDF5chvy8JfaqV8lNgO3SxVG7g==" + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/@rudderstack/json-template-engine/-/json-template-engine-0.13.8.tgz", + "integrity": "sha512-2bl6a25SEm+LapdNqR5QhLv61dQiv3squmCr4Qy9U89BIp9yX9WOL8tvoaIS1isN7lwIOViD3cD8ft0ehlj8Sw==" }, "node_modules/@rudderstack/workflow-engine": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@rudderstack/workflow-engine/-/workflow-engine-0.8.2.tgz", - "integrity": "sha512-cjn3J8CUarAE3cbASRvkf7A2745Clzkw/ffqGLzD8+9KvTN6mC28Pm9c5169LPDmt+NMUMw0W5xHgNO3cV9eqQ==", + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/@rudderstack/workflow-engine/-/workflow-engine-0.8.6.tgz", + "integrity": "sha512-O+Kj3y4DZNgV2fWAOkwYwqsnjot0YxiVrDFWUkLdFQHTSXJ5Y/eqDJglDXdcpwSPg9AlaUFVHsGzw6BkAQLcIA==", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", - "@rudderstack/json-template-engine": "^0.13.2", + "@rudderstack/json-template-engine": "^0.13.8", "jsonata": "^2.0.5", "lodash": "^4.17.21", "object-sizeof": "^2.6.4", diff --git a/package.json b/package.json index 4c558430d9..5b0d74c650 100644 --- a/package.json +++ b/package.json @@ -65,8 +65,8 @@ "@ndhoule/extend": "^2.0.0", "@pyroscope/nodejs": "^0.2.9", "@rudderstack/integrations-lib": "^0.2.10", - "@rudderstack/json-template-engine": "^0.13.2", - "@rudderstack/workflow-engine": "^0.8.2", + "@rudderstack/json-template-engine": "^0.13.8", + "@rudderstack/workflow-engine": "^0.8.6", "@shopify/jest-koa-mocks": "^5.1.1", "ajv": "^8.12.0", "ajv-draft-04": "^1.0.0", From 60fee0e5a442c4f6c773234d88d86b447434de9b Mon Sep 17 00:00:00 2001 From: Vikas26021999 <79831638+Vikas26021999@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:24:08 +0530 Subject: [PATCH 2/3] feat: garl record event support (#3403) --- .../fb_custom_audience/recordTransform.js | 53 +- .../config.js | 2 +- .../recordTransform.js | 144 ++++ .../transform.js | 165 ++--- .../google_adwords_remarketing_lists/util.js | 122 ++++ .../util.test.js | 203 ++++++ src/v0/util/recordUtils.js | 55 ++ .../processor/data.ts | 140 ++-- .../router/audience.ts | 129 ++++ .../router/data.ts | 631 +++++++++++------- .../router/record.ts | 158 +++++ 11 files changed, 1329 insertions(+), 473 deletions(-) create mode 100644 src/v0/destinations/google_adwords_remarketing_lists/recordTransform.js create mode 100644 src/v0/destinations/google_adwords_remarketing_lists/util.js create mode 100644 src/v0/destinations/google_adwords_remarketing_lists/util.test.js create mode 100644 src/v0/util/recordUtils.js create mode 100644 test/integrations/destinations/google_adwords_remarketing_lists/router/audience.ts create mode 100644 test/integrations/destinations/google_adwords_remarketing_lists/router/record.ts diff --git a/src/v0/destinations/fb_custom_audience/recordTransform.js b/src/v0/destinations/fb_custom_audience/recordTransform.js index 0f7b65c0bf..62d4bd568b 100644 --- a/src/v0/destinations/fb_custom_audience/recordTransform.js +++ b/src/v0/destinations/fb_custom_audience/recordTransform.js @@ -1,11 +1,7 @@ /* eslint-disable no-const-assign */ const lodash = require('lodash'); const get = require('get-value'); -const { - InstrumentationError, - ConfigurationError, - getErrorRespEvents, -} = require('@rudderstack/integrations-lib'); +const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib'); const { schemaFields } = require('./config'); const { MappedToDestinationKey } = require('../../../constants'); const stats = require('../../../util/stats'); @@ -15,8 +11,8 @@ const { checkSubsetOfArray, returnArrayOfSubarrays, getSuccessRespEvents, - generateErrorObject, } = require('../../util'); +const { getErrorResponse, createFinalResponse } = require('../../util/recordUtils'); const { ensureApplicableFormat, getUpdatedDataElement, @@ -26,19 +22,6 @@ const { getDataSource, } = require('./util'); -function getErrorMetaData(inputs, acceptedOperations) { - const metadata = []; - // eslint-disable-next-line no-restricted-syntax - for (const key in inputs) { - if (!acceptedOperations.includes(key)) { - inputs[key].forEach((input) => { - metadata.push(input.metadata); - }); - } - } - return metadata; -} - const processRecordEventArray = ( recordChunksArray, userSchema, @@ -177,8 +160,6 @@ async function processRecordInputs(groupedRecordInputs) { record.message.action?.toLowerCase(), ); - const finalResponse = []; - let insertResponse; let deleteResponse; let updateResponse; @@ -238,32 +219,14 @@ async function processRecordInputs(groupedRecordInputs) { ); } - const eventTypes = ['update', 'insert', 'delete']; - const errorMetaData = []; - const errorMetaDataObject = getErrorMetaData(groupedRecordsByAction, eventTypes); - if (errorMetaDataObject.length > 0) { - errorMetaData.push(errorMetaDataObject); - } + const errorResponse = getErrorResponse(groupedRecordsByAction); - const error = new InstrumentationError('Invalid action type in record event'); - const errorObj = generateErrorObject(error); - const errorResponseList = errorMetaData.map((metadata) => - getErrorRespEvents(metadata, errorObj.status, errorObj.message, errorObj.statTags), + const finalResponse = createFinalResponse( + deleteResponse, + insertResponse, + updateResponse, + errorResponse, ); - - if (deleteResponse && deleteResponse.batchedRequest.length > 0) { - finalResponse.push(deleteResponse); - } - if (insertResponse && insertResponse.batchedRequest.length > 0) { - finalResponse.push(insertResponse); - } - if (updateResponse && updateResponse.batchedRequest.length > 0) { - finalResponse.push(updateResponse); - } - if (errorResponseList.length > 0) { - finalResponse.push(...errorResponseList); - } - if (finalResponse.length === 0) { throw new InstrumentationError( 'Missing valid parameters, unable to generate transformed payload', diff --git a/src/v0/destinations/google_adwords_remarketing_lists/config.js b/src/v0/destinations/google_adwords_remarketing_lists/config.js index f8983699c6..c7b97e0e6c 100644 --- a/src/v0/destinations/google_adwords_remarketing_lists/config.js +++ b/src/v0/destinations/google_adwords_remarketing_lists/config.js @@ -1,6 +1,6 @@ const { getMappingConfig } = require('../../util'); -const BASE_ENDPOINT = 'https://googleads.googleapis.com/v15/customers'; +const BASE_ENDPOINT = 'https://googleads.googleapis.com/v16/customers'; const CONFIG_CATEGORIES = { AUDIENCE_LIST: { type: 'audienceList', name: 'offlineDataJobs' }, ADDRESSINFO: { type: 'addressInfo', name: 'addressInfo' }, diff --git a/src/v0/destinations/google_adwords_remarketing_lists/recordTransform.js b/src/v0/destinations/google_adwords_remarketing_lists/recordTransform.js new file mode 100644 index 0000000000..18e7f96821 --- /dev/null +++ b/src/v0/destinations/google_adwords_remarketing_lists/recordTransform.js @@ -0,0 +1,144 @@ +/* eslint-disable no-const-assign */ +const lodash = require('lodash'); +const { InstrumentationError } = require('@rudderstack/integrations-lib'); +const { + getValueFromMessage, + getAccessToken, + constructPayload, + returnArrayOfSubarrays, + getSuccessRespEvents, +} = require('../../util'); +const { populateConsentFromConfig } = require('../../util/googleUtils'); +const { populateIdentifiers, responseBuilder } = require('./util'); +const { getErrorResponse, createFinalResponse } = require('../../util/recordUtils'); +const { offlineDataJobsMapping, consentConfigMap } = require('./config'); + +const processRecordEventArray = ( + records, + message, + destination, + accessToken, + developerToken, + operationType, +) => { + let outputPayloads = {}; + // ** only send it if identifier > 0 + + const fieldsArray = []; + const metadata = []; + records.forEach((record) => { + fieldsArray.push(record.message.fields); + metadata.push(record.metadata); + }); + + const userIdentifiersList = populateIdentifiers(fieldsArray, destination); + + const outputPayload = constructPayload(message, offlineDataJobsMapping); + outputPayload.operations = []; + // breaking the userIdentiFier array in chunks of 20 + const userIdentifierChunks = returnArrayOfSubarrays(userIdentifiersList, 20); + // putting each chunk in different create/remove operations + switch (operationType) { + case 'add': + // for add operation + userIdentifierChunks.forEach((element) => { + const operations = { + create: {}, + }; + operations.create.userIdentifiers = element; + outputPayload.operations.push(operations); + }); + outputPayloads = { ...outputPayloads, create: outputPayload }; + break; + case 'remove': + // for remove operation + userIdentifierChunks.forEach((element) => { + const operations = { + remove: {}, + }; + operations.remove.userIdentifiers = element; + outputPayload.operations.push(operations); + }); + outputPayloads = { ...outputPayloads, remove: outputPayload }; + break; + default: + } + + const toSendEvents = []; + Object.values(outputPayloads).forEach((data) => { + const consentObj = populateConsentFromConfig(destination.Config, consentConfigMap); + toSendEvents.push( + responseBuilder(accessToken, developerToken, data, destination, message, consentObj), + ); + }); + + const successResponse = getSuccessRespEvents(toSendEvents, metadata, destination, true); + + return successResponse; +}; + +async function processRecordInputs(groupedRecordInputs) { + const { destination, message, metadata } = groupedRecordInputs[0]; + const accessToken = getAccessToken(metadata, 'accessToken'); + const developerToken = getValueFromMessage(metadata, 'secret.developer_token'); + + const groupedRecordsByAction = lodash.groupBy(groupedRecordInputs, (record) => + record.message.action?.toLowerCase(), + ); + + let insertResponse; + let deleteResponse; + let updateResponse; + + if (groupedRecordsByAction.delete) { + deleteResponse = processRecordEventArray( + groupedRecordsByAction.delete, + message, + destination, + accessToken, + developerToken, + 'remove', + ); + } + + if (groupedRecordsByAction.insert) { + insertResponse = processRecordEventArray( + groupedRecordsByAction.insert, + message, + destination, + accessToken, + developerToken, + 'add', + ); + } + + if (groupedRecordsByAction.update) { + updateResponse = processRecordEventArray( + groupedRecordsByAction.update, + message, + destination, + accessToken, + developerToken, + 'add', + ); + } + + const errorResponse = getErrorResponse(groupedRecordsByAction); + const finalResponse = createFinalResponse( + deleteResponse, + insertResponse, + updateResponse, + errorResponse, + ); + if (finalResponse.length === 0) { + throw new InstrumentationError( + 'Missing valid parameters, unable to generate transformed payload', + ); + } + + return finalResponse; +} + +module.exports = { + processRecordInputs, +}; diff --git a/src/v0/destinations/google_adwords_remarketing_lists/transform.js b/src/v0/destinations/google_adwords_remarketing_lists/transform.js index b0dfaa0c35..d879a39c63 100644 --- a/src/v0/destinations/google_adwords_remarketing_lists/transform.js +++ b/src/v0/destinations/google_adwords_remarketing_lists/transform.js @@ -1,138 +1,29 @@ -const sha256 = require('sha256'); -const get = require('get-value'); +const lodash = require('lodash'); const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib'); const logger = require('../../../logger'); const { - isDefinedAndNotNullAndNotEmpty, returnArrayOfSubarrays, constructPayload, - defaultRequestConfig, getValueFromMessage, - removeUndefinedAndNullValues, - removeHyphens, simpleProcessRouterDest, - getDestinationExternalIDInfoForRetl, getAccessToken, } = require('../../util'); const { populateConsentFromConfig } = require('../../util/googleUtils'); - -const { - offlineDataJobsMapping, - addressInfoMapping, - BASE_ENDPOINT, - attributeMapping, - hashAttributes, - TYPEOFLIST, - consentConfigMap, -} = require('./config'); -const { JSON_MIME_TYPE } = require('../../util/constant'); -const { MappedToDestinationKey } = require('../../../constants'); - -const hashEncrypt = (object) => { - Object.keys(object).forEach((key) => { - if (hashAttributes.includes(key) && object[key]) { - // eslint-disable-next-line no-param-reassign - object[key] = sha256(object[key]); +const { offlineDataJobsMapping, consentConfigMap } = require('./config'); +const { processRecordInputs } = require('./recordTransform'); +const { populateIdentifiers, responseBuilder } = require('./util'); + +function extraKeysPresent(dictionary, keyList) { + // eslint-disable-next-line no-restricted-syntax + for (const key in dictionary) { + if (!keyList.includes(key)) { + return true; } - }); -}; - -/** - * This function is used for building the response. It create a default rudder response - * and populate headers, params and body.JSON - * @param {*} metadata - * @param {*} body - * @param {*} param2 - * @returns - */ -const responseBuilder = (metadata, body, { Config }, message, consentBlock) => { - const payload = body; - const response = defaultRequestConfig(); - const filteredCustomerId = removeHyphens(Config.customerId); - response.endpoint = `${BASE_ENDPOINT}/${filteredCustomerId}/offlineUserDataJobs`; - response.body.JSON = removeUndefinedAndNullValues(payload); - const accessToken = getAccessToken(metadata, 'access_token'); - let operationAudienceId = Config.audienceId || Config.listId; - const mappedToDestination = get(message, MappedToDestinationKey); - if (!operationAudienceId && mappedToDestination) { - const { objectType } = getDestinationExternalIDInfoForRetl( - message, - 'GOOGLE_ADWORDS_REMARKETING_LISTS', - ); - operationAudienceId = objectType; - } - if (!isDefinedAndNotNullAndNotEmpty(operationAudienceId)) { - throw new ConfigurationError('List ID is a mandatory field'); } - response.params = { - listId: operationAudienceId, - customerId: filteredCustomerId, - consent: consentBlock, - }; - response.headers = { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': JSON_MIME_TYPE, - 'developer-token': getValueFromMessage(metadata, 'secret.developer_token'), - }; - if (Config.subAccount) - if (Config.loginCustomerId) { - const filteredLoginCustomerId = removeHyphens(Config.loginCustomerId); - response.headers['login-customer-id'] = filteredLoginCustomerId; - } else throw new ConfigurationError(`loginCustomerId is required as subAccount is true.`); - return response; -}; -/** - * This function helps creates an array with proper mapping for userIdentiFier. - * Logics: Here we are creating an array with all the attributes provided in the add/remove array - * inside listData. - * @param {rudder event message properties listData add} attributeArray - * @param {rudder event destination} Config - * @returns - */ + return false; +} -const populateIdentifiers = (attributeArray, { Config }) => { - const userIdentifier = []; - const { typeOfList } = Config; - const { isHashRequired, userSchema } = Config; - let attribute; - if (TYPEOFLIST[typeOfList]) { - attribute = TYPEOFLIST[typeOfList]; - } else { - attribute = userSchema; - } - if (isDefinedAndNotNullAndNotEmpty(attributeArray)) { - // traversing through every element in the add array - attributeArray.forEach((element, index) => { - if (isHashRequired) { - hashEncrypt(element); - } - // checking if the attribute is an array or not for generic type list - if (!Array.isArray(attribute)) { - if (element[attribute]) { - userIdentifier.push({ [attribute]: element[attribute] }); - } else { - logger.info(` ${attribute} is not present in index:`, index); - } - } else { - attribute.forEach((attributeElement, index2) => { - if (attributeElement === 'addressInfo') { - const addressInfo = constructPayload(element, addressInfoMapping); - // checking if addressInfo object is empty or not. - if (isDefinedAndNotNullAndNotEmpty(addressInfo)) userIdentifier.push({ addressInfo }); - } else if (element[`${attributeElement}`]) { - userIdentifier.push({ - [`${attributeMapping[attributeElement]}`]: element[`${attributeElement}`], - }); - } else { - logger.info(` ${attribute[index2]} is not present in index:`, index); - } - }); - } - }); - } - return userIdentifier; -}; /** * This function helps to create different operations by breaking the * userIdentiFier Array in chunks of 20. @@ -143,7 +34,6 @@ const populateIdentifiers = (attributeArray, { Config }) => { * @param {rudder event destination} destination * @returns */ - const createPayload = (message, destination) => { const { listData } = message.properties; const properties = ['add', 'remove']; @@ -218,9 +108,14 @@ const processEvent = async (metadata, message, destination) => { ); } + const accessToken = getAccessToken(metadata, 'accessToken'); + const developerToken = getValueFromMessage(metadata, 'secret.developer_token'); + Object.values(createdPayload).forEach((data) => { const consentObj = populateConsentFromConfig(destination.Config, consentConfigMap); - response.push(responseBuilder(metadata, data, destination, message, consentObj)); + response.push( + responseBuilder(accessToken, developerToken, data, destination, message, consentObj), + ); }); return response; } @@ -231,7 +126,29 @@ const processEvent = async (metadata, message, destination) => { const process = async (event) => processEvent(event.metadata, event.message, event.destination); const processRouterDest = async (inputs, reqMetadata) => { - const respList = await simpleProcessRouterDest(inputs, process, reqMetadata); + const respList = []; + const groupedInputs = lodash.groupBy(inputs, (input) => input.message.type?.toLowerCase()); + let transformedRecordEvent = []; + let transformedAudienceEvent = []; + + const eventTypes = ['record', 'audiencelist']; + if (extraKeysPresent(groupedInputs, eventTypes)) { + throw new ConfigurationError('unsupported events present in the event'); + } + + if (groupedInputs.record) { + transformedRecordEvent = await processRecordInputs(groupedInputs.record, reqMetadata); + } + + if (groupedInputs.audiencelist) { + transformedAudienceEvent = await simpleProcessRouterDest( + groupedInputs.audiencelist, + process, + reqMetadata, + ); + } + + respList.push(...transformedRecordEvent, ...transformedAudienceEvent); return respList; }; diff --git a/src/v0/destinations/google_adwords_remarketing_lists/util.js b/src/v0/destinations/google_adwords_remarketing_lists/util.js new file mode 100644 index 0000000000..3e04dd8f6f --- /dev/null +++ b/src/v0/destinations/google_adwords_remarketing_lists/util.js @@ -0,0 +1,122 @@ +const get = require('get-value'); +const sha256 = require('sha256'); +const { ConfigurationError } = require('@rudderstack/integrations-lib'); +const { + isDefinedAndNotNullAndNotEmpty, + constructPayload, + defaultRequestConfig, + removeHyphens, + removeUndefinedAndNullValues, + getDestinationExternalIDInfoForRetl, +} = require('../../util'); +const logger = require('../../../logger'); +const { MappedToDestinationKey } = require('../../../constants'); +const { JSON_MIME_TYPE } = require('../../util/constant'); +const { + addressInfoMapping, + attributeMapping, + TYPEOFLIST, + BASE_ENDPOINT, + hashAttributes, +} = require('./config'); + +const hashEncrypt = (object) => { + Object.keys(object).forEach((key) => { + if (hashAttributes.includes(key) && object[key]) { + // eslint-disable-next-line no-param-reassign + object[key] = sha256(object[key]); + } + }); +}; + +const responseBuilder = (accessToken, developerToken, body, { Config }, message, consentBlock) => { + const payload = body; + const response = defaultRequestConfig(); + const filteredCustomerId = removeHyphens(Config.customerId); + response.endpoint = `${BASE_ENDPOINT}/${filteredCustomerId}/offlineUserDataJobs`; + response.body.JSON = removeUndefinedAndNullValues(payload); + let operationAudienceId = Config.audienceId || Config.listId; + const mappedToDestination = get(message, MappedToDestinationKey); + if (!operationAudienceId && mappedToDestination) { + const { objectType } = getDestinationExternalIDInfoForRetl( + message, + 'GOOGLE_ADWORDS_REMARKETING_LISTS', + ); + operationAudienceId = objectType; + } + if (!isDefinedAndNotNullAndNotEmpty(operationAudienceId)) { + throw new ConfigurationError('List ID is a mandatory field'); + } + response.params = { + listId: operationAudienceId, + customerId: filteredCustomerId, + consent: consentBlock, + }; + response.headers = { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': JSON_MIME_TYPE, + 'developer-token': developerToken, + }; + if (Config.subAccount) + if (Config.loginCustomerId) { + const filteredLoginCustomerId = removeHyphens(Config.loginCustomerId); + response.headers['login-customer-id'] = filteredLoginCustomerId; + } else throw new ConfigurationError(`loginCustomerId is required as subAccount is true.`); + return response; +}; + +/** + * This function helps creates an array with proper mapping for userIdentiFier. + * Logics: Here we are creating an array with all the attributes provided in the add/remove array + * inside listData. + * @param {rudder event message properties listData add} attributeArray + * @param {rudder event destination} Config + * @returns + */ +const populateIdentifiers = (attributeArray, { Config }) => { + const userIdentifier = []; + const { typeOfList } = Config; + const { isHashRequired, userSchema } = Config; + let attribute; + if (TYPEOFLIST[typeOfList]) { + attribute = TYPEOFLIST[typeOfList]; + } else { + attribute = userSchema; + } + if (isDefinedAndNotNullAndNotEmpty(attributeArray)) { + // traversing through every element in the add array + attributeArray.forEach((element, index) => { + if (isHashRequired) { + hashEncrypt(element); + } + // checking if the attribute is an array or not for generic type list + if (!Array.isArray(attribute)) { + if (element[attribute]) { + userIdentifier.push({ [attribute]: element[attribute] }); + } else { + logger.info(` ${attribute} is not present in index:`, index); + } + } else { + attribute.forEach((attributeElement, index2) => { + if (attributeElement === 'addressInfo') { + const addressInfo = constructPayload(element, addressInfoMapping); + // checking if addressInfo object is empty or not. + if (isDefinedAndNotNullAndNotEmpty(addressInfo)) userIdentifier.push({ addressInfo }); + } else if (element[`${attributeElement}`]) { + userIdentifier.push({ + [`${attributeMapping[attributeElement]}`]: element[`${attributeElement}`], + }); + } else { + logger.info(` ${attribute[index2]} is not present in index:`, index); + } + }); + } + }); + } + return userIdentifier; +}; + +module.exports = { + populateIdentifiers, + responseBuilder, +}; diff --git a/src/v0/destinations/google_adwords_remarketing_lists/util.test.js b/src/v0/destinations/google_adwords_remarketing_lists/util.test.js new file mode 100644 index 0000000000..e9fe90c317 --- /dev/null +++ b/src/v0/destinations/google_adwords_remarketing_lists/util.test.js @@ -0,0 +1,203 @@ +const { populateIdentifiers, responseBuilder } = require('./util'); + +const accessToken = 'abcd1234'; +const developerToken = 'ijkl9101'; +const body = { + operations: [ + { + create: { + userIdentifiers: [ + { + hashedEmail: 'd3142c8f9c9129484daf28df80cc5c955791efed5e69afabb603bc8cb9ffd419', + }, + { + hashedPhoneNumber: '8846dcb6ab2d73a0e67dbd569fa17cec2d9d391e5b05d1dd42919bc21ae82c45', + }, + { + addressInfo: { + hashedFirstName: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + hashedLastName: 'dcf000c2386fb76d22cefc0d118a8511bb75999019cd373df52044bccd1bd251', + countryCode: 'US', + postalCode: '1245', + }, + }, + ], + }, + }, + ], +}; +const baseDestination = { + Config: { + rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', + listId: '7090784486', + customerId: '7693729833', + loginCustomerId: '', + subAccount: false, + userSchema: ['email', 'phone', 'addressInfo'], + isHashRequired: true, + typeOfList: 'General', + }, +}; +const consentBlock = { + adPersonalization: 'UNSPECIFIED', + adUserData: 'UNSPECIFIED', +}; +const message = { + action: 'insert', + context: { + ip: '14.5.67.21', + library: { + name: 'http', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + email: 'd3142c8f9c9129484daf28df80cc5c955791efed5e69afabb603bc8cb9ffd419', + phone: '8846dcb6ab2d73a0e67dbd569fa17cec2d9d391e5b05d1dd42919bc21ae82c45', + firstName: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + lastName: 'dcf000c2386fb76d22cefc0d118a8511bb75999019cd373df52044bccd1bd251', + country: 'US', + postalCode: '1245', + }, + type: 'record', +}; +const expectedResponse = { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', + headers: { + Authorization: 'Bearer abcd1234', + 'Content-Type': 'application/json', + 'developer-token': 'ijkl9101', + }, + params: { + listId: '7090784486', + customerId: '7693729833', + consent: { + adPersonalization: 'UNSPECIFIED', + adUserData: 'UNSPECIFIED', + }, + }, + body: { + JSON: { + operations: [ + { + create: { + userIdentifiers: [ + { + hashedEmail: 'd3142c8f9c9129484daf28df80cc5c955791efed5e69afabb603bc8cb9ffd419', + }, + { + hashedPhoneNumber: + '8846dcb6ab2d73a0e67dbd569fa17cec2d9d391e5b05d1dd42919bc21ae82c45', + }, + { + addressInfo: { + hashedFirstName: + '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + hashedLastName: + 'dcf000c2386fb76d22cefc0d118a8511bb75999019cd373df52044bccd1bd251', + countryCode: 'US', + postalCode: '1245', + }, + }, + ], + }, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, +}; +const attributeArray = [ + { + email: 'test@abc.com', + phone: '@09876543210', + firstName: 'test', + lastName: 'rudderlabs', + country: 'US', + postalCode: '1245', + }, +]; + +const hashedArray = [ + { + hashedEmail: 'd3142c8f9c9129484daf28df80cc5c955791efed5e69afabb603bc8cb9ffd419', + }, + { + hashedPhoneNumber: '8846dcb6ab2d73a0e67dbd569fa17cec2d9d391e5b05d1dd42919bc21ae82c45', + }, + { + addressInfo: { + hashedFirstName: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + hashedLastName: 'dcf000c2386fb76d22cefc0d118a8511bb75999019cd373df52044bccd1bd251', + countryCode: 'US', + postalCode: '1245', + }, + }, +]; + +describe('GARL utils test', () => { + describe('responseBuilder function tests', () => { + it('Should return correct response for given payload', () => { + const response = responseBuilder( + accessToken, + developerToken, + body, + baseDestination, + message, + consentBlock, + ); + expect(response).toEqual(expectedResponse); + }); + + it('Should throw error if subaccount is true and loginCustomerId is not defined', () => { + try { + const destination2 = Object.create(baseDestination); + destination2.Config.subAccount = true; + destination2.Config.loginCustomerId = ''; + const response = responseBuilder( + accessToken, + developerToken, + body, + destination2, + message, + consentBlock, + ); + expect(response).toEqual(); + } catch (error) { + expect(error.message).toEqual(`loginCustomerId is required as subAccount is true.`); + } + }); + + it('Should throw error if operationAudienceId is not defined', () => { + try { + const destination1 = Object.create(baseDestination); + destination1.Config.listId = ''; + const response = responseBuilder( + accessToken, + developerToken, + body, + destination1, + message, + consentBlock, + ); + expect(response).toEqual(); + } catch (error) { + expect(error.message).toEqual(`List ID is a mandatory field`); + } + }); + }); + + describe('populateIdentifiers function tests', () => { + it('Should hash and return identifiers for a given list of attributes', () => { + const identifier = populateIdentifiers(attributeArray, baseDestination); + expect(identifier).toEqual(hashedArray); + }); + }); +}); diff --git a/src/v0/util/recordUtils.js b/src/v0/util/recordUtils.js new file mode 100644 index 0000000000..a3dd65ef33 --- /dev/null +++ b/src/v0/util/recordUtils.js @@ -0,0 +1,55 @@ +const { InstrumentationError, getErrorRespEvents } = require('@rudderstack/integrations-lib'); +const { generateErrorObject } = require('./index'); + +const eventTypes = ['update', 'insert', 'delete']; + +function getErrorMetaData(inputs, acceptedOperations) { + const metadata = []; + // eslint-disable-next-line no-restricted-syntax + for (const key in inputs) { + if (!acceptedOperations.includes(key)) { + inputs[key].forEach((input) => { + metadata.push(input.metadata); + }); + } + } + return metadata; +} + +function getErrorResponse(groupedRecordsByAction) { + const errorMetaData = []; + const errorMetaDataObject = getErrorMetaData(groupedRecordsByAction, eventTypes); + if (errorMetaDataObject.length > 0) { + errorMetaData.push(errorMetaDataObject); + } + + const error = new InstrumentationError('Invalid action type in record event'); + const errorObj = generateErrorObject(error); + const errorResponseList = errorMetaData.map((data) => + getErrorRespEvents(data, errorObj.status, errorObj.message, errorObj.statTags), + ); + + return errorResponseList; +} + +function createFinalResponse(deleteResponse, insertResponse, updateResponse, errorResponseList) { + const finalResponse = []; + if (deleteResponse && deleteResponse.batchedRequest.length > 0) { + finalResponse.push(deleteResponse); + } + if (insertResponse && insertResponse.batchedRequest.length > 0) { + finalResponse.push(insertResponse); + } + if (updateResponse && updateResponse.batchedRequest.length > 0) { + finalResponse.push(updateResponse); + } + if (errorResponseList.length > 0) { + finalResponse.push(...errorResponseList); + } + return finalResponse; +} + +module.exports = { + getErrorResponse, + createFinalResponse, +}; diff --git a/test/integrations/destinations/google_adwords_remarketing_lists/processor/data.ts b/test/integrations/destinations/google_adwords_remarketing_lists/processor/data.ts index a846e0370d..639e28403c 100644 --- a/test/integrations/destinations/google_adwords_remarketing_lists/processor/data.ts +++ b/test/integrations/destinations/google_adwords_remarketing_lists/processor/data.ts @@ -11,7 +11,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -70,7 +70,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -120,7 +120,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -143,7 +143,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -203,7 +203,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -239,7 +239,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -262,7 +262,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -323,7 +323,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -358,7 +358,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -381,7 +381,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -456,7 +456,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -489,7 +489,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -546,7 +546,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -579,7 +579,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -635,7 +635,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -667,7 +667,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -711,7 +711,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -743,7 +743,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -807,7 +807,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -839,7 +839,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -1425,7 +1425,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -2712,7 +2712,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -2735,7 +2735,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -2820,7 +2820,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -2887,7 +2887,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -2900,7 +2900,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -2967,7 +2967,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -2990,7 +2990,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -4113,7 +4113,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -5400,7 +5400,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -5413,7 +5413,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -6700,7 +6700,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -6723,7 +6723,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -6790,7 +6790,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -6857,7 +6857,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -6880,7 +6880,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -6944,7 +6944,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -6977,7 +6977,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -8100,7 +8100,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -9387,7 +9387,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -9400,7 +9400,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -10687,7 +10687,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -10710,7 +10710,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -10795,7 +10795,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -10862,7 +10862,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -10875,7 +10875,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -10942,7 +10942,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -10965,7 +10965,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -11050,7 +11050,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -11115,7 +11115,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -11128,7 +11128,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -11190,7 +11190,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -11213,7 +11213,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -11272,7 +11272,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -11316,7 +11316,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -11419,7 +11419,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -11479,7 +11479,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -11523,7 +11523,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -11546,7 +11546,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -11618,7 +11618,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -11661,7 +11661,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -11684,7 +11684,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -11756,7 +11756,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -11799,7 +11799,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -11822,7 +11822,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -11896,7 +11896,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -11942,7 +11942,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -11965,7 +11965,7 @@ export const data = [ { metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, @@ -12039,7 +12039,7 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { Authorization: 'Bearer dummy-access', 'Content-Type': 'application/json', @@ -12082,7 +12082,7 @@ export const data = [ }, metadata: { secret: { - access_token: 'dummy-access', + accessToken: 'dummy-access', refresh_token: 'dummy-refresh', developer_token: 'dummy-dev-token', }, diff --git a/test/integrations/destinations/google_adwords_remarketing_lists/router/audience.ts b/test/integrations/destinations/google_adwords_remarketing_lists/router/audience.ts new file mode 100644 index 0000000000..233f160ad3 --- /dev/null +++ b/test/integrations/destinations/google_adwords_remarketing_lists/router/audience.ts @@ -0,0 +1,129 @@ +import { Destination, RouterTransformationRequest } from '../../../../../src/types'; +import { generateMetadata } from '../../../testUtils'; + +const destination: Destination = { + Config: { + rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', + listId: '7090784486', + customerId: '7693729833', + loginCustomerId: '', + subAccount: false, + userSchema: ['email', 'phone', 'addressInfo'], + isHashRequired: true, + typeOfList: 'General', + }, + ID: '1mMy5cqbtfuaKZv1IhVQKnBdVwe', + Name: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + Enabled: true, + WorkspaceID: '1TSN08muJTZwH8iCDmnnRt1pmLd', + DestinationDefinition: { + ID: '1aIXqM806xAVm92nx07YwKbRrO9', + Name: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + DisplayName: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + Config: {}, + }, + Transformations: [], + IsConnectionEnabled: true, + IsProcessorEnabled: true, +}; + +export const rETLAudienceRouterRequest: RouterTransformationRequest = { + input: [ + { + metadata: generateMetadata(1), + destination: destination, + message: { + userId: 'user 1', + anonymousId: 'anon-id-new', + event: 'event1', + type: 'audiencelist', + properties: { + listData: { + add: [ + { + email: 'test@abc.com', + phone: '@09876543210', + firstName: 'test', + lastName: 'rudderlabs', + country: 'US', + postalCode: '1245', + }, + ], + }, + enablePartialFailure: true, + }, + context: { ip: '14.5.67.21', library: { name: 'http' } }, + timestamp: '2020-02-02T00:23:09.544Z', + }, + }, + { + metadata: generateMetadata(3), + destination: destination, + message: { + userId: 'user 1', + anonymousId: 'anon-id-new', + event: 'event1', + type: 'audiencelist', + properties: { + listData: { + remove: [ + { + email: 'test@abc.com', + phone: '@09876543210', + firstName: 'test', + lastName: 'rudderlabs', + country: 'US', + postalCode: '1245', + }, + ], + }, + enablePartialFailure: true, + }, + context: { ip: '14.5.67.21', library: { name: 'http' } }, + timestamp: '2020-02-02T00:23:09.544Z', + }, + }, + { + metadata: generateMetadata(4), + destination: destination, + message: { + userId: 'user 1', + anonymousId: 'anon-id-new', + event: 'event1', + type: 'audiencelist', + properties: { + listData: { + remove: [ + { + email: 'test@abc.com', + phone: '@09876543210', + firstName: 'test', + lastName: 'rudderlabs', + country: 'US', + postalCode: '1245', + }, + ], + add: [ + { + email: 'test@abc.com', + phone: '@09876543210', + firstName: 'test', + lastName: 'rudderlabs', + country: 'US', + postalCode: '1245', + }, + ], + }, + enablePartialFailure: true, + }, + context: { ip: '14.5.67.21', library: { name: 'http' } }, + timestamp: '2020-02-02T00:23:09.544Z', + }, + }, + ], + destType: 'google_adwords_remarketing_lists', +}; + +module.exports = { + rETLAudienceRouterRequest, +}; diff --git a/test/integrations/destinations/google_adwords_remarketing_lists/router/data.ts b/test/integrations/destinations/google_adwords_remarketing_lists/router/data.ts index 31d5c72694..35da4daff5 100644 --- a/test/integrations/destinations/google_adwords_remarketing_lists/router/data.ts +++ b/test/integrations/destinations/google_adwords_remarketing_lists/router/data.ts @@ -1,3 +1,6 @@ +import { rETLAudienceRouterRequest } from './audience'; +import { rETLRecordRouterRequest } from './record'; + export const data = [ { name: 'google_adwords_remarketing_lists', @@ -7,157 +10,85 @@ export const data = [ version: 'v0', input: { request: { + body: rETLAudienceRouterRequest, + method: 'POST', + }, + }, + output: { + response: { + status: 200, body: { - input: [ + output: [ { - metadata: { - secret: { - access_token: 'abcd1234', - refresh_token: 'efgh5678', - developer_token: 'ijkl9101', - }, - jobId: 1, - userId: 'u1', - }, - destination: { - Config: { - rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', - listId: '7090784486', - customerId: '7693729833', - loginCustomerId: '', - subAccount: false, - userSchema: ['email', 'phone', 'addressInfo'], - isHashRequired: true, - typeOfList: 'General', - }, - }, - message: { - userId: 'user 1', - anonymousId: 'anon-id-new', - event: 'event1', - type: 'audiencelist', - properties: { - listData: { - add: [ - { - email: 'test@abc.com', - phone: '@09876543210', - firstName: 'test', - lastName: 'rudderlabs', - country: 'US', - postalCode: '1245', - }, - ], + batchedRequest: [ + { + version: '1', + type: 'REST', + method: 'POST', + endpoint: + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', + headers: { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', }, - enablePartialFailure: true, - }, - context: { ip: '14.5.67.21', library: { name: 'http' } }, - timestamp: '2020-02-02T00:23:09.544Z', - }, - }, - { - metadata: { - secret: { - access_token: 'abcd1234', - refresh_token: 'efgh5678', - developer_token: 'ijkl9101', - }, - jobId: 2, - userId: 'u1', - }, - destination: { - Config: { - rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', - listId: '7090784486', - customerId: '7693729833', - loginCustomerId: '', - subAccount: false, - userSchema: ['email', 'phone', 'addressInfo'], - isHashRequired: true, - typeOfList: 'userID', - }, - }, - message: { - userId: 'user 1', - anonymousId: 'anon-id-new', - event: 'event1', - type: 'audiencelist', - properties: { - listData: { - add: [ - { - email: 'test@abc.com', - phone: '@09876543210', - firstName: 'test', - lastName: 'rudderlabs', - country: 'US', - postalCode: '1245', - thirdPartyUserId: 'useri1234', - }, - ], + params: { + listId: '7090784486', + customerId: '7693729833', + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, - enablePartialFailure: true, - }, - context: { ip: '14.5.67.21', library: { name: 'http' } }, - timestamp: '2020-02-02T00:23:09.544Z', - }, - }, - { - metadata: { - secret: { - access_token: 'abcd1234', - refresh_token: 'efgh5678', - developer_token: 'ijkl9101', - }, - jobId: 3, - userId: 'u1', - }, - destination: { - Config: { - rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', - listId: '7090784486', - customerId: '7693729833', - loginCustomerId: '', - subAccount: false, - userSchema: ['email', 'phone', 'addressInfo'], - isHashRequired: true, - typeOfList: 'General', - }, - }, - message: { - userId: 'user 1', - anonymousId: 'anon-id-new', - event: 'event1', - type: 'audiencelist', - properties: { - listData: { - remove: [ - { - email: 'test@abc.com', - phone: '@09876543210', - firstName: 'test', - lastName: 'rudderlabs', - country: 'US', - postalCode: '1245', - }, - ], + body: { + JSON: { + enablePartialFailure: true, + operations: [ + { + create: { + userIdentifiers: [ + { + hashedEmail: + 'd3142c8f9c9129484daf28df80cc5c955791efed5e69afabb603bc8cb9ffd419', + }, + { + hashedPhoneNumber: + '8846dcb6ab2d73a0e67dbd569fa17cec2d9d391e5b05d1dd42919bc21ae82c45', + }, + { + addressInfo: { + hashedFirstName: + '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + hashedLastName: + 'dcf000c2386fb76d22cefc0d118a8511bb75999019cd373df52044bccd1bd251', + countryCode: 'US', + postalCode: '1245', + }, + }, + ], + }, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, }, - enablePartialFailure: true, + files: {}, }, - context: { ip: '14.5.67.21', library: { name: 'http' } }, - timestamp: '2020-02-02T00:23:09.544Z', - }, - }, - { - metadata: { - secret: { - access_token: 'abcd1234', - refresh_token: 'efgh5678', - developer_token: 'ijkl9101', + ], + metadata: [ + { + attemptNum: 1, + destinationId: 'default-destinationId', + dontBatch: false, + secret: { + accessToken: 'default-accessToken', + }, + sourceId: 'default-sourceId', + userId: 'default-userId', + workspaceId: 'default-workspaceId', + jobId: 1, }, - jobId: 4, - userId: 'u1', - }, + ], + batched: false, + statusCode: 200, destination: { Config: { rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', @@ -169,52 +100,21 @@ export const data = [ isHashRequired: true, typeOfList: 'General', }, - }, - message: { - userId: 'user 1', - anonymousId: 'anon-id-new', - event: 'event1', - type: 'audiencelist', - properties: { - listData: { - remove: [ - { - email: 'test@abc.com', - phone: '@09876543210', - firstName: 'test', - lastName: 'rudderlabs', - country: 'US', - postalCode: '1245', - }, - ], - add: [ - { - email: 'test@abc.com', - phone: '@09876543210', - firstName: 'test', - lastName: 'rudderlabs', - country: 'US', - postalCode: '1245', - }, - ], - }, - enablePartialFailure: true, + DestinationDefinition: { + Config: {}, + DisplayName: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + ID: '1aIXqM806xAVm92nx07YwKbRrO9', + Name: 'GOOGLE_ADWORDS_REMARKETING_LISTS', }, - context: { ip: '14.5.67.21', library: { name: 'http' } }, - timestamp: '2020-02-02T00:23:09.544Z', + Enabled: true, + ID: '1mMy5cqbtfuaKZv1IhVQKnBdVwe', + IsConnectionEnabled: true, + IsProcessorEnabled: true, + Name: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + Transformations: [], + WorkspaceID: '1TSN08muJTZwH8iCDmnnRt1pmLd', }, }, - ], - destType: 'google_adwords_remarketing_lists', - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: [ { batchedRequest: [ { @@ -222,11 +122,10 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { - Authorization: 'Bearer abcd1234', + Authorization: 'Bearer default-accessToken', 'Content-Type': 'application/json', - 'developer-token': 'ijkl9101', }, params: { listId: '7090784486', @@ -238,7 +137,7 @@ export const data = [ enablePartialFailure: true, operations: [ { - create: { + remove: { userIdentifiers: [ { hashedEmail: @@ -272,13 +171,16 @@ export const data = [ ], metadata: [ { + attemptNum: 1, + destinationId: 'default-destinationId', + dontBatch: false, secret: { - access_token: 'abcd1234', - refresh_token: 'efgh5678', - developer_token: 'ijkl9101', + accessToken: 'default-accessToken', }, - jobId: 1, - userId: 'u1', + sourceId: 'default-sourceId', + userId: 'default-userId', + workspaceId: 'default-workspaceId', + jobId: 3, }, ], batched: false, @@ -294,6 +196,19 @@ export const data = [ isHashRequired: true, typeOfList: 'General', }, + DestinationDefinition: { + Config: {}, + DisplayName: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + ID: '1aIXqM806xAVm92nx07YwKbRrO9', + Name: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + }, + Enabled: true, + ID: '1mMy5cqbtfuaKZv1IhVQKnBdVwe', + IsConnectionEnabled: true, + IsProcessorEnabled: true, + Name: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + Transformations: [], + WorkspaceID: '1TSN08muJTZwH8iCDmnnRt1pmLd', }, }, { @@ -303,11 +218,10 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { - Authorization: 'Bearer abcd1234', + Authorization: 'Bearer default-accessToken', 'Content-Type': 'application/json', - 'developer-token': 'ijkl9101', }, params: { listId: '7090784486', @@ -318,7 +232,81 @@ export const data = [ JSON: { enablePartialFailure: true, operations: [ - { create: { userIdentifiers: [{ thirdPartyUserId: 'useri1234' }] } }, + { + remove: { + userIdentifiers: [ + { + hashedEmail: + 'd3142c8f9c9129484daf28df80cc5c955791efed5e69afabb603bc8cb9ffd419', + }, + { + hashedPhoneNumber: + '8846dcb6ab2d73a0e67dbd569fa17cec2d9d391e5b05d1dd42919bc21ae82c45', + }, + { + addressInfo: { + hashedFirstName: + '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + hashedLastName: + 'dcf000c2386fb76d22cefc0d118a8511bb75999019cd373df52044bccd1bd251', + countryCode: 'US', + postalCode: '1245', + }, + }, + ], + }, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + { + version: '1', + type: 'REST', + method: 'POST', + endpoint: + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', + headers: { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + }, + params: { + listId: '7090784486', + customerId: '7693729833', + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, + }, + body: { + JSON: { + enablePartialFailure: true, + operations: [ + { + create: { + userIdentifiers: [ + { + hashedEmail: + 'd3142c8f9c9129484daf28df80cc5c955791efed5e69afabb603bc8cb9ffd419', + }, + { + hashedPhoneNumber: + '8846dcb6ab2d73a0e67dbd569fa17cec2d9d391e5b05d1dd42919bc21ae82c45', + }, + { + addressInfo: { + hashedFirstName: + '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + hashedLastName: + 'dcf000c2386fb76d22cefc0d118a8511bb75999019cd373df52044bccd1bd251', + countryCode: 'US', + postalCode: '1245', + }, + }, + ], + }, + }, ], }, JSON_ARRAY: {}, @@ -330,13 +318,16 @@ export const data = [ ], metadata: [ { + attemptNum: 1, + destinationId: 'default-destinationId', + dontBatch: false, secret: { - access_token: 'abcd1234', - refresh_token: 'efgh5678', - developer_token: 'ijkl9101', + accessToken: 'default-accessToken', }, - jobId: 2, - userId: 'u1', + sourceId: 'default-sourceId', + userId: 'default-userId', + workspaceId: 'default-workspaceId', + jobId: 4, }, ], batched: false, @@ -350,10 +341,45 @@ export const data = [ subAccount: false, userSchema: ['email', 'phone', 'addressInfo'], isHashRequired: true, - typeOfList: 'userID', + typeOfList: 'General', + }, + DestinationDefinition: { + Config: {}, + DisplayName: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + ID: '1aIXqM806xAVm92nx07YwKbRrO9', + Name: 'GOOGLE_ADWORDS_REMARKETING_LISTS', }, + Enabled: true, + ID: '1mMy5cqbtfuaKZv1IhVQKnBdVwe', + IsConnectionEnabled: true, + IsProcessorEnabled: true, + Name: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + Transformations: [], + WorkspaceID: '1TSN08muJTZwH8iCDmnnRt1pmLd', }, }, + ], + }, + }, + }, + }, + { + name: 'google_adwords_remarketing_lists record event tests', + description: 'Test 1', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: rETLRecordRouterRequest, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ { batchedRequest: [ { @@ -361,20 +387,21 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { - Authorization: 'Bearer abcd1234', + Authorization: 'Bearer default-accessToken', 'Content-Type': 'application/json', - 'developer-token': 'ijkl9101', }, params: { listId: '7090784486', customerId: '7693729833', - consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, + consent: { + adPersonalization: 'UNSPECIFIED', + adUserData: 'UNSPECIFIED', + }, }, body: { JSON: { - enablePartialFailure: true, operations: [ { remove: { @@ -411,16 +438,19 @@ export const data = [ ], metadata: [ { + attemptNum: 1, + destinationId: 'default-destinationId', + dontBatch: false, secret: { - access_token: 'abcd1234', - refresh_token: 'efgh5678', - developer_token: 'ijkl9101', + accessToken: 'default-accessToken', }, - jobId: 3, - userId: 'u1', + sourceId: 'default-sourceId', + userId: 'default-userId', + workspaceId: 'default-workspaceId', + jobId: 1, }, ], - batched: false, + batched: true, statusCode: 200, destination: { Config: { @@ -433,6 +463,19 @@ export const data = [ isHashRequired: true, typeOfList: 'General', }, + DestinationDefinition: { + Config: {}, + DisplayName: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + ID: '1aIXqM806xAVm92nx07YwKbRrO9', + Name: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + }, + Enabled: true, + ID: '1mMy5cqbtfuaKZv1IhVQKnBdVwe', + IsConnectionEnabled: true, + IsProcessorEnabled: true, + Name: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + Transformations: [], + WorkspaceID: '1TSN08muJTZwH8iCDmnnRt1pmLd', }, }, { @@ -442,23 +485,24 @@ export const data = [ type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { - Authorization: 'Bearer abcd1234', + Authorization: 'Bearer default-accessToken', 'Content-Type': 'application/json', - 'developer-token': 'ijkl9101', }, params: { listId: '7090784486', customerId: '7693729833', - consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, + consent: { + adPersonalization: 'UNSPECIFIED', + adUserData: 'UNSPECIFIED', + }, }, body: { JSON: { - enablePartialFailure: true, operations: [ { - remove: { + create: { userIdentifiers: [ { hashedEmail: @@ -478,6 +522,24 @@ export const data = [ postalCode: '1245', }, }, + { + hashedEmail: + 'd3142c8f9c9129484daf28df80cc5c955791efed5e69afabb603bc8cb9ffd419', + }, + { + hashedPhoneNumber: + '8846dcb6ab2d73a0e67dbd569fa17cec2d9d391e5b05d1dd42919bc21ae82c45', + }, + { + addressInfo: { + hashedFirstName: + '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + hashedLastName: + 'dcf000c2386fb76d22cefc0d118a8511bb75999019cd373df52044bccd1bd251', + countryCode: 'US', + postalCode: '1245', + }, + }, ], }, }, @@ -489,25 +551,83 @@ export const data = [ }, files: {}, }, + ], + metadata: [ + { + attemptNum: 1, + destinationId: 'default-destinationId', + dontBatch: false, + secret: { + accessToken: 'default-accessToken', + }, + sourceId: 'default-sourceId', + userId: 'default-userId', + workspaceId: 'default-workspaceId', + jobId: 2, + }, + { + attemptNum: 1, + destinationId: 'default-destinationId', + dontBatch: false, + jobId: 3, + secret: { + accessToken: 'default-accessToken', + }, + sourceId: 'default-sourceId', + userId: 'default-userId', + workspaceId: 'default-workspaceId', + }, + ], + batched: true, + statusCode: 200, + destination: { + Config: { + rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', + listId: '7090784486', + customerId: '7693729833', + loginCustomerId: '', + subAccount: false, + userSchema: ['email', 'phone', 'addressInfo'], + isHashRequired: true, + typeOfList: 'General', + }, + DestinationDefinition: { + Config: {}, + DisplayName: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + ID: '1aIXqM806xAVm92nx07YwKbRrO9', + Name: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + }, + Enabled: true, + ID: '1mMy5cqbtfuaKZv1IhVQKnBdVwe', + IsConnectionEnabled: true, + IsProcessorEnabled: true, + Name: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + Transformations: [], + WorkspaceID: '1TSN08muJTZwH8iCDmnnRt1pmLd', + }, + }, + { + batchedRequest: [ { version: '1', type: 'REST', method: 'POST', endpoint: - 'https://googleads.googleapis.com/v15/customers/7693729833/offlineUserDataJobs', + 'https://googleads.googleapis.com/v16/customers/7693729833/offlineUserDataJobs', headers: { - Authorization: 'Bearer abcd1234', + Authorization: 'Bearer default-accessToken', 'Content-Type': 'application/json', - 'developer-token': 'ijkl9101', }, params: { listId: '7090784486', customerId: '7693729833', - consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, + consent: { + adPersonalization: 'UNSPECIFIED', + adUserData: 'UNSPECIFIED', + }, }, body: { JSON: { - enablePartialFailure: true, operations: [ { create: { @@ -544,16 +664,19 @@ export const data = [ ], metadata: [ { + attemptNum: 1, + destinationId: 'default-destinationId', + dontBatch: false, secret: { - access_token: 'abcd1234', - refresh_token: 'efgh5678', - developer_token: 'ijkl9101', + accessToken: 'default-accessToken', }, + sourceId: 'default-sourceId', + userId: 'default-userId', + workspaceId: 'default-workspaceId', jobId: 4, - userId: 'u1', }, ], - batched: false, + batched: true, statusCode: 200, destination: { Config: { @@ -566,6 +689,48 @@ export const data = [ isHashRequired: true, typeOfList: 'General', }, + DestinationDefinition: { + Config: {}, + DisplayName: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + ID: '1aIXqM806xAVm92nx07YwKbRrO9', + Name: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + }, + Enabled: true, + ID: '1mMy5cqbtfuaKZv1IhVQKnBdVwe', + IsConnectionEnabled: true, + IsProcessorEnabled: true, + Name: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + Transformations: [], + WorkspaceID: '1TSN08muJTZwH8iCDmnnRt1pmLd', + }, + }, + { + metadata: [ + { + attemptNum: 1, + destinationId: 'default-destinationId', + dontBatch: false, + secret: { + accessToken: 'default-accessToken', + }, + sourceId: 'default-sourceId', + userId: 'default-userId', + workspaceId: 'default-workspaceId', + jobId: 5, + }, + ], + batched: false, + statusCode: 400, + error: 'Invalid action type in record event', + statTags: { + errorCategory: 'dataValidation', + errorType: 'instrumentation', + destinationId: 'default-destinationId', + destType: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + module: 'destination', + implementation: 'native', + feature: 'router', + workspaceId: 'default-workspaceId', }, }, ], diff --git a/test/integrations/destinations/google_adwords_remarketing_lists/router/record.ts b/test/integrations/destinations/google_adwords_remarketing_lists/router/record.ts new file mode 100644 index 0000000000..743213bcc1 --- /dev/null +++ b/test/integrations/destinations/google_adwords_remarketing_lists/router/record.ts @@ -0,0 +1,158 @@ +import { Destination, RouterTransformationRequest } from '../../../../../src/types'; +import { generateMetadata } from '../../../testUtils'; + +const destination: Destination = { + Config: { + rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', + listId: '7090784486', + customerId: '7693729833', + loginCustomerId: '', + subAccount: false, + userSchema: ['email', 'phone', 'addressInfo'], + isHashRequired: true, + typeOfList: 'General', + }, + ID: '1mMy5cqbtfuaKZv1IhVQKnBdVwe', + Name: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + Enabled: true, + WorkspaceID: '1TSN08muJTZwH8iCDmnnRt1pmLd', + DestinationDefinition: { + ID: '1aIXqM806xAVm92nx07YwKbRrO9', + Name: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + DisplayName: 'GOOGLE_ADWORDS_REMARKETING_LISTS', + Config: {}, + }, + Transformations: [], + IsConnectionEnabled: true, + IsProcessorEnabled: true, +}; + +export const rETLRecordRouterRequest: RouterTransformationRequest = { + input: [ + { + destination: destination, + message: { + action: 'insert', + context: { + ip: '14.5.67.21', + library: { + name: 'http', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + email: 'test@abc.com', + phone: '@09876543210', + firstName: 'test', + lastName: 'rudderlabs', + country: 'US', + postalCode: '1245', + }, + type: 'record', + }, + metadata: generateMetadata(2), + }, + { + destination: destination, + message: { + action: 'update', + context: { + ip: '14.5.67.21', + library: { + name: 'http', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + email: 'test@abc.com', + phone: '@09876543210', + firstName: 'test', + lastName: 'rudderlabs', + country: 'US', + postalCode: '1245', + }, + type: 'record', + }, + metadata: generateMetadata(4), + }, + { + destination: destination, + message: { + action: 'delete', + context: { + ip: '14.5.67.21', + library: { + name: 'http', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + email: 'test@abc.com', + phone: '@09876543210', + firstName: 'test', + lastName: 'rudderlabs', + country: 'US', + postalCode: '1245', + }, + type: 'record', + }, + metadata: generateMetadata(1), + }, + { + destination: destination, + message: { + action: 'lol', + context: { + ip: '14.5.67.21', + library: { + name: 'http', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + email: 'test@abc.com', + phone: '@09876543210', + firstName: 'test', + lastName: 'rudderlabs', + country: 'US', + postalCode: '1245', + }, + type: 'record', + }, + metadata: generateMetadata(5), + }, + { + destination: destination, + message: { + action: 'insert', + context: { + ip: '14.5.67.21', + library: { + name: 'http', + }, + }, + recordId: '2', + rudderId: '2', + fields: { + email: 'test@abc.com', + phone: '@09876543210', + firstName: 'test', + lastName: 'rudderlabs', + country: 'US', + postalCode: '1245', + }, + type: 'record', + }, + metadata: generateMetadata(3), + }, + ], + destType: 'google_adwords_remarketing_lists', +}; + +module.exports = { + rETLRecordRouterRequest, +}; From 1a013627d13e0dc286f55de8925bfbb12d9a462b Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Wed, 19 Jun 2024 10:46:54 +0530 Subject: [PATCH 3/3] fix: onboard custom alias support for braze (#3335) --- src/util/prometheus.js | 6 + src/v0/destinations/braze/braze.util.test.js | 119 ++++++++- src/v0/destinations/braze/transform.js | 26 +- src/v0/destinations/braze/util.js | 30 ++- .../destinations/braze/processor/data.ts | 248 ++++++++++++++++++ 5 files changed, 407 insertions(+), 22 deletions(-) diff --git a/src/util/prometheus.js b/src/util/prometheus.js index 72f424d39a..5b7fe692b4 100644 --- a/src/util/prometheus.js +++ b/src/util/prometheus.js @@ -612,6 +612,12 @@ class Prometheus { type: 'counter', labelNames: ['destination_id'], }, + { + name: 'braze_alias_missconfigured_count', + help: 'braze_alias_missconfigured_count', + type: 'counter', + labelNames: ['destination_id'], + }, { name: 'mixpanel_batch_engage_pack_size', help: 'mixpanel_batch_engage_pack_size', diff --git a/src/v0/destinations/braze/braze.util.test.js b/src/v0/destinations/braze/braze.util.test.js index cc50ae921e..460f1db565 100644 --- a/src/v0/destinations/braze/braze.util.test.js +++ b/src/v0/destinations/braze/braze.util.test.js @@ -1,6 +1,6 @@ const _ = require('lodash'); const { handleHttpRequest } = require('../../../adapters/network'); -const { BrazeDedupUtility, addAppId, getPurchaseObjs } = require('./util'); +const { BrazeDedupUtility, addAppId, getPurchaseObjs, setAliasObject } = require('./util'); const { processBatch } = require('./util'); const { removeUndefinedAndNullValues, @@ -1421,3 +1421,120 @@ describe('getPurchaseObjs', () => { ]); }); }); + +describe('setAliasObject function', () => { + // Test when integrationsObj has both alias_name and alias_label + test('should set user_alias from integrationsObj if alias_name and alias_label are defined', () => { + const payload = {}; + const result = setAliasObject(payload, { + anonymousId: '12345', + integrations: { + BRAZE: { + alias: { + alias_name: 'user123', + alias_label: 'customer_id', + }, + }, + }, + }); + + expect(result).toEqual({ + user_alias: { + alias_name: 'user123', + alias_label: 'customer_id', + }, + }); + }); + + // Test when integrationsObj is missing alias_name or alias_label + test('should set user_alias with anonymousId as alias_name and "rudder_id" as alias_label if integrationsObj does not have alias_name or alias_label', () => { + const message = { + anonymousId: '12345', + }; + const payload = {}; + const result = setAliasObject(payload, message); + + expect(result).toEqual({ + user_alias: { + alias_name: '12345', + alias_label: 'rudder_id', + }, + }); + }); + + // Test when message has no anonymousId and integrationsObj is missing + test('should return payload unchanged if message has no anonymousId and integrationsObj is missing', () => { + const message = {}; + const payload = {}; + const result = setAliasObject(payload, message); + + expect(result).toEqual(payload); + }); + + test('should set user_alias from integrationsObj if alias_name and alias_label are defined', () => { + const payload = {}; + const result = setAliasObject(payload, { + anonymousId: '12345', + integrations: { + BRAZE: { + alias: { + alias_name: 'user123', + alias_label: 'customer_id', + }, + }, + }, + }); + + expect(result).toEqual({ + user_alias: { + alias_name: 'user123', + alias_label: 'customer_id', + }, + }); + }); + + test('should set user_alias from integrationsObj if alias_name and alias_label either is not defined', () => { + const payload = {}; + const result = setAliasObject(payload, { + anonymousId: '12345', + integrations: { + BRAZE: { + alias: { + alias_name: null, + alias_label: 'customer_id', + }, + }, + }, + }); + + expect(result).toEqual({ + user_alias: { + alias_name: '12345', + alias_label: 'rudder_id', + }, + }); + }); + + test('should set user_alias from integrationsObj if alias_name and alias_label either is not defined', () => { + const payload = {}; + const result = setAliasObject(payload, { + anonymousId: '12345', + userID: 'user123', + integrations: { + BRAZE: { + alias: { + alias_name: 'rudder_id-123', + alias_label: 'customer_id', + }, + }, + }, + }); + + expect(result).toEqual({ + user_alias: { + alias_name: 'rudder_id-123', + alias_label: 'customer_id', + }, + }); + }); +}); diff --git a/src/v0/destinations/braze/transform.js b/src/v0/destinations/braze/transform.js index 11b2bb0636..3d6a99d424 100644 --- a/src/v0/destinations/braze/transform.js +++ b/src/v0/destinations/braze/transform.js @@ -12,8 +12,9 @@ const { setExternalIdOrAliasObject, getPurchaseObjs, setExternalId, - setAliasObjectWithAnonId, + setAliasObject, collectStatsForAliasFailure, + collectStatsForAliasMissConfigurations, } = require('./util'); const tags = require('../../util/tags'); const { EventType, MappedToDestinationKey } = require('../../../constants'); @@ -27,6 +28,7 @@ const { simpleProcessRouterDestSync, simpleProcessRouterDest, isNewStatusCodesAccepted, + getDestinationExternalID, } = require('../../util'); const { ConfigCategory, @@ -82,7 +84,7 @@ function buildResponse(message, destination, properties, endpoint) { function getIdentifyPayload(message) { let payload = {}; - payload = setAliasObjectWithAnonId(payload, message); + payload = setAliasObject(payload, message); payload = setExternalId(payload, message); return { aliases_to_identify: [payload], merge_behavior: 'merge' }; } @@ -202,12 +204,6 @@ function getUserAttributesObject(message, mappingJson, destination) { * @param {*} destination */ async function processIdentify(message, destination) { - // override userId with externalId in context(if present) and event is mapped to destination - const mappedToDestination = get(message, MappedToDestinationKey); - if (mappedToDestination) { - adduserIdFromExternalId(message); - } - const identifyPayload = getIdentifyPayload(message); const identifyEndpoint = getIdentifyEndpoint(getEndpointFromConfig(destination)); const { processedResponse: brazeIdentifyResp } = await handleHttpRequest( @@ -511,10 +507,19 @@ async function process(event, processParams = { userStore: new Map() }, reqMetad processParams, ); break; - case EventType.IDENTIFY: + case EventType.IDENTIFY: { category = ConfigCategory.IDENTIFY; - if (message.anonymousId) { + // override userId with externalId in context(if present) and event is mapped to destination + const mappedToDestination = get(message, MappedToDestinationKey); + if (mappedToDestination) { + adduserIdFromExternalId(message); + } + const brazeExternalID = + getDestinationExternalID(message, 'brazeExternalId') || message.userId; + if (message.anonymousId && brazeExternalID) { await processIdentify(message, destination); + } else { + collectStatsForAliasMissConfigurations(destination.ID); } response = processTrackWithUserAttributes( message, @@ -524,6 +529,7 @@ async function process(event, processParams = { userStore: new Map() }, reqMetad reqMetadata, ); break; + } case EventType.GROUP: response = processGroup(message, destination); break; diff --git a/src/v0/destinations/braze/util.js b/src/v0/destinations/braze/util.js index 4253619d33..b3b29cdf96 100644 --- a/src/v0/destinations/braze/util.js +++ b/src/v0/destinations/braze/util.js @@ -519,8 +519,18 @@ function setExternalId(payload, message) { return payload; } -function setAliasObjectWithAnonId(payload, message) { - if (message.anonymousId) { +function setAliasObject(payload, message) { + const integrationsObj = getIntegrationsObj(message, 'BRAZE'); + if ( + isDefinedAndNotNull(integrationsObj?.alias?.alias_name) && + isDefinedAndNotNull(integrationsObj?.alias?.alias_label) + ) { + const { alias_name, alias_label } = integrationsObj.alias; + payload.user_alias = { + alias_name, + alias_label, + }; + } else if (message.anonymousId) { payload.user_alias = { alias_name: message.anonymousId, alias_label: 'rudder_id', @@ -537,7 +547,7 @@ function setExternalIdOrAliasObject(payload, message) { // eslint-disable-next-line no-underscore-dangle payload._update_existing_only = false; - return setAliasObjectWithAnonId(payload, message); + return setAliasObject(payload, message); } function addMandatoryPurchaseProperties(productId, price, currencyCode, quantity, timestamp) { @@ -687,16 +697,13 @@ const collectStatsForAliasFailure = (brazeResponse, destinationId) => { const { aliases_processed: aliasesProcessed, errors } = brazeResponse; if (aliasesProcessed === 0) { stats.increment('braze_alias_failure_count', { destination_id: destinationId }); - - if (Array.isArray(errors)) { - logger.info('Braze Alias Failure Errors:', { - destinationId, - errors, - }); - } } }; +const collectStatsForAliasMissConfigurations = (destinationId) => { + stats.increment('braze_alias_missconfigured_count', { destination_id: destinationId }); +}; + module.exports = { BrazeDedupUtility, CustomAttributeOperationUtil, @@ -707,7 +714,8 @@ module.exports = { getPurchaseObjs, setExternalIdOrAliasObject, setExternalId, - setAliasObjectWithAnonId, + setAliasObject, addMandatoryPurchaseProperties, collectStatsForAliasFailure, + collectStatsForAliasMissConfigurations, }; diff --git a/test/integrations/destinations/braze/processor/data.ts b/test/integrations/destinations/braze/processor/data.ts index 58c54bb232..dc5a84470f 100644 --- a/test/integrations/destinations/braze/processor/data.ts +++ b/test/integrations/destinations/braze/processor/data.ts @@ -3863,4 +3863,252 @@ export const data = [ }, }, }, + { + name: 'braze', + description: '[Test 28] Test for alias_id overriding feature for track event', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + Config: { + restApiKey: 'dummyApiKey', + prefixProperties: true, + useNativeSDK: false, + }, + DestinationDefinition: { + DisplayName: 'Braze', + ID: '1WhbSZ6uA3H5ChVifHpfL2H6sie', + Name: 'BRAZE', + }, + Enabled: true, + ID: '1WhcOCGgj9asZu850HvugU2C3Aq', + Name: 'Braze', + Transformations: [], + }, + message: { + anonymousId: 'e6ab2c5e-2cda-44a9-a962-e2f67df78bca', + channel: 'web', + context: { + traits: { + city: 'Disney', + country: 'USA', + email: 'micky@example.com', + firstname: 'Mickey', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36', + }, + event: 'Test Event', + integrations: { + All: true, + Braze: { + alias: { + alias_label: 'email', + alias_name: 'micky@example.com', + }, + }, + }, + messageId: 'aa5f5e44-8756-40ad-ad1e-b0d3b9fa710a', + originalTimestamp: '2020-01-24T06:29:02.367Z', + properties: { + affiliation: 'Google Store', + checkout_id: 'fksdjfsdjfisjf9sdfjsd9f', + coupon: 'hasbros', + currency: 'USD', + discount: 2.5, + order_id: '50314b8e9bcf000000000000', + revenue: 25, + shipping: 3, + subtotal: 22.5, + tax: 2, + total: 27.5, + }, + receivedAt: '2020-01-24T11:59:02.403+05:30', + request_ip: '[::1]:53712', + sentAt: '2020-01-24T06:29:02.368Z', + timestamp: '2020-01-24T11:59:02.402+05:30', + type: 'track', + userId: '', + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://rest.fra-01.braze.eu/users/track', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: 'Bearer dummyApiKey', + }, + params: {}, + body: { + JSON: { + partner: 'RudderStack', + attributes: [ + { + email: 'micky@example.com', + city: 'Disney', + country: 'USA', + firstname: 'Mickey', + _update_existing_only: false, + user_alias: { + alias_name: 'micky@example.com', + alias_label: 'email', + }, + }, + ], + events: [ + { + name: 'Test Event', + time: '2020-01-24T11:59:02.402+05:30', + properties: { + affiliation: 'Google Store', + checkout_id: 'fksdjfsdjfisjf9sdfjsd9f', + coupon: 'hasbros', + discount: 2.5, + order_id: '50314b8e9bcf000000000000', + revenue: 25, + shipping: 3, + subtotal: 22.5, + tax: 2, + total: 27.5, + }, + _update_existing_only: false, + user_alias: { + alias_name: 'micky@example.com', + alias_label: 'email', + }, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: 'e6ab2c5e-2cda-44a9-a962-e2f67df78bca', + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'braze', + description: + '[Test 29] Test for alias_id overriding feature for identify event (with intermediate alias call)', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + Config: { + restApiKey: 'dummyApiKey', + prefixProperties: true, + useNativeSDK: false, + dataCenter: 'us-01', + }, + DestinationDefinition: { + DisplayName: 'Braze', + ID: '1WhbSZ6uA3H5ChVifHpfL2H6sie', + Name: 'BRAZE', + }, + Enabled: true, + ID: '1WhcOCGgj9asZu850HvugU2C3Aq', + Name: 'Braze', + Transformations: [], + }, + message: { + anonymousId: 'e6ab2c5e-2cda-44a9-a962-e2f67df78bca', + channel: 'web', + context: { + traits: { + city: 'Disney', + country: 'USA', + email: 'micky@example.com', + firstname: 'Mickey', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36', + }, + integrations: { + All: true, + Braze: { + alias: { + alias_label: 'email', + alias_name: 'micky@example.com', + }, + }, + }, + messageId: 'aa5f5e44-8756-40ad-ad1e-b0d3b9fa710a', + originalTimestamp: '2020-01-24T06:29:02.367Z', + receivedAt: '2020-01-24T11:59:02.403+05:30', + request_ip: '[::1]:53712', + sentAt: '2020-01-24T06:29:02.368Z', + timestamp: '2020-01-24T11:59:02.402+05:30', + type: 'identify', + userId: 'mickeyUser', + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://rest.iad-01.braze.com/users/track', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: 'Bearer dummyApiKey', + }, + params: {}, + body: { + JSON: { + attributes: [ + { + email: 'micky@example.com', + city: 'Disney', + country: 'USA', + firstname: 'Mickey', + external_id: 'mickeyUser', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: 'mickeyUser', + }, + statusCode: 200, + }, + ], + }, + }, + }, ];