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

feat: onboard new api for klaviyo 15-06-2024 #3574

Merged
merged 29 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2f5fcb0
chore: small fixes
anantjain45823 Jul 15, 2024
4c402fc
chore: small fixes+1
anantjain45823 Jul 15, 2024
4459b99
chore: tmp update transform.js for version switching
anantjain45823 Jul 15, 2024
b3b0a32
Update transform.js
anantjain45823 Jul 15, 2024
928aa81
chore: tmp commit
anantjain45823 Jul 15, 2024
c523a10
chore: tmp commit
anantjain45823 Jul 15, 2024
3a08c75
chore: update transformV2.js
anantjain45823 Jul 15, 2024
e7d0ea9
Update transformV2.js
anantjain45823 Jul 15, 2024
c018707
fix: improved quality for batching for v2
anantjain45823 Jul 17, 2024
7bd43a0
chore: add router test case
anantjain45823 Jul 17, 2024
1d99f2b
chore: small fix for track call and add util test
anantjain45823 Jul 17, 2024
c671518
chore: small fix for track call and add util test
anantjain45823 Jul 17, 2024
29db8d0
fix: remove error in case no identifier is present
anantjain45823 Jul 17, 2024
0877ea3
chore: remove env var lead version switching and add test cases
anantjain45823 Jul 17, 2024
d71c1ee
chore: address comments
anantjain45823 Jul 17, 2024
d150117
feat: onboard new api for klaviyo 15-06-2024
anantjain45823 Jul 19, 2024
c9a28a8
Merge branch 'develop' into feat.klaviyo_api_update_2
anantjain45823 Jul 19, 2024
caab584
chore: solve duplicate import
anantjain45823 Jul 19, 2024
11ccd2a
chore: resolve merge conflicts
anantjain45823 Jul 19, 2024
f1259fd
feat: add retl flow specific code
anantjain45823 Jul 19, 2024
09e06ce
chore: address comments
anantjain45823 Jul 19, 2024
e067fff
feat: introduce job ordering in klaviyo
anantjain45823 Jul 23, 2024
c9a4837
fix: batch logic to be more clear
anantjain45823 Jul 25, 2024
5f34cc6
chore: address comments
anantjain45823 Jul 27, 2024
f3e2998
fix: value field format and comments resolved
anantjain45823 Jul 30, 2024
f69b9d8
Merge branch 'develop' into feat.klaviyo_api_update_2
anantjain45823 Jul 30, 2024
a3cc45c
fix: merge develop fix
anantjain45823 Jul 30, 2024
1eb83fd
fix: lint errors
anantjain45823 Jul 30, 2024
364fadb
Merge branch 'develop' into feat.klaviyo_api_update_2
anantjain45823 Jul 30, 2024
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
1 change: 1 addition & 0 deletions src/constants/destinationCanonicalNames.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ const DestCanonicalNames = {
'Klaviyo Bulk Upload',
'klaviyobulkupload',
],
Klaviyo: ['KLAVIYO', 'Klaviyo', 'klaviyo'],
emarsys: ['EMARSYS', 'Emarsys', 'emarsys'],
wunderkind: ['wunderkind', 'Wunderkind', 'WUNDERKIND'],
cordial: ['cordial', 'Cordial', 'CORDIAL'],
Expand Down
201 changes: 201 additions & 0 deletions src/v0/destinations/klaviyo/batchUtil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
const lodash = require('lodash');
const { defaultRequestConfig, getSuccessRespEvents, isDefinedAndNotNull } = require('../../util');
const { JSON_MIME_TYPE } = require('../../util/constant');
const { BASE_ENDPOINT, CONFIG_CATEGORIES, MAX_BATCH_SIZE, revision } = require('./config');
const { buildRequest, getSubscriptionPayload } = require('./util');

