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 22 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 @@ -173,6 +173,7 @@ const DestCanonicalNames = {
'Klaviyo Bulk Upload',
'klaviyobulkupload',
],
Klaviyo: ['KLAVIYO', 'Klaviyo', 'klaviyo'],
emarsys: ['EMARSYS', 'Emarsys', 'emarsys'],
wunderkind: ['wunderkind', 'Wunderkind', 'WUNDERKIND'],
};
Expand Down
186 changes: 186 additions & 0 deletions src/v0/destinations/klaviyo/batchUtil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
const lodash = require('lodash');
const { defaultBatchRequestConfig, getSuccessRespEvents } = 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 returns the list of profileReq which do not metadata common with subcriptionMetadataArray
* @param {*} profileReq
* @param {*} subscriptionMetadataArray
* @returns
*/
const getRemainingProfiles = (profileReq, subscriptionMetadataArray) => {
const subscriptionListJobIds = subscriptionMetadataArray.map((metadata) => metadata.jobId);
return profileReq.filter((profile) => !subscriptionListJobIds.includes(profile.metadata.jobId));
};

/**
* This function builds all the profile requests whose metadata is not there in subscriptionMetadataArray
* @param {*} profileRespList
* @param {*} subscriptionMetadataArray
* @param {*} destination
* @returns
*/
const getProfiles = (profileRespList, subscriptionMetadataArray, destination) => {
const profiles = [];
const remainingProfileReq = getRemainingProfiles(profileRespList, subscriptionMetadataArray);
remainingProfileReq.forEach((input) => {
profiles.push(
getSuccessRespEvents(
buildRequest(input.payload, destination, CONFIG_CATEGORIES.IDENTIFYV2),
[input.metadata],
destination,
),
);
});
return profiles;
};

/**
* This function takes susbscriptions as input and batches them into a single request body
* @param {events}
* events= [
* { payload: {id:'list_id', profile: {}}, metadata:{} },
* { payload: {id:'list_id', profile: {}}, metadata:{} }
* ]
*/
const generateBatchedSubscriptionRequest = (events, destination) => {
const batchEventResponse = defaultBatchRequestConfig();
const metadata = [];
// fetching listId from first event as listId is same for all the events
const listId = events[0].payload?.listId;
const profiles = []; // list of profiles to be subscribes
// Batch profiles into dest batch structure
events.forEach((ev) => {
anantjain45823 marked this conversation as resolved.
Show resolved Hide resolved
profiles.push(...ev.payload.profile);
anantjain45823 marked this conversation as resolved.
Show resolved Hide resolved
metadata.push(ev.metadata);
});

batchEventResponse.batchedRequest = Object.values(batchEventResponse);
batchEventResponse.batchedRequest[0].body.JSON = getSubscriptionPayload(listId, profiles);

batchEventResponse.batchedRequest[0].endpoint = `${BASE_ENDPOINT}/api/profile-subscription-bulk-create-jobs`;

batchEventResponse.batchedRequest[0].headers = {
Authorization: `Klaviyo-API-Key ${destination.Config.privateApiKey}`,
'Content-Type': JSON_MIME_TYPE,
Accept: JSON_MIME_TYPE,
revision,
};

return {
...batchEventResponse,
metadata,
destination,
};
};

/**
* This function fetches the profileRequests with metadata present in metadata array build a request for them
* and add these requests batchEvent Response
* @param {*} profileReq array of profile requests
* @param {*} metadataArray array of metadata
* @param {*} batchEventResponse
* Example: /**
*
* @param {*} subscribeEventGroups
* @param {*} identifyResponseList
* @returns
* Example:
* profileReq = [
* { payload: {}, metadata:{} },
* { payload: {}, metadata:{} }
* ]
*/
const updateBatchEventResponseWithProfileRequests = (
profileReqArr,
subscriptionMetadataArray,
batchEventResponse,
) => {
const subscriptionListJobIds = subscriptionMetadataArray.map((metadata) => metadata.jobId);
const profilesRequests = [];
profileReqArr.forEach((profile) => {
if (subscriptionListJobIds.includes(profile.metadata.jobId)) {
profilesRequests.push(
buildRequest(profile.payload, batchEventResponse.destination, CONFIG_CATEGORIES.IDENTIFYV2),
);
}
});
// we are keeping profiles request prior to subscription ones
batchEventResponse.batchedRequest.unshift(...profilesRequests);
};

