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: update klaviyo api to 2024-06-15 #3489

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
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/profile-import' },
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.USE_UPDATED_KLAVIYO_API === 'true' || false;
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": "userIdOnly",
"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": "location.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
}
]
106 changes: 104 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 @@
ecomEvents,
eventNameMapping,
jsonNameMapping,
useUpdatedKlaviyoAPI,
} = require('./config');
const {
createCustomerProperties,
Expand All @@ -20,6 +22,7 @@
batchSubscribeEvents,
getIdFromNewOrExistingProfile,
profileUpdateResponseBuilder,
createCustomerPropertiesV2,
} = require('./util');
const {
defaultRequestConfig,
Expand All @@ -35,9 +38,98 @@
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, enforceEmailAsPrimary } = 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);
if (enforceEmailAsPrimary) {
if (!propertyPayload.email && !propertyPayload.phone_number) {
throw new InstrumentationError('None of email and phone are present in the payload');

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L79 was not covered by tests
}
delete propertyPayload.external_id;
customPropertyPayload = {
...customPropertyPayload,
_id: getFieldValueFromMessage(message, 'userIdOnly'),
};
}
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;
// external id -> Klaviyo generated id for every profile
const externalId = getDestinationExternalID(message, 'klaviyoExternalId');
if (isDefinedAndNotNullAndNotEmpty(externalId)) {
set(data, 'id', externalId);

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

View check run for this annotation

Codecov / codecov/patch

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

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L104 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 131 in src/v0/destinations/klaviyo/transform.js

View check run for this annotation

Codecov / codecov/patch

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

Added line #L131 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 +249,7 @@

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 +317,12 @@
? flattenJson(attributes.properties, '.', 'normal', false)
: attributes.properties;
// Map user properties to profile object
const customerProp =
useUpdatedKlaviyoAPI || useUpdatedKlaviyo
? createCustomerPropertiesV2(message, destination.Config)

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

View check run for this annotation

Codecov / codecov/patch

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

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

Expand Down Expand Up @@ -289,6 +385,12 @@
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 @@ -190,6 +190,27 @@
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(

Check warning on line 196 in src/v0/destinations/klaviyo/util.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/klaviyo/util.js#L195-L196

Added lines #L195 - L196 were not covered by tests
message,
MAPPING_CONFIG[CONFIG_CATEGORIES.PROFILE.name],
);
if (!enforceEmailAsPrimary) {
customerProperties.$id = getFieldValueFromMessage(message, 'userIdOnly');
} else {

Check warning on line 202 in src/v0/destinations/klaviyo/util.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/klaviyo/util.js#L201-L202

Added lines #L201 - L202 were not covered by tests
if (!customerProperties.$email && !customerProperties.$phone_number) {
throw new InstrumentationError('None of email and phone are present in the payload');

Check warning on line 204 in src/v0/destinations/klaviyo/util.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/klaviyo/util.js#L204

Added line #L204 was not covered by tests
}
customerProperties = {

Check warning on line 206 in src/v0/destinations/klaviyo/util.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/klaviyo/util.js#L206

Added line #L206 was not covered by tests
...customerProperties,
_id: getFieldValueFromMessage(message, 'userIdOnly'),
};
}
customerProperties = removeUndefinedAndNullValues(customerProperties);
return customerProperties;

Check warning on line 212 in src/v0/destinations/klaviyo/util.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/klaviyo/util.js#L211-L212

Added lines #L211 - L212 were not covered by tests
};

const populateCustomFieldsFromTraits = (message) => {
// Extract other K-V property from traits about user custom properties
Expand Down Expand Up @@ -309,6 +330,7 @@
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
Loading