Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: adding transformer proxy for iterable #3878

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/v0/destinations/iterable/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const ConfigCategory = {
name: 'IterableIdentifyConfig',
action: 'identify',
endpoint: `users/update`,
bulkEndpoint: 'users/bulkUpdate',
},
PAGE: {
name: 'IterablePageConfig',
Expand All @@ -35,6 +36,7 @@ const ConfigCategory = {
name: 'IterableTrackConfig',
action: 'track',
endpoint: `events/track`,
bulkEndpoint: 'events/trackBulk',
},
TRACK_PURCHASE: {
name: 'IterableTrackPurchaseConfig',
Expand Down Expand Up @@ -76,16 +78,41 @@ const constructEndpoint = (dataCenter, category) => {
return `${baseUrl}${category.endpoint}`;
};

const BULK_ENDPOINTS = Object.values(ConfigCategory)
.filter((config) => config.bulkEndpoint)
.map((config) => `/api/${config.bulkEndpoint}`);

const IDENTIFY_MAX_BATCH_SIZE = 1000;
const IDENTIFY_MAX_BODY_SIZE_IN_BYTES = 4000000;

const TRACK_MAX_BATCH_SIZE = 8000;

const ITERABLE_RESPONSE_USER_ID_PATHS = [
'invalidUserIds',
'failedUpdates.invalidUserIds',
'failedUpdates.notFoundUserIds',
'failedUpdates.forgottenUserIds',
'failedUpdates.conflictUserIds',
'failedUpdates.invalidDataUserIds',
];

const ITERABLE_RESPONSE_EMAIL_PATHS = [
'invalidEmails',
'failedUpdates.invalidEmails',
'failedUpdates.notFoundEmails',
'failedUpdates.forgottenEmails',
'failedUpdates.conflictEmails',
'failedUpdates.invalidDataEmails',
];

module.exports = {
mappingConfig,
ConfigCategory,
constructEndpoint,
TRACK_MAX_BATCH_SIZE,
IDENTIFY_MAX_BATCH_SIZE,
IDENTIFY_MAX_BODY_SIZE_IN_BYTES,
ITERABLE_RESPONSE_USER_ID_PATHS,
ITERABLE_RESPONSE_EMAIL_PATHS,
BULK_ENDPOINTS,
};
95 changes: 89 additions & 6 deletions src/v0/destinations/iterable/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ const {
TRACK_MAX_BATCH_SIZE,
IDENTIFY_MAX_BATCH_SIZE,
IDENTIFY_MAX_BODY_SIZE_IN_BYTES,
// API_RESPONSE_PATHS,
constructEndpoint,
ITERABLE_RESPONSE_USER_ID_PATHS,
ITERABLE_RESPONSE_EMAIL_PATHS,
} = require('./config');
const { JSON_MIME_TYPE } = require('../../util/constant');
const { EventType, MappedToDestinationKey } = require('../../../constants');
Expand Down Expand Up @@ -483,6 +486,7 @@ const batchUpdateUserEvents = (updateUserEvents, registerDeviceOrBrowserTokenEve

/**
* Processes chunks of catalog events, extracts the necessary data, and prepares batched requests for further processing
* ref : https://api.iterable.com/api/docs#catalogs_bulkUpdateCatalogItems
* @param {*} catalogEventsChunks
* @returns
*/
Expand Down Expand Up @@ -600,12 +604,12 @@ const batchTrackEvents = (trackEvents) => {
*/
const prepareBatchRequests = (filteredEvents) => {
const {
trackEvents,
catalogEvents,
errorRespList,
updateUserEvents,
eventResponseList,
registerDeviceOrBrowserTokenEvents,
trackEvents, // track
catalogEvents, // identify
errorRespList, // track
updateUserEvents, // identify
eventResponseList, // track
registerDeviceOrBrowserTokenEvents, // identify
} = filteredEvents;

const updateUserBatchedResponseList =
Expand Down Expand Up @@ -744,6 +748,84 @@ const filterEventsAndPrepareBatchRequests = (transformedEvents) => {
return prepareBatchRequests(filteredEvents);
};

/**
* Checks if a value is present in a response array based on a given path.
* @param {Object} response - The response object to search within.
* @param {string} path - The path to the response array.
* @param {any} value - The value to check for in the array.
* @returns {boolean} - True if the value is in the array, otherwise false.
*/
const isValueInResponseArray = (response, path, value) => {
const respArr = get(response, path);
return Array.isArray(respArr) && respArr.includes(value);
};

/**
* Determines if an event should be aborted based on the response from a destination
* and extracts an error message if applicable.
* ref:
* 1) https://api.iterable.com/api/docs#users_updateEmail
* 2) https://api.iterable.com/api/docs#events_track
* 3) https://api.iterable.com/api/docs#users_bulkUpdateUser
* 4) https://api.iterable.com/api/docs#events_trackBulk
* 5) https://api.iterable.com/api/docs#catalogs_bulkUpdateCatalogItems
* 6) https://api.iterable.com/api/docs#users_registerDeviceToken
* 7) https://api.iterable.com/api/docs#users_registerBrowserToken
* 8) https://api.iterable.com/api/docs#commerce_trackPurchase
* 9) https://api.iterable.com/api/docs#commerce_updateCart
*
* @param {Object} event - The event object containing various event properties.
* @param {Object} destinationResponse - The response object from the destination.
* @returns {Object} An object containing a boolean `isAbortable` indicating if the event
* should be aborted, and an `errorMsg` string with the error message if applicable.
*/
const checkIfEventIsAbortableAndExtractErrorMessage = (event, destinationResponse) => {
const { failCount } = destinationResponse.response;

if (failCount === 0) {
return { isAbortable: false, errorMsg: '' };
}

const eventValues = {
email: event.email,
userId: event.userId,
eventName: event.eventName,
};

let errorMsg = '';
const userIdMatchPath = ITERABLE_RESPONSE_USER_ID_PATHS.filter((userIdPath) =>
isValueInResponseArray(destinationResponse.response, userIdPath, eventValues.userId),
);
if (userIdMatchPath.length > 0) {
errorMsg += `userId error:"${eventValues.userId}" in "${userIdMatchPath}".`;
}

const emailMatchPath = ITERABLE_RESPONSE_EMAIL_PATHS.filter((emailPath) =>
isValueInResponseArray(destinationResponse.response, emailPath, eventValues.email),
);

if (emailMatchPath.length > 0) {
errorMsg += `email error:"${eventValues.email}" in "${emailMatchPath}".`;
}

const eventNameMatchPath = ['disallowedEventNames'].filter((eventNamePath) =>
isValueInResponseArray(destinationResponse.response, eventNamePath, eventValues.eventName),
);

if (eventNameMatchPath.length > 0) {
errorMsg += `eventName error:"${eventValues.eventName}" in "${eventNameMatchPath}".`;
}

if (errorMsg) {
return {
isAbortable: true,
errorMsg,
};
}

return { isAbortable: false, errorMsg: '' };
};

module.exports = {
getCatalogEndpoint,
hasMultipleResponses,
Expand All @@ -758,5 +840,6 @@ module.exports = {
filterEventsAndPrepareBatchRequests,
registerDeviceTokenEventPayloadBuilder,
registerBrowserTokenEventPayloadBuilder,
checkIfEventIsAbortableAndExtractErrorMessage,
getCategoryWithEndpoint,
};
152 changes: 152 additions & 0 deletions src/v0/destinations/iterable/util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const {
updateUserEventPayloadBuilder,
registerDeviceTokenEventPayloadBuilder,
registerBrowserTokenEventPayloadBuilder,
checkIfEventIsAbortableAndExtractErrorMessage,
} = require('./util');

const { ConfigCategory } = require('./config');
Expand Down Expand Up @@ -799,4 +800,155 @@ describe('iterable utils test', () => {
);
});
});
describe('checkIfEventIsAbortableAndExtractErrorMessage', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add test-case with multiple path matches for same email(for example) as well ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

// Returns non-abortable and empty error message when failCount is 0
it('should return non-abortable and empty error message when failCount is 0', () => {
const event = {
email: '[email protected]',
userId: 'user123',
eventName: 'testEvent',
};
const destinationResponse = {
response: {
failCount: 0,
},
};

const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse);
expect(result).toEqual({ isAbortable: false, errorMsg: '' });
});

// Handles undefined or null event fields gracefully
it('should handle undefined or null event fields gracefully', () => {
const event = {
email: null,
userId: undefined,
eventName: 'testEvent',
};
const destinationResponse = {
response: {
failCount: 1,
invalidEmails: ['[email protected]'],
},
};
const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse);
expect(result).toEqual({ isAbortable: false, errorMsg: '' });
});

// Handles events with all expected fields present
it('should handle events with all expected fields present and return non-abortable when no match', () => {
const event = {
email: '[email protected]',
userId: 'user123',
eventName: 'purchase',
id: 'event123',
createdAt: '2023-10-01T00:00:00Z',
campaignId: 'campaign123',
templateId: 'template123',
createNewFields: true,
dataFields: { field1: 'value1' },
};

const destinationResponse = {
response: {
failCount: 1,
invalidEmails: ['[email protected]'],
},
};

const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse);

expect(result.isAbortable).toBe(false);
expect(result.errorMsg).toBe('');
});

// Returns appropriate error message for abortable event

it('should find the right value for which it should fail and passes otherwise for emails', () => {
const event = {
email: 'test',
userId: 'user123',
eventName: 'purchase',
dataFields: { customField1: 'value1', customField2: 'value2' },
};
const destinationResponse = {
response: {
failCount: 1,
failedUpdates: {
invalidEmails: ['test'],
},
},
};
const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse);
expect(result).toEqual({
isAbortable: true,
errorMsg: 'email error:"test" in "failedUpdates.invalidEmails".',
});
});

