From 23c8889e69b221e42c0fea2e52eced75698ee29a Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Thu, 10 Oct 2024 17:06:25 +0530 Subject: [PATCH 01/13] Added trackEvent Batch Support in Klaviyo --- .../__snapshots__/snapshot.test.ts.snap | 1 + .../klaviyo/__tests__/multistatus.test.ts | 244 +++++++++++++++++ .../src/destinations/klaviyo/config.ts | 251 +++++++++++++++++- .../src/destinations/klaviyo/functions.ts | 224 +++++++++++++++- .../src/destinations/klaviyo/properties.ts | 17 ++ .../__snapshots__/snapshot.test.ts.snap | 1 + .../destinations/klaviyo/trackEvent/index.ts | 19 +- .../src/destinations/klaviyo/types.ts | 11 + 8 files changed, 758 insertions(+), 10 deletions(-) create mode 100644 packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts diff --git a/packages/destination-actions/src/destinations/klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap index fb456e1b0c..ca12444d29 100644 --- a/packages/destination-actions/src/destinations/klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap @@ -130,6 +130,7 @@ Object { "data": Object { "attributes": Object { "anonymous_id": "mTdOx(Nl)", + "country_code": "GA", "email": "ujoeri@ifosi.kp", "external_id": "mTdOx(Nl)", "phone_number": "+5694788449", diff --git a/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts b/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts new file mode 100644 index 0000000000..84470e0429 --- /dev/null +++ b/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts @@ -0,0 +1,244 @@ +import { SegmentEvent, createTestEvent, createTestIntegration } from '@segment/actions-core' +import nock from 'nock' +import { API_URL } from '../config' +import Braze from '../index' + +beforeEach(() => nock.cleanAll()) + +const testDestination = createTestIntegration(Braze) + +const settings = { + api_key: 'my-api-key' +} + +const timestamp = '2024-07-22T20:08:49.7931Z' + +describe('MultiStatus', () => { + describe('trackEvent', () => { + const mapping = { + profile: { + '@path': '$.properties' + }, + metric_name: { + '@path': '$.event' + }, + properties: { + '@path': '$.properties' + }, + time: { + '@path': '$.timestamp' + }, + unique_id: { + '@path': '$.messageId' + } + } + + it("should successfully handle those payload where phone_number is invalid and couldn't be converted to E164 format", async () => { + nock(API_URL).post('/event-bulk-create-jobs/').reply(202, {}) + + const events: SegmentEvent[] = [ + // Event with invalid phone_number + createTestEvent({ + type: 'track', + timestamp, + properties: { + country_code: 'IN', + phone_number: '701271', + email: 'valid@gmail.com' + } + }), + // Valid Event + createTestEvent({ + type: 'track', + timestamp, + properties: { + email: 'valid@gmail.com' + } + }) + ] + + const response = await testDestination.executeBatch('trackEvent', { + events, + settings, + mapping + }) + + // The First event fails as pre-request validation fails for having invalid phone_number and could not be converted to E164 format + expect(response[0]).toMatchObject({ + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'Phone number could not be converted to E.164 format.', + errorreporter: 'DESTINATION' + }) + + // The Second event doesn't fail as there is no error reported by Klaviyo API + expect(response[1]).toMatchObject({ + status: 200, + body: 'success' + }) + }) + + it('should successfully handle a batch of events with complete success response from Klaviyo API', async () => { + nock(API_URL).post('/event-bulk-create-jobs/').reply(202, {}) + + const events: SegmentEvent[] = [ + // Valid Event + createTestEvent({ + type: 'track', + timestamp, + properties: { + email: 'valid@gmail.com' + } + }), + // Event without any user identifier + createTestEvent({ + type: 'track', + timestamp + }) + ] + + const response = await testDestination.executeBatch('trackEvent', { + events, + settings, + mapping + }) + + // The first event doesn't fail as there is no error reported by Klaviyo API + expect(response[0]).toMatchObject({ + status: 200, + body: 'success' + }) + + // The second event fails as pre-request validation fails for not having any user identifier + expect(response[1]).toMatchObject({ + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'One of External ID, Anonymous ID, Phone Number or Email is required.', + errorreporter: 'DESTINATION' + }) + }) + + it('should successfully handle a batch of events with failure response from Klaviyo API', async () => { + // Mocking a 400 response from Klaviyo API + const mockResponse = { + errors: [ + { + id: '752f7ece-af20-44e0-aa3a-b13290d98e72', + status: 400, + code: 'invalid', + title: 'Invalid input.', + detail: 'Invalid email address', + source: { + pointer: '/data/attributes/events-bulk-create/data/0/attributes/email' + }, + links: {}, + meta: {} + } + ] + } + nock(API_URL).post('/event-bulk-create-jobs/').reply(400, mockResponse) + + const events: SegmentEvent[] = [ + // Invalid Event + createTestEvent({ + type: 'track', + timestamp, + properties: { + email: 'invalid_email' + } + }), + // Valid Event + createTestEvent({ + type: 'track', + timestamp, + properties: { + external_id: 'Xi1234' + } + }) + ] + + const response = await testDestination.executeBatch('trackEvent', { + events, + settings, + mapping + }) + + // The first doesn't fail as there is no error reported by Braze API + expect(response[0]).toMatchObject({ + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'Invalid email address', + sent: { + profile: { + email: 'invalid_email' + }, + metric_name: 'Test Event', + properties: { + email: 'invalid_email' + }, + time: timestamp + }, + body: '{"id":"752f7ece-af20-44e0-aa3a-b13290d98e72","status":400,"code":"invalid","title":"Invalid input.","detail":"Invalid email address","source":{"pointer":"/data/attributes/events-bulk-create/data/0/attributes/email"},"links":{},"meta":{}}' + }) + + // The second event fails as Braze API reports an error + expect(response[1]).toMatchObject({ + status: 429, + sent: { + profile: { + external_id: 'Xi1234' + }, + metric_name: 'Test Event', + properties: { + external_id: 'Xi1234' + }, + time: timestamp + }, + body: 'Retry' + }) + }) + + it('should successfully handle a batch when all payload is invalid', async () => { + const events: SegmentEvent[] = [ + // Event with invalid phone_number + createTestEvent({ + type: 'track', + timestamp, + properties: { + country_code: 'IN', + phone_number: '701271', + email: 'valid@gmail.com' + } + }), + // Event without any user identifier + createTestEvent({ + type: 'track', + timestamp, + properties: {} + }) + ] + + const response = await testDestination.executeBatch('trackEvent', { + events, + settings, + mapping + }) + + // The First event fails as pre-request validation fails for having invalid phone_number and could not be converted to E164 format + expect(response[0]).toMatchObject({ + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'Phone number could not be converted to E.164 format.', + errorreporter: 'DESTINATION' + }) + + // The second event fails as pre-request validation fails for not having any user identifier + expect(response[1]).toMatchObject({ + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'One of External ID, Anonymous ID, Phone Number or Email is required.', + errorreporter: 'DESTINATION' + }) + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/klaviyo/config.ts b/packages/destination-actions/src/destinations/klaviyo/config.ts index 37fb5a6316..1999022b0c 100644 --- a/packages/destination-actions/src/destinations/klaviyo/config.ts +++ b/packages/destination-actions/src/destinations/klaviyo/config.ts @@ -1,2 +1,251 @@ export const API_URL = 'https://a.klaviyo.com/api' -export const REVISION_DATE = '2023-09-15' +export const REVISION_DATE = '2024-05-15' +export const COUNTRY_CODES = [ + { label: 'AD - Andorra', value: 'AD' }, + { label: 'AE - United Arab Emirates', value: 'AE' }, + { label: 'AF - Afghanistan', value: 'AF' }, + { label: 'AG - Antigua and Barbuda', value: 'AG' }, + { label: 'AI - Anguilla', value: 'AI' }, + { label: 'AL - Albania', value: 'AL' }, + { label: 'AM - Armenia', value: 'AM' }, + { label: 'AO - Angola', value: 'AO' }, + { label: 'AQ - Antarctica', value: 'AQ' }, + { label: 'AR - Argentina', value: 'AR' }, + { label: 'AS - American Samoa', value: 'AS' }, + { label: 'AT - Austria', value: 'AT' }, + { label: 'AU - Australia', value: 'AU' }, + { label: 'AW - Aruba', value: 'AW' }, + { label: 'AX - Åland Islands', value: 'AX' }, + { label: 'AZ - Azerbaijan', value: 'AZ' }, + { label: 'BA - Bosnia and Herzegovina', value: 'BA' }, + { label: 'BB - Barbados', value: 'BB' }, + { label: 'BD - Bangladesh', value: 'BD' }, + { label: 'BE - Belgium', value: 'BE' }, + { label: 'BF - Burkina Faso', value: 'BF' }, + { label: 'BG - Bulgaria', value: 'BG' }, + { label: 'BH - Bahrain', value: 'BH' }, + { label: 'BI - Burundi', value: 'BI' }, + { label: 'BJ - Benin', value: 'BJ' }, + { label: 'BL - Saint Barthélemy', value: 'BL' }, + { label: 'BM - Bermuda', value: 'BM' }, + { label: 'BN - Brunei Darussalam', value: 'BN' }, + { label: 'BO - Bolivia (Plurinational State of)', value: 'BO' }, + { label: 'BQ - Bonaire, Sint Eustatius and Saba', value: 'BQ' }, + { label: 'BR - Brazil', value: 'BR' }, + { label: 'BS - Bahamas', value: 'BS' }, + { label: 'BT - Bhutan', value: 'BT' }, + { label: 'BV - Bouvet Island', value: 'BV' }, + { label: 'BW - Botswana', value: 'BW' }, + { label: 'BY - Belarus', value: 'BY' }, + { label: 'BZ - Belize', value: 'BZ' }, + { label: 'CA - Canada', value: 'CA' }, + { label: 'CC - Cocos (Keeling) Islands', value: 'CC' }, + { label: 'CD - Congo, Democratic Republic of the', value: 'CD' }, + { label: 'CF - Central African Republic', value: 'CF' }, + { label: 'CG - Congo', value: 'CG' }, + { label: 'CH - Switzerland', value: 'CH' }, + { label: "CI - Côte d'Ivoire", value: 'CI' }, + { label: 'CK - Cook Islands', value: 'CK' }, + { label: 'CL - Chile', value: 'CL' }, + { label: 'CM - Cameroon', value: 'CM' }, + { label: 'CN - China', value: 'CN' }, + { label: 'CO - Colombia', value: 'CO' }, + { label: 'CR - Costa Rica', value: 'CR' }, + { label: 'CU - Cuba', value: 'CU' }, + { label: 'CV - Cabo Verde', value: 'CV' }, + { label: 'CW - Curaçao', value: 'CW' }, + { label: 'CX - Christmas Island', value: 'CX' }, + { label: 'CY - Cyprus', value: 'CY' }, + { label: 'CZ - Czechia', value: 'CZ' }, + { label: 'DE - Germany', value: 'DE' }, + { label: 'DJ - Djibouti', value: 'DJ' }, + { label: 'DK - Denmark', value: 'DK' }, + { label: 'DM - Dominica', value: 'DM' }, + { label: 'DO - Dominican Republic', value: 'DO' }, + { label: 'DZ - Algeria', value: 'DZ' }, + { label: 'EC - Ecuador', value: 'EC' }, + { label: 'EE - Estonia', value: 'EE' }, + { label: 'EG - Egypt', value: 'EG' }, + { label: 'EH - Western Sahara', value: 'EH' }, + { label: 'ER - Eritrea', value: 'ER' }, + { label: 'ES - Spain', value: 'ES' }, + { label: 'ET - Ethiopia', value: 'ET' }, + { label: 'FI - Finland', value: 'FI' }, + { label: 'FJ - Fiji', value: 'FJ' }, + { label: 'FK - Falkland Islands (Malvinas)', value: 'FK' }, + { label: 'FM - Micronesia (Federated States of)', value: 'FM' }, + { label: 'FO - Faroe Islands', value: 'FO' }, + { label: 'FR - France', value: 'FR' }, + { label: 'GA - Gabon', value: 'GA' }, + { label: 'GB - United Kingdom of Great Britain and Northern Ireland', value: 'GB' }, + { label: 'GD - Grenada', value: 'GD' }, + { label: 'GE - Georgia', value: 'GE' }, + { label: 'GF - French Guiana', value: 'GF' }, + { label: 'GG - Guernsey', value: 'GG' }, + { label: 'GH - Ghana', value: 'GH' }, + { label: 'GI - Gibraltar', value: 'GI' }, + { label: 'GL - Greenland', value: 'GL' }, + { label: 'GM - Gambia', value: 'GM' }, + { label: 'GN - Guinea', value: 'GN' }, + { label: 'GP - Guadeloupe', value: 'GP' }, + { label: 'GQ - Equatorial Guinea', value: 'GQ' }, + { label: 'GR - Greece', value: 'GR' }, + { label: 'GT - Guatemala', value: 'GT' }, + { label: 'GU - Guam', value: 'GU' }, + { label: 'GW - Guinea-Bissau', value: 'GW' }, + { label: 'GY - Guyana', value: 'GY' }, + { label: 'HK - Hong Kong', value: 'HK' }, + { label: 'HM - Heard Island and McDonald Islands', value: 'HM' }, + { label: 'HN - Honduras', value: 'HN' }, + { label: 'HR - Croatia', value: 'HR' }, + { label: 'HT - Haiti', value: 'HT' }, + { label: 'HU - Hungary', value: 'HU' }, + { label: 'ID - Indonesia', value: 'ID' }, + { label: 'IE - Ireland', value: 'IE' }, + { label: 'IL - Israel', value: 'IL' }, + { label: 'IM - Isle of Man', value: 'IM' }, + { label: 'IN - India', value: 'IN' }, + { label: 'IO - British Indian Ocean Territory', value: 'IO' }, + { label: 'IQ - Iraq', value: 'IQ' }, + { label: 'IR - Iran (Islamic Republic of)', value: 'IR' }, + { label: 'IS - Iceland', value: 'IS' }, + { label: 'IT - Italy', value: 'IT' }, + { label: 'JE - Jersey', value: 'JE' }, + { label: 'JM - Jamaica', value: 'JM' }, + { label: 'JO - Jordan', value: 'JO' }, + { label: 'JP - Japan', value: 'JP' }, + { label: 'KE - Kenya', value: 'KE' }, + { label: 'KG - Kyrgyzstan', value: 'KG' }, + { label: 'KH - Cambodia', value: 'KH' }, + { label: 'KI - Kiribati', value: 'KI' }, + { label: 'KM - Comoros', value: 'KM' }, + { label: 'KN - Saint Kitts and Nevis', value: 'KN' }, + { label: "KP - Korea (Democratic People's Republic of)", value: 'KP' }, + { label: 'KR - Korea, Republic of', value: 'KR' }, + { label: 'KW - Kuwait', value: 'KW' }, + { label: 'KY - Cayman Islands', value: 'KY' }, + { label: 'KZ - Kazakhstan', value: 'KZ' }, + { label: "LA - Lao People's Democratic Republic", value: 'LA' }, + { label: 'LB - Lebanon', value: 'LB' }, + { label: 'LC - Saint Lucia', value: 'LC' }, + { label: 'LI - Liechtenstein', value: 'LI' }, + { label: 'LK - Sri Lanka', value: 'LK' }, + { label: 'LR - Liberia', value: 'LR' }, + { label: 'LS - Lesotho', value: 'LS' }, + { label: 'LT - Lithuania', value: 'LT' }, + { label: 'LU - Luxembourg', value: 'LU' }, + { label: 'LV - Latvia', value: 'LV' }, + { label: 'LY - Libya', value: 'LY' }, + { label: 'MA - Morocco', value: 'MA' }, + { label: 'MC - Monaco', value: 'MC' }, + { label: 'MD - Moldova (Republic of)', value: 'MD' }, + { label: 'ME - Montenegro', value: 'ME' }, + { label: 'MF - Saint Martin (French part)', value: 'MF' }, + { label: 'MG - Madagascar', value: 'MG' }, + { label: 'MH - Marshall Islands', value: 'MH' }, + { label: 'MK - North Macedonia', value: 'MK' }, + { label: 'ML - Mali', value: 'ML' }, + { label: 'MM - Myanmar', value: 'MM' }, + { label: 'MN - Mongolia', value: 'MN' }, + { label: 'MO - Macao', value: 'MO' }, + { label: 'MP - Northern Mariana Islands', value: 'MP' }, + { label: 'MQ - Martinique', value: 'MQ' }, + { label: 'MR - Mauritania', value: 'MR' }, + { label: 'MS - Montserrat', value: 'MS' }, + { label: 'MT - Malta', value: 'MT' }, + { label: 'MU - Mauritius', value: 'MU' }, + { label: 'MV - Maldives', value: 'MV' }, + { label: 'MW - Malawi', value: 'MW' }, + { label: 'MX - Mexico', value: 'MX' }, + { label: 'MY - Malaysia', value: 'MY' }, + { label: 'MZ - Mozambique', value: 'MZ' }, + { label: 'NA - Namibia', value: 'NA' }, + { label: 'NC - New Caledonia', value: 'NC' }, + { label: 'NE - Niger', value: 'NE' }, + { label: 'NF - Norfolk Island', value: 'NF' }, + { label: 'NG - Nigeria', value: 'NG' }, + { label: 'NI - Nicaragua', value: 'NI' }, + { label: 'NL - Netherlands', value: 'NL' }, + { label: 'NO - Norway', value: 'NO' }, + { label: 'NP - Nepal', value: 'NP' }, + { label: 'NR - Nauru', value: 'NR' }, + { label: 'NU - Niue', value: 'NU' }, + { label: 'NZ - New Zealand', value: 'NZ' }, + { label: 'OM - Oman', value: 'OM' }, + { label: 'PA - Panama', value: 'PA' }, + { label: 'PE - Peru', value: 'PE' }, + { label: 'PF - French Polynesia', value: 'PF' }, + { label: 'PG - Papua New Guinea', value: 'PG' }, + { label: 'PH - Philippines', value: 'PH' }, + { label: 'PK - Pakistan', value: 'PK' }, + { label: 'PL - Poland', value: 'PL' }, + { label: 'PM - Saint Pierre and Miquelon', value: 'PM' }, + { label: 'PN - Pitcairn', value: 'PN' }, + { label: 'PR - Puerto Rico', value: 'PR' }, + { label: 'PT - Portugal', value: 'PT' }, + { label: 'PW - Palau', value: 'PW' }, + { label: 'PY - Paraguay', value: 'PY' }, + { label: 'QA - Qatar', value: 'QA' }, + { label: 'RE - Réunion', value: 'RE' }, + { label: 'RO - Romania', value: 'RO' }, + { label: 'RS - Serbia', value: 'RS' }, + { label: 'RU - Russian Federation', value: 'RU' }, + { label: 'RW - Rwanda', value: 'RW' }, + { label: 'SA - Saudi Arabia', value: 'SA' }, + { label: 'SB - Solomon Islands', value: 'SB' }, + { label: 'SC - Seychelles', value: 'SC' }, + { label: 'SD - Sudan', value: 'SD' }, + { label: 'SE - Sweden', value: 'SE' }, + { label: 'SG - Singapore', value: 'SG' }, + { label: 'SH - Saint Helena', value: 'SH' }, + { label: 'SI - Slovenia', value: 'SI' }, + { label: 'SJ - Svalbard and Jan Mayen', value: 'SJ' }, + { label: 'SK - Slovakia', value: 'SK' }, + { label: 'SL - Sierra Leone', value: 'SL' }, + { label: 'SM - San Marino', value: 'SM' }, + { label: 'SN - Senegal', value: 'SN' }, + { label: 'SO - Somalia', value: 'SO' }, + { label: 'SR - Suriname', value: 'SR' }, + { label: 'SS - South Sudan', value: 'SS' }, + { label: 'ST - São Tomé and Príncipe', value: 'ST' }, + { label: 'SV - El Salvador', value: 'SV' }, + { label: 'SX - Sint Maarten (Dutch part)', value: 'SX' }, + { label: 'SY - Syrian Arab Republic', value: 'SY' }, + { label: 'SZ - Eswatini', value: 'SZ' }, + { label: 'TC - Turks and Caicos Islands', value: 'TC' }, + { label: 'TD - Chad', value: 'TD' }, + { label: 'TF - French Southern Territories', value: 'TF' }, + { label: 'TG - Togo', value: 'TG' }, + { label: 'TH - Thailand', value: 'TH' }, + { label: 'TJ - Tajikistan', value: 'TJ' }, + { label: 'TK - Tokelau', value: 'TK' }, + { label: 'TL - Timor-Leste', value: 'TL' }, + { label: 'TM - Turkmenistan', value: 'TM' }, + { label: 'TN - Tunisia', value: 'TN' }, + { label: 'TO - Tonga', value: 'TO' }, + { label: 'TR - Turkey', value: 'TR' }, + { label: 'TT - Trinidad and Tobago', value: 'TT' }, + { label: 'TV - Tuvalu', value: 'TV' }, + { label: 'TZ - Tanzania, United Republic of', value: 'TZ' }, + { label: 'UA - Ukraine', value: 'UA' }, + { label: 'UG - Uganda', value: 'UG' }, + { label: 'UM - United States Minor Outlying Islands', value: 'UM' }, + { label: 'UN - United Nations', value: 'UN' }, + { label: 'US - United States of America', value: 'US' }, + { label: 'UY - Uruguay', value: 'UY' }, + { label: 'UZ - Uzbekistan', value: 'UZ' }, + { label: 'VA - Holy See', value: 'VA' }, + { label: 'VC - Saint Vincent and the Grenadines', value: 'VC' }, + { label: 'VE - Venezuela (Bolivarian Republic of)', value: 'VE' }, + { label: 'VG - Virgin Islands (British)', value: 'VG' }, + { label: 'VI - Virgin Islands (U.S.)', value: 'VI' }, + { label: 'VN - Viet Nam', value: 'VN' }, + { label: 'VU - Vanuatu', value: 'VU' }, + { label: 'WF - Wallis and Futuna', value: 'WF' }, + { label: 'WS - Samoa', value: 'WS' }, + { label: 'YE - Yemen', value: 'YE' }, + { label: 'YT - Mayotte', value: 'YT' }, + { label: 'ZA - South Africa', value: 'ZA' }, + { label: 'ZM - Zambia', value: 'ZM' }, + { label: 'ZW - Zimbabwe', value: 'ZW' } +] diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index 965f3a1fad..fdf4122b47 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -3,8 +3,10 @@ import { RequestClient, DynamicFieldResponse, IntegrationError, - PayloadValidationError + PayloadValidationError, + MultiStatusResponse } from '@segment/actions-core' +import { JSONLikeObject } from '@segment/actions-core' import { API_URL, REVISION_DATE } from './config' import { Settings } from './generated-types' import { @@ -20,9 +22,15 @@ import { UnsubscribeProfile, UnsubscribeEventData, GroupedProfiles, - AdditionalAttributes + AdditionalAttributes, + KlaviyoAPIErrorResponse } from './types' import { Payload } from './upsertProfile/generated-types' +import { Payload as TrackEventPayload } from './trackEvent/generated-types' +import dayjs from 'dayjs' +import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber' +import { ActionDestinationErrorResponseType } from '@segment/actions-core/destination-kittypes' +const phoneUtil = PhoneNumberUtil.getInstance() export async function getListIdDynamicData(request: RequestClient): Promise { try { @@ -408,3 +416,215 @@ export function validatePhoneNumber(phone?: string): boolean { const e164Regex = /^\+[1-9]\d{1,14}$/ return e164Regex.test(phone) } + +export function validateAndConvertPhoneNumber(phone?: string, countryCode?: string): string | undefined | null { + if (!phone) return + + const e164Regex = /^\+[1-9]\d{1,14}$/ + + // Check if the phone number is already in E.164 format + if (e164Regex.test(phone)) { + return phone + } + + // If phone number is not in E.164 format, attempt to convert it using the country code + if (countryCode) { + try { + const parsedPhone = phoneUtil.parse(phone, countryCode) + const isValid = phoneUtil.isValidNumberForRegion(parsedPhone, countryCode) + + if (!isValid) { + return null + } + + return phoneUtil.format(parsedPhone, PhoneNumberFormat.E164) + } catch (error) { + return null + } + } + + return null +} + +export async function sendBatchedTrackEvent(request: RequestClient, payloads: TrackEventPayload[]) { + const multiStatusResponse = new MultiStatusResponse() + const { filteredPayloads, validPayloadIndicesBitmap } = validateAndPreparePayloads(payloads, multiStatusResponse) + // if there are no payloads with valid phone number/email/external_id, return multiStatusResponse + if (filteredPayloads.length) { + const payloadToSend = { + data: { + type: 'event-bulk-create-job', + attributes: { + 'events-bulk-create': { + data: filteredPayloads + } + } + } + } + + try { + await request(`${API_URL}/event-bulk-create-jobs/`, { + method: 'POST', + json: payloadToSend + }) + } catch (err: any) { + await handleKlaviyoAPIErrorResponse( + transformPayloadsType(payloads), + await err?.response?.json(), + multiStatusResponse, + validPayloadIndicesBitmap + ) + } + } + + return multiStatusResponse +} + +function validateAndPreparePayloads(payloads: TrackEventPayload[], multiStatusResponse: MultiStatusResponse) { + const filteredPayloads: JSONLikeObject[] = [] + const validPayloadIndicesBitmap: number[] = [] + + payloads.forEach((payload, originalBatchIndex) => { + const { country_code, phone_number: initialPhoneNumber } = payload.profile + + if (initialPhoneNumber) { + // Validate and convert the phone number if present + const validPhoneNumber = validateAndConvertPhoneNumber(initialPhoneNumber, country_code as string) + // If the phone number is not valid, skip this payload + if (!validPhoneNumber) { + multiStatusResponse.setErrorResponseAtIndex(originalBatchIndex, { + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'Phone number could not be converted to E.164 format.' + }) + return // Skip this payload + } + + // Update the payload's phone number with the validated format + payload.profile.phone_number = validPhoneNumber + delete payload?.profile?.country_code + } + + // Filter out and record if payload is invalid + const validationError: ActionDestinationErrorResponseType | null = validatePayload(payload) + if (validationError) { + multiStatusResponse.setErrorResponseAtIndex(originalBatchIndex, validationError) + return + } + + // if (!email && !phone_number && !external_id && !anonymous_id) { + // multiStatusResponse.setErrorResponseAtIndex(originalBatchIndex, { + // status: 400, + // errortype: 'PAYLOAD_VALIDATION_FAILED', + // errormessage: 'One of External ID, Anonymous ID, Phone Number or Email is required.' + // }) + // return + // } + + const profileToAdd = constructProfilePayload(payload) + filteredPayloads.push(profileToAdd as JSONLikeObject) + validPayloadIndicesBitmap.push(originalBatchIndex) + multiStatusResponse.setSuccessResponseAtIndex(originalBatchIndex, { + status: 200, + sent: profileToAdd as JSONLikeObject, + body: 'success' + }) + }) + + return { filteredPayloads, validPayloadIndicesBitmap } +} + +function constructProfilePayload(payload: TrackEventPayload) { + return { + type: 'event-bulk-create', + attributes: { + profile: { + data: { + type: 'profile', + attributes: payload.profile + } + }, + events: { + data: [ + { + type: 'event', + attributes: { + metric: { + data: { + type: 'metric', + attributes: { + name: payload.metric_name + } + } + }, + properties: { ...payload.properties }, + time: payload?.time ? dayjs(payload.time).toISOString() : undefined, + value: payload.value, + unique_id: payload.unique_id + } + } + ] + } + } + } +} + +async function handleKlaviyoAPIErrorResponse( + payloads: JSONLikeObject[], + response: any, + multiStatusResponse: MultiStatusResponse, + validPayloadIndicesBitmap: number[] +) { + if (response?.errors && Array.isArray(response.errors)) { + const invalidIndexSet = new Set() + response.errors.forEach((error: KlaviyoAPIErrorResponse) => { + const indexInOriginalPayload = getIndexFromErrorPointer(error.source.pointer, validPayloadIndicesBitmap) + if (indexInOriginalPayload !== -1 && !multiStatusResponse.isErrorResponseAtIndex(indexInOriginalPayload)) { + multiStatusResponse.setErrorResponseAtIndex(indexInOriginalPayload, { + status: error.status, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: error.detail, + sent: payloads[indexInOriginalPayload], + body: JSON.stringify(error) + }) + invalidIndexSet.add(indexInOriginalPayload) + } + }) + + for (const index of validPayloadIndicesBitmap) { + if (!invalidIndexSet.has(index)) { + multiStatusResponse.setSuccessResponseAtIndex(index, { + status: 429, + sent: payloads[index], + body: 'Retry' + }) + } + } + } +} + +function getIndexFromErrorPointer(pointer: string, validPayloadIndicesBitmap: number[]) { + const match = /\/data\/attributes\/events-bulk-create\/data\/(\d+)/.exec(pointer) + if (match && match[1]) { + const index = parseInt(match[1], 10) + return validPayloadIndicesBitmap[index] !== undefined ? validPayloadIndicesBitmap[index] : -1 + } + return -1 +} + +function transformPayloadsType(obj: object[]) { + return obj as JSONLikeObject[] +} + +function validatePayload(payload: TrackEventPayload): ActionDestinationErrorResponseType | null { + const { email, phone_number, external_id, anonymous_id } = payload.profile + + if (!email && !phone_number && !external_id && !anonymous_id) { + return { + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'One of External ID, Anonymous ID, Phone Number or Email is required.' + } + } + return null +} diff --git a/packages/destination-actions/src/destinations/klaviyo/properties.ts b/packages/destination-actions/src/destinations/klaviyo/properties.ts index dace8f7a8d..f1101f1ee9 100644 --- a/packages/destination-actions/src/destinations/klaviyo/properties.ts +++ b/packages/destination-actions/src/destinations/klaviyo/properties.ts @@ -1,4 +1,5 @@ import { InputField } from '@segment/actions-core/destination-kit/types' +import { COUNTRY_CODES } from './config' export const list_id: InputField = { label: 'List Id', @@ -145,3 +146,19 @@ export const phone_number: InputField = { type: 'string', default: { '@path': '$.context.traits.phone' } } + +export const country_code: InputField = { + label: 'Country Code', + description: `Country Code of the user. We support ISO 3166-1 alpha-2 country code.`, + type: 'string', + choices: COUNTRY_CODES, + depends_on: { + conditions: [ + { + fieldKey: 'phone_number', + operator: 'is_not', + value: '' + } + ] + } +} diff --git a/packages/destination-actions/src/destinations/klaviyo/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/klaviyo/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap index 634a3104bb..397ebb8293 100644 --- a/packages/destination-actions/src/destinations/klaviyo/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/klaviyo/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -16,6 +16,7 @@ Object { "data": Object { "attributes": Object { "anonymous_id": "]DD4LgSzT#hw(U]@J$a", + "country_code": "TO", "email": "so@uzwumiz.wf", "external_id": "]DD4LgSzT#hw(U]@J$a", "phone_number": "+2458829936", diff --git a/packages/destination-actions/src/destinations/klaviyo/trackEvent/index.ts b/packages/destination-actions/src/destinations/klaviyo/trackEvent/index.ts index b0c66efe7f..a8ae37ac33 100644 --- a/packages/destination-actions/src/destinations/klaviyo/trackEvent/index.ts +++ b/packages/destination-actions/src/destinations/klaviyo/trackEvent/index.ts @@ -1,12 +1,12 @@ -import type { ActionDefinition } from '@segment/actions-core' +import { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { PayloadValidationError } from '@segment/actions-core' import { API_URL } from '../config' -import { validatePhoneNumber } from '../functions' +import { validatePhoneNumber, sendBatchedTrackEvent } from '../functions' +import { batch_size, enable_batching, country_code } from '../properties' import dayjs from 'dayjs' - const action: ActionDefinition = { title: 'Track Event', description: 'Track user events and associate it with their profile.', @@ -25,6 +25,9 @@ const action: ActionDefinition = { label: 'Phone Number', type: 'string' }, + country_code: { + ...country_code + }, external_id: { label: 'External Id', description: @@ -87,15 +90,15 @@ const action: ActionDefinition = { default: { '@path': '$.messageId' } - } + }, + enable_batching: { ...enable_batching }, + batch_size: { ...batch_size } }, perform: (request, { payload }) => { const { email, phone_number, external_id, anonymous_id } = payload.profile - if (!email && !phone_number && !external_id && !anonymous_id) { throw new PayloadValidationError('One of External ID, Anonymous ID, Phone Number or Email is required.') } - if (phone_number && !validatePhoneNumber(phone_number)) { throw new PayloadValidationError(`${phone_number} is not a valid E.164 phone number.`) } @@ -124,11 +127,13 @@ const action: ActionDefinition = { } } } - return request(`${API_URL}/events/`, { method: 'POST', json: eventData }) + }, + performBatch: (request, { payload }) => { + return sendBatchedTrackEvent(request, payload) } } diff --git a/packages/destination-actions/src/destinations/klaviyo/types.ts b/packages/destination-actions/src/destinations/klaviyo/types.ts index fc645ad0c5..b85fc2605c 100644 --- a/packages/destination-actions/src/destinations/klaviyo/types.ts +++ b/packages/destination-actions/src/destinations/klaviyo/types.ts @@ -219,3 +219,14 @@ export interface AdditionalAttributes { title?: string image?: string } +export interface KlaviyoAPIErrorResponse { + id: string + status: number + code: string + title: string + detail: string + source: { + pointer: string + parameter?: string + } +} From 8a8e54a342f916c95c4ca815829263b2a6ffc4e5 Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Fri, 11 Oct 2024 19:00:53 +0530 Subject: [PATCH 02/13] Added KlaviyoAPIErrorResponse Error interface --- .../src/destinations/klaviyo/functions.ts | 4 ++-- .../destination-actions/src/destinations/klaviyo/types.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index fdf4122b47..6e6bd0740c 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -571,13 +571,13 @@ function constructProfilePayload(payload: TrackEventPayload) { async function handleKlaviyoAPIErrorResponse( payloads: JSONLikeObject[], - response: any, + response: KlaviyoAPIErrorResponse, multiStatusResponse: MultiStatusResponse, validPayloadIndicesBitmap: number[] ) { if (response?.errors && Array.isArray(response.errors)) { const invalidIndexSet = new Set() - response.errors.forEach((error: KlaviyoAPIErrorResponse) => { + response.errors.forEach((error: KlaviyoAPIError) => { const indexInOriginalPayload = getIndexFromErrorPointer(error.source.pointer, validPayloadIndicesBitmap) if (indexInOriginalPayload !== -1 && !multiStatusResponse.isErrorResponseAtIndex(indexInOriginalPayload)) { multiStatusResponse.setErrorResponseAtIndex(indexInOriginalPayload, { diff --git a/packages/destination-actions/src/destinations/klaviyo/types.ts b/packages/destination-actions/src/destinations/klaviyo/types.ts index b85fc2605c..d28bcf24f6 100644 --- a/packages/destination-actions/src/destinations/klaviyo/types.ts +++ b/packages/destination-actions/src/destinations/klaviyo/types.ts @@ -219,7 +219,7 @@ export interface AdditionalAttributes { title?: string image?: string } -export interface KlaviyoAPIErrorResponse { +export interface KlaviyoAPIError { id: string status: number code: string @@ -230,3 +230,6 @@ export interface KlaviyoAPIErrorResponse { parameter?: string } } +export interface KlaviyoAPIErrorResponse { + errors: KlaviyoAPIError[] +} From f517de540e64a1eb64251cbeaaa52de91a5adaf2 Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Fri, 11 Oct 2024 19:30:26 +0530 Subject: [PATCH 03/13] Added generated Types --- .../klaviyo/trackEvent/generated-types.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/destination-actions/src/destinations/klaviyo/trackEvent/generated-types.ts b/packages/destination-actions/src/destinations/klaviyo/trackEvent/generated-types.ts index 3b2b01a175..017750b618 100644 --- a/packages/destination-actions/src/destinations/klaviyo/trackEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/klaviyo/trackEvent/generated-types.ts @@ -7,6 +7,10 @@ export interface Payload { profile: { email?: string phone_number?: string + /** + * Country Code of the user. We support ISO 3166-1 alpha-2 country code. + */ + country_code?: string /** * A unique identifier used by customers to associate Klaviyo profiles with profiles in an external system. */ @@ -46,4 +50,12 @@ export interface Payload { * */ unique_id?: string + /** + * When enabled, the action will use the klaviyo batch API. + */ + enable_batching?: boolean + /** + * Maximum number of events to include in each batch. Actual batch sizes may be lower. + */ + batch_size?: number } From f1d807a5569e518cfc674305d0f1daf1d7f49976 Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Mon, 14 Oct 2024 12:47:37 +0530 Subject: [PATCH 04/13] Remove Commented unnecessary code --- .../src/destinations/klaviyo/functions.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index 6e6bd0740c..8c8cf7bc71 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -512,15 +512,6 @@ function validateAndPreparePayloads(payloads: TrackEventPayload[], multiStatusRe return } - // if (!email && !phone_number && !external_id && !anonymous_id) { - // multiStatusResponse.setErrorResponseAtIndex(originalBatchIndex, { - // status: 400, - // errortype: 'PAYLOAD_VALIDATION_FAILED', - // errormessage: 'One of External ID, Anonymous ID, Phone Number or Email is required.' - // }) - // return - // } - const profileToAdd = constructProfilePayload(payload) filteredPayloads.push(profileToAdd as JSONLikeObject) validPayloadIndicesBitmap.push(originalBatchIndex) From d560f6542f54a8fe3adfc158b881986276ebcd26 Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Wed, 16 Oct 2024 19:42:26 +0530 Subject: [PATCH 05/13] Updated snapshots --- .../klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap | 1 - .../trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap index ca12444d29..fb456e1b0c 100644 --- a/packages/destination-actions/src/destinations/klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap @@ -130,7 +130,6 @@ Object { "data": Object { "attributes": Object { "anonymous_id": "mTdOx(Nl)", - "country_code": "GA", "email": "ujoeri@ifosi.kp", "external_id": "mTdOx(Nl)", "phone_number": "+5694788449", diff --git a/packages/destination-actions/src/destinations/klaviyo/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/klaviyo/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap index 397ebb8293..634a3104bb 100644 --- a/packages/destination-actions/src/destinations/klaviyo/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/klaviyo/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -16,7 +16,6 @@ Object { "data": Object { "attributes": Object { "anonymous_id": "]DD4LgSzT#hw(U]@J$a", - "country_code": "TO", "email": "so@uzwumiz.wf", "external_id": "]DD4LgSzT#hw(U]@J$a", "phone_number": "+2458829936", From 0432eea6cf2aed18d72b7efd04741f33d0b598df Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Thu, 17 Oct 2024 17:18:16 +0530 Subject: [PATCH 06/13] handle Pr comments --- .../src/destinations/klaviyo/functions.ts | 64 ++++++++----------- 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index 66b6046e51..a001ab9f05 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -4,7 +4,8 @@ import { DynamicFieldResponse, IntegrationError, PayloadValidationError, - MultiStatusResponse + MultiStatusResponse, + HTTPError } from '@segment/actions-core' import { JSONLikeObject } from '@segment/actions-core' import { API_URL, REVISION_DATE } from './config' @@ -29,7 +30,6 @@ import { Payload } from './upsertProfile/generated-types' import { Payload as TrackEventPayload } from './trackEvent/generated-types' import dayjs from 'dayjs' import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber' -import { ActionDestinationErrorResponseType } from '@segment/actions-core/destination-kittypes' const phoneUtil = PhoneNumberUtil.getInstance() @@ -479,13 +479,19 @@ export async function sendBatchedTrackEvent(request: RequestClient, payloads: Tr method: 'POST', json: payloadToSend }) - } catch (err: any) { - await handleKlaviyoAPIErrorResponse( - transformPayloadsType(payloads), - await err?.response?.json(), - multiStatusResponse, - validPayloadIndicesBitmap - ) + } catch (err) { + if (err instanceof HTTPError) { + const errorResponse = await err?.response?.json() + handleKlaviyoAPIErrorResponse( + payloads as object as JSONLikeObject[], + errorResponse, + multiStatusResponse, + validPayloadIndicesBitmap + ) + } else { + // Bubble up the error and let Actions Framework handle it + throw err + } } } @@ -497,11 +503,19 @@ function validateAndPreparePayloads(payloads: TrackEventPayload[], multiStatusRe const validPayloadIndicesBitmap: number[] = [] payloads.forEach((payload, originalBatchIndex) => { - const { country_code, phone_number: initialPhoneNumber } = payload.profile + const { email, phone_number, external_id, anonymous_id, country_code } = payload.profile + if (!email && !phone_number && !external_id && !anonymous_id) { + multiStatusResponse.setErrorResponseAtIndex(originalBatchIndex, { + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'One of External ID, Anonymous ID, Phone Number or Email is required.' + }) + return + } - if (initialPhoneNumber) { + if (phone_number) { // Validate and convert the phone number if present - const validPhoneNumber = validateAndConvertPhoneNumber(initialPhoneNumber, country_code as string) + const validPhoneNumber = validateAndConvertPhoneNumber(phone_number, country_code as string) // If the phone number is not valid, skip this payload if (!validPhoneNumber) { multiStatusResponse.setErrorResponseAtIndex(originalBatchIndex, { @@ -517,13 +531,6 @@ function validateAndPreparePayloads(payloads: TrackEventPayload[], multiStatusRe delete payload?.profile?.country_code } - // Filter out and record if payload is invalid - const validationError: ActionDestinationErrorResponseType | null = validatePayload(payload) - if (validationError) { - multiStatusResponse.setErrorResponseAtIndex(originalBatchIndex, validationError) - return - } - const profileToAdd = constructProfilePayload(payload) filteredPayloads.push(profileToAdd as JSONLikeObject) validPayloadIndicesBitmap.push(originalBatchIndex) @@ -572,7 +579,7 @@ function constructProfilePayload(payload: TrackEventPayload) { } } -async function handleKlaviyoAPIErrorResponse( +function handleKlaviyoAPIErrorResponse( payloads: JSONLikeObject[], response: KlaviyoAPIErrorResponse, multiStatusResponse: MultiStatusResponse, @@ -614,20 +621,3 @@ function getIndexFromErrorPointer(pointer: string, validPayloadIndicesBitmap: nu } return -1 } - -function transformPayloadsType(obj: object[]) { - return obj as JSONLikeObject[] -} - -function validatePayload(payload: TrackEventPayload): ActionDestinationErrorResponseType | null { - const { email, phone_number, external_id, anonymous_id } = payload.profile - - if (!email && !phone_number && !external_id && !anonymous_id) { - return { - status: 400, - errortype: 'PAYLOAD_VALIDATION_FAILED', - errormessage: 'One of External ID, Anonymous ID, Phone Number or Email is required.' - } - } - return null -} From b71e6aff73087aca587c05c23618949fbf76138b Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Thu, 17 Oct 2024 18:08:57 +0530 Subject: [PATCH 07/13] ActionDefinition as type --- .../src/destinations/klaviyo/trackEvent/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/trackEvent/index.ts b/packages/destination-actions/src/destinations/klaviyo/trackEvent/index.ts index 073125a3c7..bd66a2346c 100644 --- a/packages/destination-actions/src/destinations/klaviyo/trackEvent/index.ts +++ b/packages/destination-actions/src/destinations/klaviyo/trackEvent/index.ts @@ -1,4 +1,4 @@ -import { ActionDefinition } from '@segment/actions-core' +import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' From bc442a78d5a5dbe9af9f0bcac754cf4ac4910174 Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Fri, 18 Oct 2024 15:20:20 +0530 Subject: [PATCH 08/13] Making validation generic so that same can be reused in other actions --- .../src/destinations/klaviyo/functions.ts | 109 +++++++++++------- .../src/destinations/klaviyo/properties.ts | 2 + 2 files changed, 70 insertions(+), 41 deletions(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index a001ab9f05..49817ce178 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -30,6 +30,8 @@ import { Payload } from './upsertProfile/generated-types' import { Payload as TrackEventPayload } from './trackEvent/generated-types' import dayjs from 'dayjs' import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber' +import { eventBulkCreateRegex } from './properties' +import { ActionDestinationErrorResponseType } from '@segment/actions-core/destination-kittypes' const phoneUtil = PhoneNumberUtil.getInstance() @@ -460,7 +462,11 @@ export function processPhoneNumber(initialPhoneNumber?: string, country_code?: s export async function sendBatchedTrackEvent(request: RequestClient, payloads: TrackEventPayload[]) { const multiStatusResponse = new MultiStatusResponse() - const { filteredPayloads, validPayloadIndicesBitmap } = validateAndPreparePayloads(payloads, multiStatusResponse) + const { filteredPayloads, validPayloadIndicesBitmap } = validateAndPreparePayloads( + payloads, + multiStatusResponse, + trackEventValidationRules + ) // if there are no payloads with valid phone number/email/external_id, return multiStatusResponse if (filteredPayloads.length) { const payloadToSend = { @@ -486,11 +492,11 @@ export async function sendBatchedTrackEvent(request: RequestClient, payloads: Tr payloads as object as JSONLikeObject[], errorResponse, multiStatusResponse, - validPayloadIndicesBitmap + validPayloadIndicesBitmap, + eventBulkCreateRegex ) } else { - // Bubble up the error and let Actions Framework handle it - throw err + throw err // Bubble up the error } } } @@ -498,50 +504,70 @@ export async function sendBatchedTrackEvent(request: RequestClient, payloads: Tr return multiStatusResponse } -function validateAndPreparePayloads(payloads: TrackEventPayload[], multiStatusResponse: MultiStatusResponse) { +function validateAndPreparePayloads( + payloads: T[], + multiStatusResponse: MultiStatusResponse, + validationRules: (payload: T) => { + validPayload?: JSONLikeObject + error?: ActionDestinationErrorResponseType + } +) { const filteredPayloads: JSONLikeObject[] = [] const validPayloadIndicesBitmap: number[] = [] payloads.forEach((payload, originalBatchIndex) => { - const { email, phone_number, external_id, anonymous_id, country_code } = payload.profile - if (!email && !phone_number && !external_id && !anonymous_id) { - multiStatusResponse.setErrorResponseAtIndex(originalBatchIndex, { - status: 400, - errortype: 'PAYLOAD_VALIDATION_FAILED', - errormessage: 'One of External ID, Anonymous ID, Phone Number or Email is required.' + const { validPayload, error } = validationRules(payload) + if (error) { + multiStatusResponse.setErrorResponseAtIndex(originalBatchIndex, error) + } else { + filteredPayloads.push(validPayload as JSONLikeObject) + validPayloadIndicesBitmap.push(originalBatchIndex) + multiStatusResponse.setSuccessResponseAtIndex(originalBatchIndex, { + status: 200, + sent: validPayload as JSONLikeObject, + body: 'success' }) - return } + }) - if (phone_number) { - // Validate and convert the phone number if present - const validPhoneNumber = validateAndConvertPhoneNumber(phone_number, country_code as string) - // If the phone number is not valid, skip this payload - if (!validPhoneNumber) { - multiStatusResponse.setErrorResponseAtIndex(originalBatchIndex, { - status: 400, - errortype: 'PAYLOAD_VALIDATION_FAILED', - errormessage: 'Phone number could not be converted to E.164 format.' - }) - return // Skip this payload - } + return { filteredPayloads, validPayloadIndicesBitmap } +} - // Update the payload's phone number with the validated format - payload.profile.phone_number = validPhoneNumber - delete payload?.profile?.country_code +const trackEventValidationRules = ( + payload: TrackEventPayload +): { validPayload?: JSONLikeObject; error?: ActionDestinationErrorResponseType } => { + const { email, phone_number, external_id, anonymous_id, country_code } = payload.profile + const response: { + validPayload?: JSONLikeObject + error?: ActionDestinationErrorResponseType + } = {} + + if (!email && !phone_number && !external_id && !anonymous_id) { + response.error = { + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'One of External ID, Anonymous ID, Phone Number or Email is required.' } + return response + } - const profileToAdd = constructProfilePayload(payload) - filteredPayloads.push(profileToAdd as JSONLikeObject) - validPayloadIndicesBitmap.push(originalBatchIndex) - multiStatusResponse.setSuccessResponseAtIndex(originalBatchIndex, { - status: 200, - sent: profileToAdd as JSONLikeObject, - body: 'success' - }) - }) + if (phone_number) { + const validPhoneNumber = validateAndConvertPhoneNumber(phone_number, country_code as string) + if (!validPhoneNumber) { + response.error = { + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'Phone number could not be converted to E.164 format.' + } + return response + } + payload.profile.phone_number = validPhoneNumber + delete payload.profile.country_code + } - return { filteredPayloads, validPayloadIndicesBitmap } + // If all validations pass, construct the valid payload + response.validPayload = constructProfilePayload(payload) as JSONLikeObject + return response } function constructProfilePayload(payload: TrackEventPayload) { @@ -583,12 +609,13 @@ function handleKlaviyoAPIErrorResponse( payloads: JSONLikeObject[], response: KlaviyoAPIErrorResponse, multiStatusResponse: MultiStatusResponse, - validPayloadIndicesBitmap: number[] + validPayloadIndicesBitmap: number[], + regex: RegExp ) { if (response?.errors && Array.isArray(response.errors)) { const invalidIndexSet = new Set() response.errors.forEach((error: KlaviyoAPIError) => { - const indexInOriginalPayload = getIndexFromErrorPointer(error.source.pointer, validPayloadIndicesBitmap) + const indexInOriginalPayload = getIndexFromErrorPointer(error.source.pointer, validPayloadIndicesBitmap, regex) if (indexInOriginalPayload !== -1 && !multiStatusResponse.isErrorResponseAtIndex(indexInOriginalPayload)) { multiStatusResponse.setErrorResponseAtIndex(indexInOriginalPayload, { status: error.status, @@ -613,8 +640,8 @@ function handleKlaviyoAPIErrorResponse( } } -function getIndexFromErrorPointer(pointer: string, validPayloadIndicesBitmap: number[]) { - const match = /\/data\/attributes\/events-bulk-create\/data\/(\d+)/.exec(pointer) +function getIndexFromErrorPointer(pointer: string, validPayloadIndicesBitmap: number[], regex: RegExp) { + const match = regex.exec(pointer) if (match && match[1]) { const index = parseInt(match[1], 10) return validPayloadIndicesBitmap[index] !== undefined ? validPayloadIndicesBitmap[index] : -1 diff --git a/packages/destination-actions/src/destinations/klaviyo/properties.ts b/packages/destination-actions/src/destinations/klaviyo/properties.ts index f2f440a40b..a8cef9bcb4 100644 --- a/packages/destination-actions/src/destinations/klaviyo/properties.ts +++ b/packages/destination-actions/src/destinations/klaviyo/properties.ts @@ -162,3 +162,5 @@ export const country_code: InputField = { ] } } + +export const eventBulkCreateRegex = /\/data\/attributes\/events-bulk-create\/data\/(\d+)/ From f94fe2f06195baa3f93e5e0b684342c7168cd609 Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Fri, 18 Oct 2024 15:45:40 +0530 Subject: [PATCH 09/13] import from dayjs lib --- .../destination-actions/src/destinations/klaviyo/functions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index 49817ce178..f7ca6eaf1b 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -28,7 +28,7 @@ import { } from './types' import { Payload } from './upsertProfile/generated-types' import { Payload as TrackEventPayload } from './trackEvent/generated-types' -import dayjs from 'dayjs' +import dayjs from '../../lib/dayjs' import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber' import { eventBulkCreateRegex } from './properties' import { ActionDestinationErrorResponseType } from '@segment/actions-core/destination-kittypes' From 5cfbe47682d02b5ac08a5829b9c276948aaebc75 Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Mon, 21 Oct 2024 14:33:09 +0530 Subject: [PATCH 10/13] Revert "Making validation generic so that same can be reused in other actions" This reverts commit bc442a78d5a5dbe9af9f0bcac754cf4ac4910174. --- .../src/destinations/klaviyo/functions.ts | 109 +++++++----------- .../src/destinations/klaviyo/properties.ts | 2 - 2 files changed, 41 insertions(+), 70 deletions(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index f7ca6eaf1b..9d384e43b2 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -30,8 +30,6 @@ import { Payload } from './upsertProfile/generated-types' import { Payload as TrackEventPayload } from './trackEvent/generated-types' import dayjs from '../../lib/dayjs' import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber' -import { eventBulkCreateRegex } from './properties' -import { ActionDestinationErrorResponseType } from '@segment/actions-core/destination-kittypes' const phoneUtil = PhoneNumberUtil.getInstance() @@ -462,11 +460,7 @@ export function processPhoneNumber(initialPhoneNumber?: string, country_code?: s export async function sendBatchedTrackEvent(request: RequestClient, payloads: TrackEventPayload[]) { const multiStatusResponse = new MultiStatusResponse() - const { filteredPayloads, validPayloadIndicesBitmap } = validateAndPreparePayloads( - payloads, - multiStatusResponse, - trackEventValidationRules - ) + const { filteredPayloads, validPayloadIndicesBitmap } = validateAndPreparePayloads(payloads, multiStatusResponse) // if there are no payloads with valid phone number/email/external_id, return multiStatusResponse if (filteredPayloads.length) { const payloadToSend = { @@ -492,11 +486,11 @@ export async function sendBatchedTrackEvent(request: RequestClient, payloads: Tr payloads as object as JSONLikeObject[], errorResponse, multiStatusResponse, - validPayloadIndicesBitmap, - eventBulkCreateRegex + validPayloadIndicesBitmap ) } else { - throw err // Bubble up the error + // Bubble up the error and let Actions Framework handle it + throw err } } } @@ -504,70 +498,50 @@ export async function sendBatchedTrackEvent(request: RequestClient, payloads: Tr return multiStatusResponse } -function validateAndPreparePayloads( - payloads: T[], - multiStatusResponse: MultiStatusResponse, - validationRules: (payload: T) => { - validPayload?: JSONLikeObject - error?: ActionDestinationErrorResponseType - } -) { +function validateAndPreparePayloads(payloads: TrackEventPayload[], multiStatusResponse: MultiStatusResponse) { const filteredPayloads: JSONLikeObject[] = [] const validPayloadIndicesBitmap: number[] = [] payloads.forEach((payload, originalBatchIndex) => { - const { validPayload, error } = validationRules(payload) - if (error) { - multiStatusResponse.setErrorResponseAtIndex(originalBatchIndex, error) - } else { - filteredPayloads.push(validPayload as JSONLikeObject) - validPayloadIndicesBitmap.push(originalBatchIndex) - multiStatusResponse.setSuccessResponseAtIndex(originalBatchIndex, { - status: 200, - sent: validPayload as JSONLikeObject, - body: 'success' + const { email, phone_number, external_id, anonymous_id, country_code } = payload.profile + if (!email && !phone_number && !external_id && !anonymous_id) { + multiStatusResponse.setErrorResponseAtIndex(originalBatchIndex, { + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'One of External ID, Anonymous ID, Phone Number or Email is required.' }) + return } - }) - return { filteredPayloads, validPayloadIndicesBitmap } -} + if (phone_number) { + // Validate and convert the phone number if present + const validPhoneNumber = validateAndConvertPhoneNumber(phone_number, country_code as string) + // If the phone number is not valid, skip this payload + if (!validPhoneNumber) { + multiStatusResponse.setErrorResponseAtIndex(originalBatchIndex, { + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'Phone number could not be converted to E.164 format.' + }) + return // Skip this payload + } -const trackEventValidationRules = ( - payload: TrackEventPayload -): { validPayload?: JSONLikeObject; error?: ActionDestinationErrorResponseType } => { - const { email, phone_number, external_id, anonymous_id, country_code } = payload.profile - const response: { - validPayload?: JSONLikeObject - error?: ActionDestinationErrorResponseType - } = {} - - if (!email && !phone_number && !external_id && !anonymous_id) { - response.error = { - status: 400, - errortype: 'PAYLOAD_VALIDATION_FAILED', - errormessage: 'One of External ID, Anonymous ID, Phone Number or Email is required.' + // Update the payload's phone number with the validated format + payload.profile.phone_number = validPhoneNumber + delete payload?.profile?.country_code } - return response - } - if (phone_number) { - const validPhoneNumber = validateAndConvertPhoneNumber(phone_number, country_code as string) - if (!validPhoneNumber) { - response.error = { - status: 400, - errortype: 'PAYLOAD_VALIDATION_FAILED', - errormessage: 'Phone number could not be converted to E.164 format.' - } - return response - } - payload.profile.phone_number = validPhoneNumber - delete payload.profile.country_code - } + const profileToAdd = constructProfilePayload(payload) + filteredPayloads.push(profileToAdd as JSONLikeObject) + validPayloadIndicesBitmap.push(originalBatchIndex) + multiStatusResponse.setSuccessResponseAtIndex(originalBatchIndex, { + status: 200, + sent: profileToAdd as JSONLikeObject, + body: 'success' + }) + }) - // If all validations pass, construct the valid payload - response.validPayload = constructProfilePayload(payload) as JSONLikeObject - return response + return { filteredPayloads, validPayloadIndicesBitmap } } function constructProfilePayload(payload: TrackEventPayload) { @@ -609,13 +583,12 @@ function handleKlaviyoAPIErrorResponse( payloads: JSONLikeObject[], response: KlaviyoAPIErrorResponse, multiStatusResponse: MultiStatusResponse, - validPayloadIndicesBitmap: number[], - regex: RegExp + validPayloadIndicesBitmap: number[] ) { if (response?.errors && Array.isArray(response.errors)) { const invalidIndexSet = new Set() response.errors.forEach((error: KlaviyoAPIError) => { - const indexInOriginalPayload = getIndexFromErrorPointer(error.source.pointer, validPayloadIndicesBitmap, regex) + const indexInOriginalPayload = getIndexFromErrorPointer(error.source.pointer, validPayloadIndicesBitmap) if (indexInOriginalPayload !== -1 && !multiStatusResponse.isErrorResponseAtIndex(indexInOriginalPayload)) { multiStatusResponse.setErrorResponseAtIndex(indexInOriginalPayload, { status: error.status, @@ -640,8 +613,8 @@ function handleKlaviyoAPIErrorResponse( } } -function getIndexFromErrorPointer(pointer: string, validPayloadIndicesBitmap: number[], regex: RegExp) { - const match = regex.exec(pointer) +function getIndexFromErrorPointer(pointer: string, validPayloadIndicesBitmap: number[]) { + const match = /\/data\/attributes\/events-bulk-create\/data\/(\d+)/.exec(pointer) if (match && match[1]) { const index = parseInt(match[1], 10) return validPayloadIndicesBitmap[index] !== undefined ? validPayloadIndicesBitmap[index] : -1 diff --git a/packages/destination-actions/src/destinations/klaviyo/properties.ts b/packages/destination-actions/src/destinations/klaviyo/properties.ts index a8cef9bcb4..f2f440a40b 100644 --- a/packages/destination-actions/src/destinations/klaviyo/properties.ts +++ b/packages/destination-actions/src/destinations/klaviyo/properties.ts @@ -162,5 +162,3 @@ export const country_code: InputField = { ] } } - -export const eventBulkCreateRegex = /\/data\/attributes\/events-bulk-create\/data\/(\d+)/ From 28810ebb9698bf3ca36ca4ef7635d2f046bc5b8f Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Mon, 21 Oct 2024 14:59:42 +0530 Subject: [PATCH 11/13] Small Fixes --- .../src/destinations/klaviyo/functions.ts | 65 ++++++++++--------- .../src/destinations/klaviyo/properties.ts | 1 + 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index 9d384e43b2..22ad7c2aac 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -30,6 +30,7 @@ import { Payload } from './upsertProfile/generated-types' import { Payload as TrackEventPayload } from './trackEvent/generated-types' import dayjs from '../../lib/dayjs' import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber' +import { eventBulkCreateRegex } from './properties' const phoneUtil = PhoneNumberUtil.getInstance() @@ -462,39 +463,40 @@ export async function sendBatchedTrackEvent(request: RequestClient, payloads: Tr const multiStatusResponse = new MultiStatusResponse() const { filteredPayloads, validPayloadIndicesBitmap } = validateAndPreparePayloads(payloads, multiStatusResponse) // if there are no payloads with valid phone number/email/external_id, return multiStatusResponse - if (filteredPayloads.length) { - const payloadToSend = { - data: { - type: 'event-bulk-create-job', - attributes: { - 'events-bulk-create': { - data: filteredPayloads - } + if (!filteredPayloads.length) { + return multiStatusResponse + } + const payloadToSend = { + data: { + type: 'event-bulk-create-job', + attributes: { + 'events-bulk-create': { + data: filteredPayloads } } } + } - try { - await request(`${API_URL}/event-bulk-create-jobs/`, { - method: 'POST', - json: payloadToSend - }) - } catch (err) { - if (err instanceof HTTPError) { - const errorResponse = await err?.response?.json() - handleKlaviyoAPIErrorResponse( - payloads as object as JSONLikeObject[], - errorResponse, - multiStatusResponse, - validPayloadIndicesBitmap - ) - } else { - // Bubble up the error and let Actions Framework handle it - throw err - } + try { + await request(`${API_URL}/event-bulk-create-jobs/`, { + method: 'POST', + json: payloadToSend + }) + } catch (err) { + if (err instanceof HTTPError) { + const errorResponse = await err?.response?.json() + handleKlaviyoAPIErrorResponse( + payloads as object as JSONLikeObject[], + errorResponse, + multiStatusResponse, + validPayloadIndicesBitmap, + eventBulkCreateRegex + ) + } else { + // Bubble up the error and let Actions Framework handle it + throw err } } - return multiStatusResponse } @@ -583,12 +585,13 @@ function handleKlaviyoAPIErrorResponse( payloads: JSONLikeObject[], response: KlaviyoAPIErrorResponse, multiStatusResponse: MultiStatusResponse, - validPayloadIndicesBitmap: number[] + validPayloadIndicesBitmap: number[], + regex: RegExp ) { if (response?.errors && Array.isArray(response.errors)) { const invalidIndexSet = new Set() response.errors.forEach((error: KlaviyoAPIError) => { - const indexInOriginalPayload = getIndexFromErrorPointer(error.source.pointer, validPayloadIndicesBitmap) + const indexInOriginalPayload = getIndexFromErrorPointer(error.source.pointer, validPayloadIndicesBitmap, regex) if (indexInOriginalPayload !== -1 && !multiStatusResponse.isErrorResponseAtIndex(indexInOriginalPayload)) { multiStatusResponse.setErrorResponseAtIndex(indexInOriginalPayload, { status: error.status, @@ -613,8 +616,8 @@ function handleKlaviyoAPIErrorResponse( } } -function getIndexFromErrorPointer(pointer: string, validPayloadIndicesBitmap: number[]) { - const match = /\/data\/attributes\/events-bulk-create\/data\/(\d+)/.exec(pointer) +function getIndexFromErrorPointer(pointer: string, validPayloadIndicesBitmap: number[], regex: RegExp) { + const match = regex.exec(pointer) if (match && match[1]) { const index = parseInt(match[1], 10) return validPayloadIndicesBitmap[index] !== undefined ? validPayloadIndicesBitmap[index] : -1 diff --git a/packages/destination-actions/src/destinations/klaviyo/properties.ts b/packages/destination-actions/src/destinations/klaviyo/properties.ts index f2f440a40b..6228b7e9f7 100644 --- a/packages/destination-actions/src/destinations/klaviyo/properties.ts +++ b/packages/destination-actions/src/destinations/klaviyo/properties.ts @@ -162,3 +162,4 @@ export const country_code: InputField = { ] } } +export const eventBulkCreateRegex = /\/data\/attributes\/events-bulk-create\/data\/(\d+)/ From 31591e0eb37e5cedb7c84678ad7b282550652786 Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Mon, 21 Oct 2024 15:17:15 +0530 Subject: [PATCH 12/13] Just rename function constructBulkCreateEventPayload --- .../destination-actions/src/destinations/klaviyo/functions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index 22ad7c2aac..5ec92f62e5 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -533,7 +533,7 @@ function validateAndPreparePayloads(payloads: TrackEventPayload[], multiStatusRe delete payload?.profile?.country_code } - const profileToAdd = constructProfilePayload(payload) + const profileToAdd = constructBulkCreateEventPayload(payload) filteredPayloads.push(profileToAdd as JSONLikeObject) validPayloadIndicesBitmap.push(originalBatchIndex) multiStatusResponse.setSuccessResponseAtIndex(originalBatchIndex, { @@ -546,7 +546,7 @@ function validateAndPreparePayloads(payloads: TrackEventPayload[], multiStatusRe return { filteredPayloads, validPayloadIndicesBitmap } } -function constructProfilePayload(payload: TrackEventPayload) { +function constructBulkCreateEventPayload(payload: TrackEventPayload) { return { type: 'event-bulk-create', attributes: { From 53755217d77045ab818e895963bf5b0445d4035e Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Mon, 21 Oct 2024 19:09:34 +0530 Subject: [PATCH 13/13] importted dayjs from internal lib --- .../src/destinations/klaviyo/trackEvent/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/trackEvent/index.ts b/packages/destination-actions/src/destinations/klaviyo/trackEvent/index.ts index bd66a2346c..b38988727e 100644 --- a/packages/destination-actions/src/destinations/klaviyo/trackEvent/index.ts +++ b/packages/destination-actions/src/destinations/klaviyo/trackEvent/index.ts @@ -1,12 +1,12 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' - import { PayloadValidationError } from '@segment/actions-core' import { API_URL } from '../config' import { batch_size, enable_batching, country_code } from '../properties' import { processPhoneNumber, sendBatchedTrackEvent } from '../functions' -import dayjs from 'dayjs' +import dayjs from '../../../lib/dayjs' + const action: ActionDefinition = { title: 'Track Event', description: 'Track user events and associate it with their profile.',