const processSubscribeChunk = (chunk, destination, profileRespList) => {
const batchEventResponse = generateBatchedSubscriptionRequest(chunk, destination);
const { metadata: subscriptionMetadataArray } = batchEventResponse;
updateBatchEventResponseWithProfileRequests(
profileRespList,
subscriptionMetadataArray,
batchEventResponse,
);
return batchEventResponse;
};

/**
* This function batches the requests. Alogorithm
* Batch events from Subscribe Resp List having same listId/groupId to be subscribed and have their metadata array
* For this metadata array get all profileRequests and add them prior to batched Subscribe Request in the same batched Request
* Make another batched request for the remaning profile requests and another for all the event requests
* @param {*} subscribeRespList
* @param {*} profileRespList
* @param {*} eventRespList
* subscribeRespList = [
* { payload: {id:'list_id', profile: {}}, metadata:{} },
* { payload: {id:'list_id', profile: {}}, metadata:{} }
* ]
* profileRespList = [
* { payload: {}, metadata:{} },
* { payload: {}, metadata:{} }
* ]
*
*/
const batchSubscriptionRequestV2 = (subscribeRespList, profileRespList, destination) => {
const batchedResponseList = [];
const subscriptionMetadataArrayForAll = [];
const subscribeEventGroups = groupSubscribeResponsesUsingListIdV2(subscribeRespList);
Object.keys(subscribeEventGroups).forEach((listId) => {
// eventChunks = [[e1,e2,e3,..batchSize],[e1,e2,e3,..batchSize]..]
const eventChunks = lodash.chunk(subscribeEventGroups[listId], MAX_BATCH_SIZE);
const batchedResponse = [];
eventChunks.forEach((chunk) => {
// returns subscriptionMetadata and batchEventResponse
const { metadata: subscriptionMetadataArray, batchedRequest } = processSubscribeChunk(
chunk,
destination,
profileRespList,
);
subscriptionMetadataArrayForAll.push(...subscriptionMetadataArray);
batchedResponse.push(
getSuccessRespEvents(batchedRequest, subscriptionMetadataArray, destination, true),
);
});
batchedResponseList.push(...batchedResponse);
});
const profiles = getProfiles(profileRespList, subscriptionMetadataArrayForAll, destination);

return [...profiles, ...batchedResponseList];
};
module.exports = { batchSubscriptionRequestV2, groupSubscribeResponsesUsingListIdV2 };
38 changes: 35 additions & 3 deletions src/v0/destinations/klaviyo/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ const MAX_BATCH_SIZE = 100;