it('should find the right value for which it should fail', () => {
const event = {
email: '[email protected]',
userId: 'user123',
eventName: 'purchase',
dataFields: { customField1: 'test', customField2: 'value2' },
};
const destinationResponse = {
response: {
failCount: 1,
failedUpdates: {
invalidEmails: ['test'],
},
},
};
const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse);
expect(result.isAbortable).toBe(false);
expect(result.errorMsg).toBe('');
});

it('should find the right value for which it should fail and passes otherwise for userIds', () => {
const event = {
email: 'test',
userId: 'user123',
eventName: 'purchase',
dataFields: { customField1: 'value1', customField2: 'value2' },
};
const destinationResponse = {
response: {
failCount: 1,
failedUpdates: {
invalidUserIds: ['user123'],
},
},
};
const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse);
expect(result).toEqual({
isAbortable: true,
errorMsg: 'userId error:"user123" in "failedUpdates.invalidUserIds".',
});
});

it('should find the right value for which it should fail and passes otherwise for disallowed events', () => {
const event = {
email: 'test',
userId: 'user123',
eventName: 'purchase',
dataFields: { customField1: 'value1', customField2: 'value2' },
};
const destinationResponse = {
response: {
failCount: 1,
disallowedEventNames: ['purchase'],
failedUpdates: {
invalidUserIds: [],
},
},
};
const result = checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse);
expect(result).toEqual({
isAbortable: true,
errorMsg: 'eventName error:"purchase" in "disallowedEventNames".',
});
});
});
});
38 changes: 38 additions & 0 deletions src/v1/destinations/iterable/networkHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { prepareProxyRequest, proxyRequest } from '../../../adapters/network';
import { processAxiosResponse } from '../../../adapters/utils/networkUtils';
import { BULK_ENDPOINTS } from '../../../v0/destinations/iterable/config';
import { GenericStrategy } from './strategies/generic';
import { TrackIdentifyStrategy } from './strategies/track-identify';

type ResponseParams = {
destinationRequest: {
endpoint: string;
};
};

const strategyRegistry: { [key: string]: any } = {
[TrackIdentifyStrategy.name]: new TrackIdentifyStrategy(),
[GenericStrategy.name]: new GenericStrategy(),
};

const getResponseStrategy = (endpoint: string) => {
if (BULK_ENDPOINTS.some((path) => endpoint.includes(path))) {
return strategyRegistry[TrackIdentifyStrategy.name];
}
return strategyRegistry[GenericStrategy.name];
};

const responseHandler = (responseParams: ResponseParams) => {
const { destinationRequest } = responseParams;
const strategy = getResponseStrategy(destinationRequest.endpoint);
return strategy.handleResponse(responseParams);
};

function networkHandler(this: any) {
this.prepareProxy = prepareProxyRequest;
this.proxy = proxyRequest;
this.processAxiosResponse = processAxiosResponse;
this.responseHandler = responseHandler;
}

export { networkHandler };
Loading
Loading