From 5ac81860c51f9971343df8c61bfd0b2de8161735 Mon Sep 17 00:00:00 2001 From: Anant Jain <62471433+anantjain45823@users.noreply.github.com> Date: Tue, 8 Oct 2024 20:20:06 +0530 Subject: [PATCH] feat: onboard Amazon Audience (#3727) --- package-lock.json | 6 + package.json | 1 + src/features.json | 1 + src/v0/destinations/amazon_audience/config.js | 5 + .../amazon_audience/networkHandler.js | 139 ++++++++++ .../destinations/amazon_audience/transform.js | 58 +++++ src/v0/destinations/amazon_audience/utils.js | 175 +++++++++++++ .../destinations/amazon_audience/common.ts | 30 +++ .../amazon_audience/dataDelivery/data.ts | 197 ++++++++++++++ .../destinations/amazon_audience/network.ts | 123 +++++++++ .../amazon_audience/processor/data.ts | 246 ++++++++++++++++++ .../amazon_audience/router/data.ts | 226 ++++++++++++++++ 12 files changed, 1207 insertions(+) create mode 100644 src/v0/destinations/amazon_audience/config.js create mode 100644 src/v0/destinations/amazon_audience/networkHandler.js create mode 100644 src/v0/destinations/amazon_audience/transform.js create mode 100644 src/v0/destinations/amazon_audience/utils.js create mode 100644 test/integrations/destinations/amazon_audience/common.ts create mode 100644 test/integrations/destinations/amazon_audience/dataDelivery/data.ts create mode 100644 test/integrations/destinations/amazon_audience/network.ts create mode 100644 test/integrations/destinations/amazon_audience/processor/data.ts create mode 100644 test/integrations/destinations/amazon_audience/router/data.ts diff --git a/package-lock.json b/package-lock.json index 243b989a6d..b8f677520f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "ajv": "^8.12.0", "ajv-draft-04": "^1.0.0", "ajv-formats": "^2.1.1", + "amazon-dsp-formatter": "^1.0.2", "axios": "^1.7.3", "btoa": "^1.2.1", "component-each": "^0.2.6", @@ -8197,6 +8198,11 @@ } } }, + "node_modules/amazon-dsp-formatter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/amazon-dsp-formatter/-/amazon-dsp-formatter-1.0.2.tgz", + "integrity": "sha512-CfsssMzLFh0IK6oz3j38ENGgp5LZ/q21YX4yXSavfI50CU2cJbupKOk+Bgg0sY67V0lWsYsmYrpkEI2aFG/duA==" + }, "node_modules/ansi-align": { "version": "3.0.1", "dev": true, diff --git a/package.json b/package.json index 8a104f3081..7aedc782ba 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "ajv": "^8.12.0", "ajv-draft-04": "^1.0.0", "ajv-formats": "^2.1.1", + "amazon-dsp-formatter": "^1.0.2", "axios": "^1.7.3", "btoa": "^1.2.1", "component-each": "^0.2.6", diff --git a/src/features.json b/src/features.json index f95bd09d7e..63862eefed 100644 --- a/src/features.json +++ b/src/features.json @@ -82,6 +82,7 @@ "BLOOMREACH_CATALOG": true, "SMARTLY": true, "HTTP": true, + "AMAZON_AUDIENCE": true, "INTERCOM_V2": true }, "regulations": [ diff --git a/src/v0/destinations/amazon_audience/config.js b/src/v0/destinations/amazon_audience/config.js new file mode 100644 index 0000000000..f377bceaae --- /dev/null +++ b/src/v0/destinations/amazon_audience/config.js @@ -0,0 +1,5 @@ +const CREATE_USERS_URL = 'https://advertising-api.amazon.com/dp/records/hashed/'; +const ASSOCIATE_USERS_URL = 'https://advertising-api.amazon.com/v2/dp/audience'; +const MAX_PAYLOAD_SIZE_IN_BYTES = 4000000; +const DESTINATION = 'amazon_audience'; +module.exports = { CREATE_USERS_URL, MAX_PAYLOAD_SIZE_IN_BYTES, ASSOCIATE_USERS_URL, DESTINATION }; diff --git a/src/v0/destinations/amazon_audience/networkHandler.js b/src/v0/destinations/amazon_audience/networkHandler.js new file mode 100644 index 0000000000..c1fbeeccea --- /dev/null +++ b/src/v0/destinations/amazon_audience/networkHandler.js @@ -0,0 +1,139 @@ +const { + NetworkError, + ThrottledError, + AbortedError, + RetryableError, +} = require('@rudderstack/integrations-lib'); +const { prepareProxyRequest, handleHttpRequest } = require('../../../adapters/network'); +const { isHttpStatusSuccess } = require('../../util/index'); +const { + processAxiosResponse, + getDynamicErrorType, +} = require('../../../adapters/utils/networkUtils'); +const { REFRESH_TOKEN } = require('../../../adapters/networkhandler/authConstants'); +const { DESTINATION, CREATE_USERS_URL, ASSOCIATE_USERS_URL } = require('./config'); +const { TAG_NAMES } = require('../../util/tags'); + +const amazonAudienceRespHandler = (destResponse, stageMsg) => { + const { status, response } = destResponse; + + // to handle the case when authorization-token is invalid + // docs for error codes: https://advertising.amazon.com/API/docs/en-us/reference/concepts/errors#tag/Audiences/operation/dspCreateAudiencesPost + if (status === 401 && response.message === 'Unauthorized') { + // 401 takes place in case of authorization isue meaning token is epxired or access is not enough. + // Since acces is configured from dashboard only refresh token makes sense + throw new NetworkError( + `${response?.message} ${stageMsg}`, + status, + { + [TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + }, + response, + REFRESH_TOKEN, + ); + } else if (status === 429) { + throw new ThrottledError( + `Request Failed: ${stageMsg} - due to Request Limit exceeded, (Throttled)`, + destResponse, + ); + } else if (status === 504 || status === 502 || status === 500) { + // see if its 5xx internal error + throw new RetryableError(`Request Failed: ${stageMsg} (Retryable)`, 500, destResponse); + } + // else throw the error + throw new AbortedError( + `Request Failed: ${stageMsg} with status "${status}" due to "${JSON.stringify( + response, + )}", (Aborted) `, + 400, + destResponse, + ); +}; + +const responseHandler = (responseParams) => { + const { destinationResponse } = responseParams; + const message = `[${DESTINATION} Response Handler] - Request Processed Successfully`; + const { status } = destinationResponse; + + if (!isHttpStatusSuccess(status)) { + // if error, successfully return status, message and original destination response + amazonAudienceRespHandler( + destinationResponse, + 'during amazon_audience response transformation', + ); + } + return { + status, + message, + destinationResponse, + }; +}; + +const makeRequest = async (url, data, headers, metadata, method, args) => { + const { httpResponse } = await handleHttpRequest(method, url, data, { headers }, args); + return httpResponse; +}; + +const amazonAudienceProxyRequest = async (request) => { + const { body, metadata } = request; + const { headers } = request; + const { createUsers, associateUsers } = body.JSON; + + // step 1: Create users + const firstResponse = await makeRequest( + CREATE_USERS_URL, + createUsers, + headers, + metadata, + 'post', + { + destType: 'amazon_audience', + feature: 'proxy', + requestMethod: 'POST', + module: 'dataDelivery', + endpointPath: '/records/hashed', + metadata, + }, + ); + // validate response success + if (!firstResponse.success && !isHttpStatusSuccess(firstResponse?.response?.status)) { + amazonAudienceRespHandler( + { + response: firstResponse.response?.response?.data || firstResponse, + status: firstResponse.response?.response?.status || firstResponse, + }, + 'during creating users', + ); + } + // we are returning above in case of failure because if first step is not executed then there is no sense of executing second step + // step2: Associate Users to Audience Id + const secondResponse = await makeRequest( + ASSOCIATE_USERS_URL, + associateUsers, + headers, + metadata, + 'patch', + { + destType: 'amazon_audience', + feature: 'proxy', + requestMethod: 'PATCH', + module: 'dataDelivery', + endpointPath: '/v2/dp/audience', + metadata, + }, + ); + return secondResponse; +}; +// eslint-disable-next-line @typescript-eslint/naming-convention +class networkHandler { + constructor() { + this.responseHandler = responseHandler; + this.proxy = amazonAudienceProxyRequest; + this.prepareProxy = prepareProxyRequest; + this.processAxiosResponse = processAxiosResponse; + } +} + +module.exports = { + networkHandler, +}; diff --git a/src/v0/destinations/amazon_audience/transform.js b/src/v0/destinations/amazon_audience/transform.js new file mode 100644 index 0000000000..834810de54 --- /dev/null +++ b/src/v0/destinations/amazon_audience/transform.js @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +const { InstrumentationError } = require('@rudderstack/integrations-lib'); +const { handleRtTfSingleEventError } = require('../../util'); +const { batchEvents, buildResponseWithUsers, getUserDetails } = require('./utils'); +/** + * This function returns the user traits list required in request for + * making a call to create a group of users in amazon_audience + * @param {*} record + * @param {*} destination + * @param {*} metadata + */ +const processRecord = (record, config) => { + const { fields, action, type } = record; + if (type !== 'record') { + throw new InstrumentationError(`[AMAZON AUDIENCE]: ${type} is not supported`); + } + return { user: getUserDetails(fields, config), action: action !== 'delete' ? 'add' : 'remove' }; +}; + +// This function is used to process a single record +const process = (event) => { + const { message, destination, metadata } = event; + const { Config } = destination; + const { user, action } = processRecord(message, Config); + return buildResponseWithUsers([user], action, Config, [metadata.jobId], metadata.secret); +}; +// This function is used to process multiple records +const processRouterDest = async (inputs, reqMetadata) => { + const responseList = []; // list containing all successful responses + const errorRespList = []; // list of error + const { destination } = inputs[0]; + const { Config } = destination; + inputs.map(async (event) => { + try { + if (event.message.statusCode) { + // already transformed event + responseList.push(event); + } else { + // if not transformed + responseList.push({ + message: processRecord(event.message, Config), + metadata: event.metadata, + destination, + }); + } + } catch (error) { + const errRespEvent = handleRtTfSingleEventError(event, error, reqMetadata); + errorRespList.push(errRespEvent); + } + }); + let batchedResponseList = []; + if (responseList.length > 0) { + batchedResponseList = batchEvents(responseList, destination); + } + return [...batchedResponseList, ...errorRespList]; +}; + +module.exports = { process, processRouterDest }; diff --git a/src/v0/destinations/amazon_audience/utils.js b/src/v0/destinations/amazon_audience/utils.js new file mode 100644 index 0000000000..c25f301378 --- /dev/null +++ b/src/v0/destinations/amazon_audience/utils.js @@ -0,0 +1,175 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +const sha256 = require('sha256'); +const AmazonAdsFormatter = require('amazon-dsp-formatter'); +const lodash = require('lodash'); +const { ConfigurationError, OAuthSecretError } = require('@rudderstack/integrations-lib'); +const { + defaultRequestConfig, + defaultPostRequestConfig, + getSuccessRespEvents, + removeUndefinedAndNullAndEmptyValues, +} = require('../../util'); + +const buildResponseWithUsers = (users, action, config, jobIdList, secret) => { + const { audienceId } = config; + if (!audienceId) { + throw new ConfigurationError('[AMAZON AUDIENCE]: Audience Id not found'); + } + if (!secret?.accessToken) { + throw new OAuthSecretError('OAuth - access token not found'); + } + if (!secret?.clientId) { + throw new OAuthSecretError('OAuth - Client Id not found'); + } + const externalId = `Rudderstack_${sha256(`${jobIdList}`)}`; + const response = defaultRequestConfig(); + response.endpoint = ''; + response.method = defaultPostRequestConfig.requestMethod; + response.headers = { + 'Amazon-Advertising-API-ClientId': `${secret.clientId}`, + 'Content-Type': 'application/json', + Authorization: `Bearer ${secret.accessToken}`, + }; + response.body.JSON = { + createUsers: { + records: [ + { + hashedRecords: users, + externalId, + }, + ], + }, + associateUsers: { + patches: [ + { + op: action, + path: `/EXTERNAL_USER_ID-${externalId}/audiences`, + value: [audienceId], + }, + ], + }, + }; + return response; +}; + +/** + * This function groups the response list based upon `operation` + * @param {*} respList + * @returns object + */ +const groupResponsesUsingOperation = (respList) => { + const eventGroups = lodash.groupBy(respList, (item) => item.message.action); + return eventGroups; +}; + +/** + * Input: [{ + message: { + users: {} + action + }, + }, + metadata, + destination, +}] + * @param {*} responseList + */ +const batchEvents = (responseList, destination) => { + const { secret } = responseList[0].metadata; + const eventGroups = groupResponsesUsingOperation(responseList); + const respList = []; + const opList = ['remove', 'add']; + opList.forEach((op) => { + if (eventGroups?.[op]) { + const { userList, jobIdList, metadataList } = eventGroups[op].reduce( + (acc, event) => ({ + userList: acc.userList.concat(event.message.user), + jobIdList: acc.jobIdList.concat(event.metadata.jobId), + metadataList: acc.metadataList.concat(event.metadata), + }), + { userList: [], metadataList: [], jobIdList: [] }, + ); + respList.push( + getSuccessRespEvents( + buildResponseWithUsers( + userList, + op, + destination.config || destination.Config, + jobIdList, + secret, + ), + metadataList, + destination, + true, + ), + ); + } + }); + return respList; +}; + +/** + * This function fetches the user details and + * hash them after normalizing if enable hash is turned on in config + * @param {*} fields + * @param {*} config + * @returns + */ +const getUserDetails = (fields, config) => { + const { enableHash } = config; + const { + email, + phone: phone_number, + firstName, + lastName, + address, + country, + city, + state, + postalCode, + } = fields; + if (!enableHash) { + return removeUndefinedAndNullAndEmptyValues({ + email, + phone: phone_number, + firstName, + lastName, + address, + country, + city, + state, + postalCode, + }); + } + // Since all fields are optional hence notusing formatRecord function from formatter but doing it for every parameter + const formatter = new AmazonAdsFormatter(); + const user = { + email: sha256(formatter.formatEmail(email)), + firstName: sha256(formatter.formatName(firstName)), + lastName: sha256(formatter.formatName(lastName)), + city: sha256(formatter.formatCity(city)), + state: sha256(formatter.formatState(state, country)), + postalCode: sha256(formatter.formatPostal(postalCode)), + }; + // formating guidelines https://advertising.amazon.com/help/GCCXMZYCK4RXWS6C + if (country) { + const country_code = formatter.formatCountry(country); + user.country = sha256(country_code); + if (phone_number) { + user.phone = sha256(formatter.formatPhone(phone_number, country_code)); + } + } + if (address) { + const formatted_address = + address + ?.normalize('NFD') + ?.replace(/[\u0300-\u036f]/g, '') + ?.trim() + ?.toLowerCase() + .replace(/[^\dA-Za-z]/g, '') || undefined; + user.address = sha256(formatter.formatAddress(formatted_address, country)); + } + return removeUndefinedAndNullAndEmptyValues(user); +}; + +module.exports = { batchEvents, getUserDetails, buildResponseWithUsers }; diff --git a/test/integrations/destinations/amazon_audience/common.ts b/test/integrations/destinations/amazon_audience/common.ts new file mode 100644 index 0000000000..728bdf1d25 --- /dev/null +++ b/test/integrations/destinations/amazon_audience/common.ts @@ -0,0 +1,30 @@ +export const destination = { + DestinationDefinition: { + Config: { + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + advertiserId: '{"Dummy Name":"1234"}', + audienceId: 'dummyId', + }, + ID: 'amazonAud-1234', +}; + +export const generateMetadata = (jobId: number, userId?: string): any => { + return { + jobId, + attemptNum: 1, + userId: userId || 'default-userId', + sourceId: 'default-sourceId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + dontBatch: false, + secret: { + accessToken: 'dummyAccessToken', + refreshToken: 'dummyRefreshToken', + clientId: 'dummyClientId', + }, + }; +}; diff --git a/test/integrations/destinations/amazon_audience/dataDelivery/data.ts b/test/integrations/destinations/amazon_audience/dataDelivery/data.ts new file mode 100644 index 0000000000..c78a53c899 --- /dev/null +++ b/test/integrations/destinations/amazon_audience/dataDelivery/data.ts @@ -0,0 +1,197 @@ +import { generateMetadata, generateProxyV0Payload } from '../../../testUtils'; + +const commonStatTags = { + destType: 'AMAZON_AUDIENCE', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', +}; +export const data = [ + { + name: 'amazon_audience', + id: 'Test 0', + description: 'Successfull Delivery case', + successCriteria: 'It should be passed with 200 Ok with no errors', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + endpoint: '', + JSON: { + associateUsers: { + patches: [ + { + op: 'remove', + path: '/EXTERNAL_USER_ID-Rudderstack_c73bcaadd94985269eeafd457c9f395135874dad5536cf1f6d75c132f602a14c/audiences', + value: ['dummyId'], + }, + ], + }, + createUsers: { + records: [ + { + externalId: + 'Rudderstack_c73bcaadd94985269eeafd457c9f395135874dad5536cf1f6d75c132f602a14c', + hashedRecords: [ + { + email: 'email4@abc.com', + }, + ], + }, + ], + }, + }, + }), + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[amazon_audience Response Handler] - Request Processed Successfully', + destinationResponse: { + response: { + requestId: 'dummy request id', + jobId: 'dummy job id', + }, + status: 200, + }, + }, + }, + }, + }, + }, + { + name: 'amazon_audience', + id: 'Test 1', + description: 'Unsuccessfull Delivery case for step 2', + successCriteria: 'It should be passed with 500 Internal with error with invalid payload', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + endpoint: '', + JSON: { + associateUsers: { + patches: [ + { + op: 'add', + path: '/EXTERNAL_USER_ID-Fail_Case/audiences', + value: ['dummyId'], + }, + ], + }, + createUsers: { + records: [ + { + externalId: + 'Rudderstack_c73bcaadd94985269eeafd457c9f395135874dad5536cf1f6d75c132f602a14c', + hashedRecords: [ + { + email: 'email4@abc.com', + }, + ], + }, + ], + }, + }, + }), + }, + }, + output: { + response: { + status: 500, + body: { + output: { + status: 500, + destinationResponse: { + response: { + code: 'Internal Error', + }, + status: 500, + }, + message: 'Request Failed: during amazon_audience response transformation (Retryable)', + statTags: { + destType: 'AMAZON_AUDIENCE', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'retryable', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + }, + }, + }, + }, + }, + }, + { + name: 'amazon_audience', + id: 'Test 2 - Oauth Refresh Token', + description: 'Unsuccessfull Access Error for step 1', + successCriteria: 'It should be passed with 401 Unauthorized error', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + headers: { + Authorization: 'Bearer fail_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + endpoint: '', + JSON: { + associateUsers: [], + createUsers: { + records: [ + { + externalId: 'access token expired fail case', + hashedRecords: [], + }, + ], + }, + }, + }), + }, + }, + output: { + response: { + status: 401, + body: { + output: { + status: 401, + destinationResponse: { + message: 'Unauthorized', + }, + authErrorCategory: 'REFRESH_TOKEN', + message: 'Unauthorized during creating users', + statTags: commonStatTags, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/amazon_audience/network.ts b/test/integrations/destinations/amazon_audience/network.ts new file mode 100644 index 0000000000..b0941712c6 --- /dev/null +++ b/test/integrations/destinations/amazon_audience/network.ts @@ -0,0 +1,123 @@ +const headers = { + 'Amazon-Advertising-API-ClientId': 'dummyClientId', + 'Content-Type': 'application/json', + Authorization: 'Bearer success_access_token', +}; +export const networkCallsData = [ + { + description: 'successful step 1', + httpReq: { + url: 'https://advertising-api.amazon.com/dp/records/hashed/', + data: { + records: [ + { + externalId: + 'Rudderstack_c73bcaadd94985269eeafd457c9f395135874dad5536cf1f6d75c132f602a14c', + hashedRecords: [ + { + email: 'email4@abc.com', + }, + ], + }, + ], + }, + params: {}, + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + method: 'post', + }, + httpRes: { + data: { + requestId: 'dummy request id', + }, + status: 200, + }, + }, + { + description: 'successful step 2', + httpReq: { + url: 'https://advertising-api.amazon.com/v2/dp/audience', + data: { + patches: [ + { + op: 'remove', + path: '/EXTERNAL_USER_ID-Rudderstack_c73bcaadd94985269eeafd457c9f395135874dad5536cf1f6d75c132f602a14c/audiences', + value: ['dummyId'], + }, + ], + }, + params: {}, + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + method: 'patch', + }, + httpRes: { + data: { + requestId: 'dummy request id', + jobId: 'dummy job id', + }, + status: 200, + }, + }, + { + description: 'unsuccessful step 2', + httpReq: { + url: 'https://advertising-api.amazon.com/v2/dp/audience', + data: { + patches: [ + { + op: 'add', + path: '/EXTERNAL_USER_ID-Fail_Case/audiences', + value: ['dummyId'], + }, + ], + }, + params: {}, + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + method: 'patch', + }, + httpRes: { + data: { + code: 'Internal Error', + }, + status: 500, + }, + }, + { + description: 'unsuccessful step 1', + httpReq: { + url: 'https://advertising-api.amazon.com/dp/records/hashed/', + data: { + records: [ + { + externalId: 'access token expired fail case', + hashedRecords: [], + }, + ], + }, + params: {}, + headers: { + Authorization: 'Bearer fail_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + method: 'post', + }, + httpRes: { + data: { + message: 'Unauthorized', + }, + status: 401, + }, + }, +]; diff --git a/test/integrations/destinations/amazon_audience/processor/data.ts b/test/integrations/destinations/amazon_audience/processor/data.ts new file mode 100644 index 0000000000..49931ed92b --- /dev/null +++ b/test/integrations/destinations/amazon_audience/processor/data.ts @@ -0,0 +1,246 @@ +import { destination, generateMetadata } from '../common'; +const sha256 = require('sha256'); + +const fields = { + email: 'abc@xyz.com', + phone: '9876543323', + firstName: 'test', + lastName: 'user', + address: ' Été très chaud! ', +}; + +export const data = [ + { + name: 'amazon_audience', + id: 'Test 1', + description: 'All traits are present with hash enbaled for the audience with insert operation', + successCriteria: 'It should be passed with 200 Ok with all traits mapped after hashing', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + ...fields, + city: 'Edmonton', + state: 'alberta', + country: 'Canada', + postalCode: '12345', + }, + context: {}, + recordId: '1', + }, + destination: { ...destination, Config: { ...destination.Config, enableHash: true } }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + body: { + FORM: {}, + JSON_ARRAY: {}, + JSON: { + createUsers: { + records: [ + { + hashedRecords: [ + { + country: + '6959097001d10501ac7d54c0bdb8db61420f658f2922cc26e46d536119a31126', + address: + '7e68f87b9675dca9a6cbd0b3b715af6cd9e0b75b72b96feec98dd334d665a76c', + city: '5ae1b46bce91b626720727f9d8d1eb8998e5b6586b339b97c2288595fe25116a', + firstName: + '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + email: + 'ee278943de84e5d6243578ee1a1057bcce0e50daad9755f45dfa64b60b13bc5d', + lastName: + '04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb', + phone: + '3daf505bba309a952bb4bbd010d1d39e413e40c679ac3bbcee1ea9b009023ffa', + postalCode: + '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', + state: + 'fb8e20fc2e4c3f248c60c39bd652f3c1347298bb977b8b4d5903b85055620603', + }, + ], + externalId: + 'Rudderstack_6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', + }, + ], + }, + associateUsers: { + patches: [ + { + op: 'add', + path: `/EXTERNAL_USER_ID-Rudderstack_6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b/audiences`, + value: ['dummyId'], + }, + ], + }, + }, + XML: {}, + }, + endpoint: '', + files: {}, + headers: { + 'Amazon-Advertising-API-ClientId': 'dummyClientId', + 'Content-Type': 'application/json', + Authorization: 'Bearer dummyAccessToken', + }, + method: 'POST', + params: {}, + type: 'REST', + userId: '', + version: '1', + }, + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + name: 'amazon_audience', + id: 'Test 2', + description: 'All traits are present with hash disabled for the audience with delete operation', + successCriteria: 'It should be passed with 200 Ok with all traits mapped without hashing', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'record', + action: 'delete', + fields, + channel: 'sources', + context: {}, + recordId: '1', + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: '', + headers: { + 'Amazon-Advertising-API-ClientId': 'dummyClientId', + 'Content-Type': 'application/json', + Authorization: 'Bearer dummyAccessToken', + }, + params: {}, + body: { + JSON: { + createUsers: { + records: [ + { + hashedRecords: [ + { + email: 'abc@xyz.com', + phone: '9876543323', + firstName: 'test', + lastName: 'user', + address: ' Été très chaud! ', + }, + ], + externalId: + 'Rudderstack_6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', + }, + ], + }, + associateUsers: { + patches: [ + { + op: 'remove', + path: `/EXTERNAL_USER_ID-Rudderstack_6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b/audiences`, + value: ['dummyId'], + }, + ], + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: generateMetadata(1), + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'amazon_audience', + id: 'Test 3', + description: 'Type Validation case', + successCriteria: 'It should be passed with 200 Ok giving validation error', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'identify', + context: {}, + recordId: '1', + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: generateMetadata(1), + statusCode: 400, + error: '[AMAZON AUDIENCE]: identify is not supported', + statTags: { + errorCategory: 'dataValidation', + destinationId: 'default-destinationId', + errorType: 'instrumentation', + destType: 'AMAZON_AUDIENCE', + module: 'destination', + implementation: 'native', + workspaceId: 'default-workspaceId', + feature: 'processor', + }, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/amazon_audience/router/data.ts b/test/integrations/destinations/amazon_audience/router/data.ts new file mode 100644 index 0000000000..6787e03160 --- /dev/null +++ b/test/integrations/destinations/amazon_audience/router/data.ts @@ -0,0 +1,226 @@ +import { destination, generateMetadata } from '../common'; + +export const data = [ + { + name: 'amazon_audience', + id: 'router-test-1', + description: 'batching based upon action', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + destination, + message: { + type: 'record', + action: 'delete', + fields: { email: 'email4@abc.com' }, + channel: 'sources', + context: {}, + recordId: '4', + }, + metadata: generateMetadata(1), + }, + { + destination, + message: { + type: 'record', + action: 'delete', + fields: { + email: 'email5@abc.com', + }, + channel: 'sources', + context: {}, + recordId: '5', + }, + metadata: generateMetadata(2), + }, + { + destination, + message: { + type: 'record', + action: 'insert', + fields: { email: 'email3@abc.com' }, + channel: 'sources', + context: {}, + recordId: '3', + }, + metadata: generateMetadata(3), + }, + { + destination, + message: { + type: 'record', + action: 'update', + fields: { + email: 'email1@abc.com', + }, + channel: 'sources', + context: {}, + recordId: '1', + }, + metadata: generateMetadata(4), + }, + { + destination, + message: { + type: 'record', + action: 'insert', + fields: { email: 'email2@abc.com' }, + channel: 'sources', + context: {}, + recordId: '2', + }, + metadata: generateMetadata(5), + }, + { + destination, + message: { + type: 'identify', + context: {}, + recordId: '1', + }, + metadata: generateMetadata(6), + }, + ], + destType: 'amazon_audience', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: true, + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: '', + headers: { + 'Amazon-Advertising-API-ClientId': 'dummyClientId', + 'Content-Type': 'application/json', + Authorization: 'Bearer dummyAccessToken', + }, + params: {}, + body: { + JSON: { + associateUsers: { + patches: [ + { + op: 'remove', + path: '/EXTERNAL_USER_ID-Rudderstack_17f8af97ad4a7f7639a4c9171d5185cbafb85462877a4746c21bdb0a4f940ca0/audiences', + value: ['dummyId'], + }, + ], + }, + createUsers: { + records: [ + { + externalId: + 'Rudderstack_17f8af97ad4a7f7639a4c9171d5185cbafb85462877a4746c21bdb0a4f940ca0', + hashedRecords: [ + { + email: 'email4@abc.com', + }, + { + email: 'email5@abc.com', + }, + ], + }, + ], + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + destination, + metadata: [generateMetadata(1), generateMetadata(2)], + statusCode: 200, + }, + { + batched: true, + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: '', + headers: { + 'Amazon-Advertising-API-ClientId': 'dummyClientId', + 'Content-Type': 'application/json', + Authorization: 'Bearer dummyAccessToken', + }, + params: {}, + body: { + JSON: { + associateUsers: { + patches: [ + { + op: 'add', + path: '/EXTERNAL_USER_ID-Rudderstack_a752d8ffaabe4c4d8a7a10cbdb2ee1525130a56a8290eef5d8a695434c49928f/audiences', + value: ['dummyId'], + }, + ], + }, + createUsers: { + records: [ + { + externalId: + 'Rudderstack_a752d8ffaabe4c4d8a7a10cbdb2ee1525130a56a8290eef5d8a695434c49928f', + hashedRecords: [ + { + email: 'email3@abc.com', + }, + { + email: 'email1@abc.com', + }, + { + email: 'email2@abc.com', + }, + ], + }, + ], + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + destination, + metadata: [generateMetadata(3), generateMetadata(4), generateMetadata(5)], + statusCode: 200, + }, + { + metadata: [generateMetadata(6)], + destination, + batched: false, + statusCode: 400, + error: '[AMAZON AUDIENCE]: identify is not supported', + statTags: { + errorCategory: 'dataValidation', + destinationId: 'default-destinationId', + errorType: 'instrumentation', + destType: 'AMAZON_AUDIENCE', + module: 'destination', + implementation: 'native', + workspaceId: 'default-workspaceId', + feature: 'router', + }, + }, + ], + }, + }, + }, + }, +];