/**
* This function groups the subscription responses on list id
* @param {*} subscribeResponseList
* @returns
* Example subsribeResponseList =
* [
* { payload: {id:'list_id', profile: {}}, metadata:{} },
* { payload: {id:'list_id', profile: {}}, metadata:{} }
* ]
*/
const groupSubscribeResponsesUsingListIdV2 = (subscribeResponseList) => {
const subscribeEventGroups = lodash.groupBy(
subscribeResponseList,
(event) => event.payload.listId,
);
return subscribeEventGroups;
};

/**
* This function takes susbscription as input and batches them into a single request body
* @param {subscription}
* subscription= {listId, subscriptionProfileList}
*/
const generateBatchedSubscriptionRequest = (subscription, destination) => {
const subscriptionPayloadResponse = defaultRequestConfig();
// fetching listId from first event as listId is same for all the events
const profiles = []; // list of profiles to be subscribed
const { listId, subscriptionProfileList } = subscription;
subscriptionProfileList.forEach((profileList) => profiles.push(...profileList));
subscriptionPayloadResponse.body.JSON = getSubscriptionPayload(listId, profiles);
subscriptionPayloadResponse.endpoint = `${BASE_ENDPOINT}/api/profile-subscription-bulk-create-jobs`;
subscriptionPayloadResponse.headers = {
Authorization: `Klaviyo-API-Key ${destination.Config.privateApiKey}`,
'Content-Type': JSON_MIME_TYPE,
Accept: JSON_MIME_TYPE,
revision,
};
return subscriptionPayloadResponse;
};

/**
* This function generates requests using profiles array and returns an array of all these requests
* @param {*} profiles
* @param {*} destination
*/
const getProfileRequests = (profiles, destination) => {
const profilePayloadResponses = profiles.map((profile) =>
buildRequest(profile, destination, CONFIG_CATEGORIES.IDENTIFYV2),
);
return profilePayloadResponses;
};

/**
* this function populates profileSubscriptionAndMetadataArr with respective profiles based upon common metadata
* @param {*} profileSubscriptionAndMetadataArr
* @param {*} metaDataIndexMap
* @param {*} profiles
* @returns updated profileSubscriptionAndMetadataArr obj
*/
const populateArrWithRespectiveProfileData = (
profileSubscriptionAndMetadataArr,
metaDataIndexMap,
profiles,
) => {
const updatedPSMArr = lodash.cloneDeep(profileSubscriptionAndMetadataArr);
profiles.forEach((profile) => {
const index = metaDataIndexMap.get(profile.metadata.jobId);
if (isDefinedAndNotNull(index)) {
// using isDefinedAndNotNull as index can be 0
updatedPSMArr[index].profiles.push(profile.payload);
} else {
// in case there is no subscription for a given profile
updatedPSMArr.push({
profiles: [profile.payload],
metadataList: [profile.metadata],
});
}
});
return updatedPSMArr;
};

/**
* This function generates the final output batched payload for each object in profileSubscriptionAndMetadataArr
* ex:
* profileSubscriptionAndMetadataArr = [
{
subscription: { subscriptionProfileList, listId1 },
metadataList1,
profiles: [respectiveProfiles for above metadata]
},
{
subscription: { subscriptionProfile List With No Profiles, listId2 },
metadataList2,
},
{
metadataList3,
profiles: [respectiveProfiles for above metadata with no subscription]
}
]
* @param {*} profileSubscriptionAndMetadataArr
* @param {*} destination
* @returns
*/
const buildRequestsForProfileSubscriptionAndMetadataArr = (
profileSubscriptionAndMetadataArr,
destination,
) => {
const finalResponseList = [];
profileSubscriptionAndMetadataArr.forEach((profileSubscriptionData) => {
const batchedRequest = [];
// we are keeping profiles request prior to subscription ones as first profile creation and then subscription should happen
if (profileSubscriptionData.profiles?.length > 0) {
batchedRequest.push(...getProfileRequests(profileSubscriptionData.profiles, destination));
}

if (profileSubscriptionData.subscription?.subscriptionProfileList?.length > 0) {
batchedRequest.push(
generateBatchedSubscriptionRequest(profileSubscriptionData.subscription, destination),
);
}

finalResponseList.push(
getSuccessRespEvents(batchedRequest, profileSubscriptionData.metadataList, destination, true),
);
});
return finalResponseList;
};

