Skip to content

fix: adding transformer proxy for iterable #3878

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

Merged
merged 25 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
096d008
fix: adding transformer proxy for iterable
shrouti1507 Nov 14, 2024
2a10f70
Merge branch 'develop' into fix.iterable-networkhandler
shrouti1507 Nov 14, 2024
2b34182
fix: adding component test cases
shrouti1507 Nov 14, 2024
8742244
fix: adding factory pattern
shrouti1507 Nov 18, 2024
25a5ced
fix: code redesigning
shrouti1507 Nov 20, 2024
c87c5ce
fix: decoupling networkHandler and specific strategy
shrouti1507 Nov 20, 2024
1058ec1
fix: simplifying logic
shrouti1507 Nov 20, 2024
f11d7eb
Merge branch 'develop' into fix.iterable-networkhandler
shrouti1507 Dec 3, 2024
909cad0
fix: convert to registry pattern
shrouti1507 Dec 3, 2024
2337c7b
fix: converting to typescript
shrouti1507 Dec 3, 2024
9eb31bf
fix: removing unnecessary comments
shrouti1507 Dec 3, 2024
97d531f
fix: adding data delivery test cases for all endpoints
shrouti1507 Dec 6, 2024
3789652
chore: improve iterable network handler (#3918)
Dec 7, 2024
aae7b3d
Merge branch 'develop' into fix.iterable-networkhandler
shrouti1507 Dec 12, 2024
9ee2847
fix: review comments addressed
shrouti1507 Dec 12, 2024
a5ba58c
fix: small refactoring
shrouti1507 Dec 12, 2024
d00f31f
fix: update for supporting disallowed events
shrouti1507 Dec 12, 2024
707dfe8
fix: code review suggestion
shrouti1507 Dec 16, 2024
8e5d6ab
fix: fixing test cases
shrouti1507 Dec 16, 2024
9ce4575
fix: review comment addressed
shrouti1507 Dec 16, 2024
de1bb5c
fix: adding type definitions
shrouti1507 Jan 3, 2025
85b1166
fix: separating handle error functions
shrouti1507 Jan 3, 2025
933473a
Merge branch 'develop' into fix.iterable-networkhandler
shrouti1507 Jan 3, 2025
ebfeffc
fix: migrating processor and router test cases
shrouti1507 Jan 6, 2025
4cc52d2
fix: review comment addressed
shrouti1507 Jan 6, 2025
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', () => {
// 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