Skip to content

Commit

Permalink
chore: add network handler with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
anantjain45823 committed Sep 17, 2024
1 parent 38faf36 commit 51f92d4
Show file tree
Hide file tree
Showing 9 changed files with 495 additions and 9 deletions.
3 changes: 2 additions & 1 deletion src/v0/destinations/amazon_audience/config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const CREATE_USERS_URL = 'https://advertising-api.amazon.com/dp/records/hashed/';
const ASSOCIATE_USERS_URL = 'https://advertising-api.amazon.com/dp/records/hashed/';
const MAX_PAYLOAD_SIZE_IN_BYTES = 4000000;
module.exports = { CREATE_USERS_URL, MAX_PAYLOAD_SIZE_IN_BYTES, ASSOCIATE_USERS_URL };
const DESTINATION = 'amazon_audience';
module.exports = { CREATE_USERS_URL, MAX_PAYLOAD_SIZE_IN_BYTES, ASSOCIATE_USERS_URL, DESTINATION };
124 changes: 124 additions & 0 deletions src/v0/destinations/amazon_audience/networkHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
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) {
// 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(

Check warning on line 35 in src/v0/destinations/amazon_audience/networkHandler.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/amazon_audience/networkHandler.js#L35

Added line #L35 was not covered by tests
`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(

Check warning on line 44 in src/v0/destinations/amazon_audience/networkHandler.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/amazon_audience/networkHandler.js#L44

Added line #L44 was not covered by tests
`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) => {
const { httpResponse } = await handleHttpRequest(
method,
url,
data,
{ headers },
{
destType: 'amazon_audience',
feature: 'proxy',
endpointPath: '/records/hashed',
requestMethod: 'POST',
module: 'dataDelivery',
metadata,
},
);
return httpResponse;
};

const amazonAudienceProxyRequest = async (request) => {
const { body, metadata } = request;
const { headers } = request;
const { JSON } = body;
const { createUsers, associateUsers } = JSON;

// step 1: Create users
const firstResponse = await makeRequest(CREATE_USERS_URL, createUsers, headers, metadata, 'post');
// 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
return makeRequest(ASSOCIATE_USERS_URL, associateUsers, headers, metadata, 'patch');
};
// 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,
};
2 changes: 1 addition & 1 deletion src/v0/destinations/amazon_audience/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const process = (event) => {
const { message, destination, metadata } = event;
const { Config } = destination;
const { user, action } = processRecord(message, Config);
return buildResponseWithUsers([user], action, Config, [metadata.jobId]);
return buildResponseWithUsers([user], action, Config, [metadata.jobId], metadata.secret);
};
// This function is used to process multiple records
const processRouterDest = async (inputs, reqMetadata) => {
Expand Down
25 changes: 22 additions & 3 deletions src/v0/destinations/amazon_audience/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
const sha256 = require('sha256');
const lodash = require('lodash');
const { ConfigurationError } = require('@rudderstack/integrations-lib');
const { ConfigurationError, OAuthSecretError } = require('@rudderstack/integrations-lib');
const {
defaultRequestConfig,
defaultPostRequestConfig,
Expand All @@ -10,15 +10,26 @@ const {
} = require('../../util');

// Docs: https://developer.x.com/en/docs/x-ads-api/audiences/api-reference/custom-audience-user
const buildResponseWithUsers = (users, action, config, jobIdList) => {
const buildResponseWithUsers = (users, action, config, jobIdList, secret) => {
const { audienceId } = config;
if (!audienceId) {
throw new ConfigurationError('[AMAZON AUDIENCE]: Audience Id not found');

Check warning on line 16 in src/v0/destinations/amazon_audience/utils.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/amazon_audience/utils.js#L16

Added line #L16 was not covered by tests
}
if (!secret?.accessToken) {
throw new OAuthSecretError('OAuth - access token not found');

Check warning on line 19 in src/v0/destinations/amazon_audience/utils.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/amazon_audience/utils.js#L19

Added line #L19 was not covered by tests
}
if (!secret?.clientId) {
throw new OAuthSecretError('OAuth - Client Id not found');

Check warning on line 22 in src/v0/destinations/amazon_audience/utils.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/amazon_audience/utils.js#L22

Added line #L22 was not covered by tests
}
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: [
Expand Down Expand Up @@ -64,8 +75,10 @@ const groupResponsesUsingOperation = (respList) => {
* @param {*} responseList
*/
const batchEvents = (responseList, destination) => {
const { secret } = responseList[0].metadata;
const eventGroups = groupResponsesUsingOperation(responseList);
const respList = [];

Object.keys(eventGroups).forEach((op) => {
const { userList, jobIdList, metadataList } = eventGroups[op].reduce(
(acc, event) => ({
Expand All @@ -77,7 +90,13 @@ const batchEvents = (responseList, destination) => {
);
respList.push(
getSuccessRespEvents(
buildResponseWithUsers(userList, op, destination.config || destination.Config, jobIdList),
buildResponseWithUsers(
userList,
op,
destination.config || destination.Config,
jobIdList,
secret,
),
metadataList,
destination,
true,
Expand Down
5 changes: 5 additions & 0 deletions test/integrations/destinations/amazon_audience/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,10 @@ export const generateMetadata = (jobId: number, userId?: string): any => {
destinationId: 'default-destinationId',
workspaceId: 'default-workspaceId',
dontBatch: false,
secret: {
accessToken: 'dummyAccessToken',
refreshToken: 'dummyRefreshToken',
clientId: 'dummyClientId',
},
};
};
Loading

0 comments on commit 51f92d4

Please sign in to comment.