const batchRequestV2 = (subscribeRespList, profileRespList, destination) => {
const subscribeEventGroups = groupSubscribeResponsesUsingListIdV2(subscribeRespList);
let profileSubscriptionAndMetadataArr = [];
const metaDataIndexMap = new Map();
Object.keys(subscribeEventGroups).forEach((listId) => {
// eventChunks = [[e1,e2,e3,..batchSize],[e1,e2,e3,..batchSize]..]
const eventChunks = lodash.chunk(subscribeEventGroups[listId], MAX_BATCH_SIZE);
eventChunks.forEach((chunk, index) => {
// get subscriptionProfiles for the chunk
const subscriptionProfileList = chunk.map((event) => event.payload?.profile);
// get metadata for this chunk
const metadataList = chunk.map((event) => event.metadata);
// get list of jobIds from the above metdadata
const jobIdList = metadataList.map((metadata) => metadata.jobId);
// push the jobId: index to metadataIndex mapping which let us know the metadata respective payload index position in batched request
jobIdList.forEach((jobId) => {
metaDataIndexMap.set(jobId, index);
});
profileSubscriptionAndMetadataArr.push({
subscription: { subscriptionProfileList, listId },
metadataList,
profiles: [],
});
});
});
profileSubscriptionAndMetadataArr = populateArrWithRespectiveProfileData(
profileSubscriptionAndMetadataArr,
metaDataIndexMap,
profileRespList,
);
/* Till this point I have a profileSubscriptionAndMetadataArr
containing the the events in one object for which batching has to happen in following format
[
{
subscription: { subscriptionProfileList, listId1 },
metadataList1,
profiles: [respectiveProfiles for above metadata]
},
{
subscription: { subscriptionProfile List With No Profiles, listId2 },
metadataList2,
},
{
metadataList3,
profiles: [respectiveProfiles for above metadata with no subscription]
}
]
*/
return buildRequestsForProfileSubscriptionAndMetadataArr(
profileSubscriptionAndMetadataArr,
destination,
);
/* for identify calls with batching batched with identify with no batching
we will sonctruct O/P as:
[
[2 calls for identifywith batching],
[1 call identify calls with batching]
]
*/
};

module.exports = {
groupSubscribeResponsesUsingListIdV2,
populateArrWithRespectiveProfileData,
generateBatchedSubscriptionRequest,
batchRequestV2,
};
189 changes: 189 additions & 0 deletions src/v0/destinations/klaviyo/batchUtil.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
const {
groupSubscribeResponsesUsingListIdV2,
populateArrWithRespectiveProfileData,
generateBatchedSubscriptionRequest,
} = require('./batchUtil');
const { revision } = require('./config');

describe('groupSubscribeResponsesUsingListIdV2', () => {
// Groups subscription responses by listId correctly
it('should group subscription responses by listId correctly when given a valid list', () => {
const subscribeResponseList = [
{ payload: { listId: 'list_1', profile: {} }, metadata: {} },
{ payload: { listId: 'list_1', profile: {} }, metadata: {} },
{ payload: { listId: 'list_2', profile: {} }, metadata: {} },
];

const expectedOutput = {
list_1: [
{ payload: { listId: 'list_1', profile: {} }, metadata: {} },
{ payload: { listId: 'list_1', profile: {} }, metadata: {} },
],
list_2: [{ payload: { listId: 'list_2', profile: {} }, metadata: {} }],
};

const result = groupSubscribeResponsesUsingListIdV2(subscribeResponseList);

expect(result).toEqual(expectedOutput);
});

// Handles empty subscription response list
it('should return an empty object when given an empty subscription response list', () => {
const subscribeResponseList = [];

const expectedOutput = {};

const result = groupSubscribeResponsesUsingListIdV2(subscribeResponseList);

expect(result).toEqual(expectedOutput);
});
});

