Skip to content

Commit

Permalink
feat: update klaviyo api to 2024-06-15
Browse files Browse the repository at this point in the history
  • Loading branch information
anantjain45823 committed Jun 20, 2024
1 parent dc8eae2 commit 5388074
Show file tree
Hide file tree
Showing 7 changed files with 370 additions and 20 deletions.
3 changes: 3 additions & 0 deletions src/v0/destinations/klaviyo/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const MAX_BATCH_SIZE = 100;

const CONFIG_CATEGORIES = {
IDENTIFY: { name: 'KlaviyoIdentify', apiUrl: '/api/profiles' },
IDENTIFY_V2: { name: 'KlaviyoIdentifyV2', apiUrl: '/api/profiles' },
SCREEN: { name: 'KlaviyoTrack', apiUrl: '/api/events' },
TRACK: { name: 'KlaviyoTrack', apiUrl: '/api/events' },
GROUP: { name: 'KlaviyoGroup' },
Expand All @@ -18,6 +19,7 @@ const CONFIG_CATEGORIES = {
ADDED_TO_CART: { name: 'AddedToCart' },
ITEMS: { name: 'Items' },
};
const { useUpdatedKlaviyoAPI } = process.env;
const ecomExclusionKeys = [
'name',
'product_id',
Expand Down Expand Up @@ -70,4 +72,5 @@ module.exports = {
eventNameMapping,
jsonNameMapping,
destType,
useUpdatedKlaviyoAPI,
};
142 changes: 142 additions & 0 deletions src/v0/destinations/klaviyo/data/KlaviyoIdentifyV2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
[
{
"destKey": "external_id",
"sourceKeys": "userId",
"required": false,
"sourceFromGenericMap": true
},
{
"destKey": "anonymous_id",
"sourceKeys": "anonymousId",
"required": false
},
{
"destKey": "email",
"sourceKeys": "emailOnly",
"required": false,
"sourceFromGenericMap": true
},
{
"destKey": "first_name",
"sourceKeys": "firstName",
"required": false,
"sourceFromGenericMap": true
},
{
"destKey": "last_name",
"sourceKeys": "lastName",
"required": false,
"sourceFromGenericMap": true
},
{
"destKey": "phone_number",
"sourceKeys": "phone",
"required": false,
"sourceFromGenericMap": true
},
{
"destKey": "title",
"sourceKeys": ["traits.title", "context.traits.title", "properties.title"],
"required": false
},
{
"destKey": "organization",
"sourceKeys": ["traits.organization", "context.traits.organization", "properties.organization"],
"required": false
},
{
"destKey": "location.city",
"sourceKeys": [
"traits.city",
"traits.address.city",
"context.traits.city",
"context.traits.address.city",
"properties.city"
],
"required": false
},
{
"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"
],
"required": false
},
{
"destKey": "location.country",
"sourceKeys": [
"traits.country",
"traits.address.country",
"context.traits.country",
"context.traits.address.country",
"properties.country"
],
"required": false
},
{
"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"
],
"required": false
},
{
"destKey": "ip",
"sourceKeys": ["context.ip", "request_ip"],
"required": false
},
{
"destKey": "_kx",
"sourceKeys": ["traits._kx", "context.traits._kx"],
"required": false
},
{
"destKey": "location.timezone",
"sourceKeys": ["traits.timezone", "context.traits.timezone", "properties.timezone"],
"required": false
},
{
"destKey": "latitude",
"sourceKeys": ["latitude", "context.address.latitude", "context.location.latitude"],
"required": false
},
{
"destKey": "longitude",
"sourceKeys": ["longitude", "context.address.longitude", "context.location.longitude"],
"required": false
},
{
"destKey": "location.address1",
"sourceKeys": [
"traits.street",
"traits.address.street",
"context.traits.street",
"context.traits.address.street",
"properties.street"
],
"required": false
}
]
97 changes: 95 additions & 2 deletions src/v0/destinations/klaviyo/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/* eslint-disable no-underscore-dangle */
/* eslint-disable array-callback-return */
const get = require('get-value');
const set = require('set-value');
const { ConfigurationError, InstrumentationError } = require('@rudderstack/integrations-lib');
const { EventType, WhiteListedTraits, MappedToDestinationKey } = require('../../../constants');
const {
Expand All @@ -12,6 +13,7 @@ const {
ecomEvents,
eventNameMapping,
jsonNameMapping,
useUpdatedKlaviyoAPI,
} = require('./config');
const {
createCustomerProperties,
Expand All @@ -20,6 +22,7 @@ const {
batchSubscribeEvents,
getIdFromNewOrExistingProfile,
profileUpdateResponseBuilder,
createCustomerPropertiesV2,
} = require('./util');
const {
defaultRequestConfig,
Expand All @@ -35,9 +38,89 @@ const {
handleRtTfSingleEventError,
flattenJson,
isNewStatusCodesAccepted,
getDestinationExternalID,
isDefinedAndNotNullAndNotEmpty,
} = require('../../util');
const { JSON_MIME_TYPE, HTTP_STATUS_CODES } = require('../../util/constant');

/**
* Identify function to handle request for Klaviyo version '2024-06-15'
* The function is used to create/update new (same endpoint and no intermediate API calls) users and also for adding/subscribing
* users to the list.
* DOCS: 1. https://developers.klaviyo.com/en/reference/create_or_update_profile
* 2. https://developers.klaviyo.com/en/v2023-02-22/reference/subscribe_profiles
* @param {*} message
* @param {*} category
* @param {*} destination
* @param {*} reqMetadata
* @returns
*/
const identifyRequestHandlerV2 = (message, category, destination) => {
// If listId property is present try to subscribe/member user in list
const { privateApiKey, listId, flattenProperties } = destination.Config;
const mappedToDestination = get(message, MappedToDestinationKey);
if (mappedToDestination) {
addExternalIdToTraits(message);
adduserIdFromExternalId(message);

Check warning on line 64 in src/v0/destinations/klaviyo/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/klaviyo/transform.js#L63-L64

Added lines #L63 - L64 were not covered by tests
}
const traitsInfo = getFieldValueFromMessage(message, 'traits');
let propertyPayload = constructPayload(message, MAPPING_CONFIG[category.name]);
// Extract other K-V property from traits about user custom properties
let customPropertyPayload = {};
customPropertyPayload = extractCustomFields(
message,
customPropertyPayload,
['traits', 'context.traits'],
[...WhiteListedTraits, '_kx'],
);

propertyPayload = removeUndefinedAndNullValues(propertyPayload);
const data = {
type: 'profile',
attributes: {
...propertyPayload,
properties: removeUndefinedAndNullValues(customPropertyPayload),
},
};
// if flattenProperties is enabled from UI, flatten the user properties
data.attributes.properties = flattenProperties
? flattenJson(data.attributes.properties, '.', 'normal', false)
: data.attributes.properties;

Check warning on line 88 in src/v0/destinations/klaviyo/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/klaviyo/transform.js#L88

Added line #L88 was not covered by tests
// external id -> Klaviyo generated id for every profile
const externalId = getDestinationExternalID(message, 'klaviyoExternalId');
if (isDefinedAndNotNullAndNotEmpty(externalId)) {
set(data, 'id', externalId);

Check warning on line 92 in src/v0/destinations/klaviyo/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/klaviyo/transform.js#L92

Added line #L92 was not covered by tests
}
if (isEmptyObject(data.attributes.properties)) {
delete data.attributes.properties;

Check warning on line 95 in src/v0/destinations/klaviyo/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/klaviyo/transform.js#L95

Added line #L95 was not covered by tests
}
const payload = {
data: removeUndefinedAndNullValues(data),
};
const endpoint = `${BASE_ENDPOINT}${category.apiUrl}`;
const requestOptions = {
headers: {
Authorization: `Klaviyo-API-Key ${privateApiKey}`,
Accept: JSON_MIME_TYPE,
'Content-Type': JSON_MIME_TYPE,
revision: '2024-06-15',
},
};

const profileRequest = defaultRequestConfig();
profileRequest.endpoint = endpoint;
profileRequest.body.JSON = payload;
profileRequest.headers = requestOptions.headers;

let subscriptionRequest;
// check if user wants to subscribe profile or not and listId is present or not
if (traitsInfo?.properties?.subscribe && (traitsInfo.properties?.listId || listId)) {
subscriptionRequest = subscribeUserToList(message, traitsInfo, destination);
return [profileRequest, subscriptionRequest];
}

return profileRequest;

Check warning on line 122 in src/v0/destinations/klaviyo/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/klaviyo/transform.js#L122

Added line #L122 was not covered by tests
};
/**
* Main Identify request handler func
* The function is used to create/update new users and also for adding/subscribing
Expand Down Expand Up @@ -157,7 +240,7 @@ const identifyRequestHandler = async (

const trackRequestHandler = (message, category, destination) => {
const payload = {};
const { privateApiKey, flattenProperties } = destination.Config;
const { privateApiKey, flattenProperties, useUpdatedKlaviyo } = destination.Config;
let event = get(message, 'event');
if (event && typeof event !== 'string') {
throw new InstrumentationError('Event type should be a string');
Expand Down Expand Up @@ -225,8 +308,12 @@ const trackRequestHandler = (message, category, destination) => {
? flattenJson(attributes.properties, '.', 'normal', false)
: attributes.properties;
// Map user properties to profile object
const customerProp =
useUpdatedKlaviyoAPI || useUpdatedKlaviyo
? createCustomerProperties(message, destination.Config)

Check warning on line 313 in src/v0/destinations/klaviyo/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/klaviyo/transform.js#L313

Added line #L313 was not covered by tests
: createCustomerPropertiesV2(message, destination.Config);
attributes.profile = {
...createCustomerProperties(message, destination.Config),
...customerProp,
...populateCustomFieldsFromTraits(message),
};

Expand Down Expand Up @@ -289,6 +376,12 @@ const processEvent = async (event, reqMetadata) => {
let response;
switch (messageType) {
case EventType.IDENTIFY:
// checking if we want to use the updated klaviyo api for identify
if (useUpdatedKlaviyoAPI || destination.Config?.useUpdatedKlaviyo) {
category = CONFIG_CATEGORIES.IDENTIFY_V2;
response = identifyRequestHandlerV2(message, category, destination);
break;
}
category = CONFIG_CATEGORIES.IDENTIFY;
response = await identifyRequestHandler(
{ message, category, destination, metadata },
Expand Down
22 changes: 22 additions & 0 deletions src/v0/destinations/klaviyo/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,27 @@ const createCustomerProperties = (message, Config) => {
customerProperties = removeUndefinedAndNullValues(customerProperties);
return customerProperties;
};
// This function is used for creating and returning customer properties using mapping json
const createCustomerPropertiesV2 = (message, Config) => {
const { enforceEmailAsPrimary } = Config;
let customerProperties = constructPayload(
message,
MAPPING_CONFIG[CONFIG_CATEGORIES.PROFILE.name],
);
if (!enforceEmailAsPrimary) {
customerProperties.$id = getFieldValueFromMessage(message, 'userIdOnly');
} else {
if (!customerProperties.$email && !customerProperties.$phone_number) {
throw new InstrumentationError('None of email and phone are present in the payload');
}
customerProperties = {
...customerProperties,
_id: getFieldValueFromMessage(message, 'userIdOnly'),
};
}
customerProperties = removeUndefinedAndNullValues(customerProperties);
return customerProperties;
};

const populateCustomFieldsFromTraits = (message) => {
// Extract other K-V property from traits about user custom properties
Expand Down Expand Up @@ -327,6 +348,7 @@ const batchSubscribeEvents = (subscribeRespList) => {
module.exports = {
subscribeUserToList,
createCustomerProperties,
createCustomerPropertiesV2,
populateCustomFieldsFromTraits,
generateBatchedPaylaodForArray,
batchSubscribeEvents,
Expand Down
18 changes: 0 additions & 18 deletions test/integrations/destinations/klaviyo/network.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,4 @@
export const networkCallsData = [
{
httpReq: {
url: 'https://a.klaviyo.com/api/v2/list/XUepkK/subscribe',
method: 'GET',
},
httpRes: {
status: 200,
},
},
{
httpReq: {
url: 'https://a.klaviyo.com/api/v2/list/XUepkK/members',
method: 'GET',
},
httpRes: {
status: 200,
},
},
{
httpReq: {
url: 'https://a.klaviyo.com/api/profiles',
Expand Down
Loading

0 comments on commit 5388074

Please sign in to comment.