const CONFIG_CATEGORIES = {
IDENTIFY: { name: 'KlaviyoIdentify', apiUrl: '/api/profiles' },
SCREEN: { name: 'KlaviyoTrack', apiUrl: '/api/events' },
IDENTIFYV2: { name: 'KlaviyoProfileV2', apiUrl: '/api/profile-import' },
TRACK: { name: 'KlaviyoTrack', apiUrl: '/api/events' },
TRACKV2: { name: 'KlaviyoTrackV2', apiUrl: '/api/events' },
GROUP: { name: 'KlaviyoGroup' },
PROFILE: { name: 'KlaviyoProfile' },
PROFILEV2: { name: 'KlaviyoProfileV2' },
STARTED_CHECKOUT: { name: 'StartedCheckout' },
VIEWED_PRODUCT: { name: 'ViewedProduct' },
ADDED_TO_CART: { name: 'AddedToCart' },
Expand Down Expand Up @@ -55,10 +57,37 @@ const LIST_CONF = {
SUBSCRIBE: 'subscribe_with_consentInfo',
ADD_TO_LIST: 'subscribe_without_consentInfo',
};

const useUpdatedKlaviyoAPI = process.env.USE_UPDATED_KLAVIYO_API === 'true' || false;
const MAPPING_CONFIG = getMappingConfig(CONFIG_CATEGORIES, __dirname);
const destType = 'klaviyo';

const WhiteListedTraitsV2 = [
'email',
'firstName',
'firstname',
'first_name',
'lastName',
'lastname',
'last_name',
'phone',
'title',
'organization',
'city',
'region',
'country',
'zip',
'image',
'timezone',
'anonymousId',
'userId',
'properties',
'location',
'_kx',
'street',
'address',
];
const destType = 'klaviyo';
// api version used
const revision = '2024-06-15';
module.exports = {
BASE_ENDPOINT,
MAX_BATCH_SIZE,
Expand All @@ -70,4 +99,7 @@ module.exports = {
eventNameMapping,
jsonNameMapping,
destType,
revision,
WhiteListedTraitsV2,
useUpdatedKlaviyoAPI,
};
128 changes: 128 additions & 0 deletions src/v0/destinations/klaviyo/data/KlaviyoProfileV2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
[
{
"destKey": "external_id",
"sourceKeys": "userIdOnly",
"sourceFromGenericMap": true
},
{
"destKey": "anonymous_id",
"sourceKeys": "anonymousId"
},
{
"destKey": "email",
"sourceKeys": "emailOnly",
"sourceFromGenericMap": true
},
{
"destKey": "first_name",
"sourceKeys": "firstName",
"sourceFromGenericMap": true
},
{
"destKey": "last_name",
"sourceKeys": "lastName",
"sourceFromGenericMap": true
},
{
"destKey": "phone_number",
"sourceKeys": "phone",
"sourceFromGenericMap": true
},
{
"destKey": "title",
"sourceKeys": ["traits.title", "context.traits.title", "properties.title"]
},
{
"destKey": "organization",
"sourceKeys": ["traits.organization", "context.traits.organization", "properties.organization"]
},
{
"destKey": "location.city",
"sourceKeys": [
"traits.city",
"traits.address.city",
"context.traits.city",
"context.traits.address.city",
"properties.city"
]
},
{
"destKey": "location.region",
"sourceKeys": [
"traits.region",
"traits.address.region",
"context.traits.region",
"context.traits.address.region",
"properties.region",
"traits.state",
"traits.address.state",
"context.traits.address.state",
"context.traits.state",
"properties.state"
]
},
{
"destKey": "location.country",
"sourceKeys": [
"traits.country",
"traits.address.country",
"context.traits.country",
"context.traits.address.country",
"properties.country"
]
},
{
"destKey": "location.zip",
"sourceKeys": [
"traits.zip",
"traits.postalcode",
"traits.postalCode",
"traits.address.zip",
"traits.address.postalcode",
"traits.address.postalCode",
"context.traits.zip",
"context.traits.postalcode",
"context.traits.postalCode",
"context.traits.address.zip",
"context.traits.address.postalcode",
"context.traits.address.postalCode",
"properties.zip",
"properties.postalcode",
"properties.postalCode"
]
},
{
"destKey": "location.ip",
"sourceKeys": ["context.ip", "request_ip"]
},
{
"destKey": "_kx",
"sourceKeys": ["traits._kx", "context.traits._kx"]
},
{
"destKey": "image",
"sourceKeys": ["traits.image", "context.traits.image", "properties.image"]
},
{
"destKey": "location.timezone",
"sourceKeys": ["traits.timezone", "context.traits.timezone", "properties.timezone"]
},
{
"destKey": "location.latitude",
"sourceKeys": ["latitude", "context.address.latitude", "context.location.latitude"]
},
{
"destKey": "location.longitude",
"sourceKeys": ["longitude", "context.address.longitude", "context.location.longitude"]
},
{
"destKey": "location.address1",
"sourceKeys": [
"traits.street",
"traits.address.street",
"context.traits.street",
"context.traits.address.street",
"properties.street"
]
}
]
Loading
Loading