describe('populateArrWithRespectiveProfileData', () => {
// Correctly populates array when all profiles have corresponding metadata
it('should correctly populate array when all profiles have corresponding metadata', () => {
const profileSubscriptionAndMetadataArr = [
{ profiles: [], metadataList: [{ jobId: '1' }], subscriptions: [] },
{ profiles: [], metadataList: [{ jobId: '2' }], subscriptions: [] },
];
const metadataIndexMap = new Map([
['1', 0],
['2', 1],
]);
const profiles = [
{ payload: { name: 'John' }, metadata: { jobId: '1' } },
{ payload: { name: 'Doe' }, metadata: { jobId: '2' } },
];

const result = populateArrWithRespectiveProfileData(
profileSubscriptionAndMetadataArr,
metadataIndexMap,
profiles,
);

expect(result[0].profiles).toEqual([{ name: 'John' }]);
expect(result[1].profiles).toEqual([{ name: 'Doe' }]);
});

// Handles empty profileSubscriptionAndMetadataArr input
it('should handle empty profileSubscriptionAndMetadataArr input', () => {
const profileSubscriptionAndMetadataArr = [];
const metadataIndexMap = new Map();
const profiles = [{ payload: { name: 'John' }, metadata: { jobId: '1' } }];

const result = populateArrWithRespectiveProfileData(
profileSubscriptionAndMetadataArr,
metadataIndexMap,
profiles,
);

expect(result).toEqual([
{
profiles: [{ name: 'John' }],
metadataList: [{ jobId: '1' }],
},
]);
});
});

// Generated by CodiumAI

describe('generateBatchedSubscriptionRequest', () => {
// Generates a batched subscription request with valid subscription and destination inputs
it('should generate a valid batched subscription request when given valid subscription and destination inputs', () => {
const subscription = {
listId: 'test-list-id',
subscriptionProfileList: [[{ id: 'profile1' }, { id: 'profile2' }], [{ id: 'profile3' }]],
};
const destination = {
Config: {
privateApiKey: 'test-api-key',
},
};
const expectedRequest = {
version: '1',
type: 'REST',
method: 'POST',
endpoint: 'https://a.klaviyo.com/api/profile-subscription-bulk-create-jobs',
headers: {
Authorization: 'Klaviyo-API-Key test-api-key',
'Content-Type': 'application/json',
Accept: 'application/json',
revision,
},
params: {},
body: {
JSON: {
data: {
type: 'profile-subscription-bulk-create-job',
attributes: {
profiles: { data: [{ id: 'profile1' }, { id: 'profile2' }, { id: 'profile3' }] },
},
relationships: {
list: {
data: {
type: 'list',
id: 'test-list-id',
},
},
},
},
},
JSON_ARRAY: {},
XML: {},
FORM: {},
},
files: {},
};
const result = generateBatchedSubscriptionRequest(subscription, destination);
expect(result).toEqual(expectedRequest);
});

// Handles empty subscriptionProfileList gracefully
it('should handle empty subscriptionProfileList gracefully', () => {
const subscription = {
listId: 'test-list-id',
subscriptionProfileList: [],
};
const destination = {
Config: {
privateApiKey: 'test-api-key',
},
};
const expectedRequest = {
version: '1',
type: 'REST',
method: 'POST',
endpoint: 'https://a.klaviyo.com/api/profile-subscription-bulk-create-jobs',
headers: {
Authorization: 'Klaviyo-API-Key test-api-key',
'Content-Type': 'application/json',
Accept: 'application/json',
revision,
},
params: {},
body: {
JSON: {
data: {
type: 'profile-subscription-bulk-create-job',
attributes: { profiles: { data: [] } },
relationships: {
list: {
data: {
type: 'list',
id: 'test-list-id',
},
},
},
},
},
JSON_ARRAY: {},
XML: {},
FORM: {},
},
files: {},
};
const result = generateBatchedSubscriptionRequest(subscription, destination);
expect(result).toEqual(expectedRequest);
});
});
Loading
Loading