From d47ead18b3fcadc80aae8887608927a98b0e5587 Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Tue, 22 Oct 2024 17:46:57 +0530 Subject: [PATCH 1/9] Worked on adding multistatusResponse to AddProfileToList --- .../klaviyo/__tests__/multistatus.test.ts | 261 ++++++++++++++++++ .../addProfileToList/__tests__/index.test.ts | 259 ----------------- .../klaviyo/addProfileToList/index.ts | 28 +- .../src/destinations/klaviyo/functions.ts | 164 ++++++++++- .../src/destinations/klaviyo/properties.ts | 1 + .../src/destinations/klaviyo/types.ts | 18 ++ 6 files changed, 443 insertions(+), 288 deletions(-) create mode 100644 packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts 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..4fe943a97f --- /dev/null +++ b/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts @@ -0,0 +1,261 @@ +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('addProfileToList', () => { + beforeEach(() => { + nock.cleanAll() + jest.resetAllMocks() + }) + afterEach(() => { + jest.resetAllMocks() + }) + const mapping = { + email: { + '@path': '$.properties.email' + }, + phone_number: { + '@path': '$.properties.phone_number' + }, + first_name: { + '@path': '$.properties.firstName' + }, + last_name: { + '@path': '$.properties.lastName' + }, + list_id: { + '@path': '$.properties.list_id' + }, + properties: { + '@path': '$.properties' + }, + country_code: { + '@path': '$.properties.country_code' + }, + external_id: { + '@path': '$.properties.external_id' + } + } + + it("should successfully handle those payload where phone_number is invalid and couldn't be converted to E164 format", async () => { + nock(API_URL).post('/profile-bulk-import-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', + list_id: '123' + } + }), + // Valid Event + createTestEvent({ + type: 'track', + timestamp, + properties: { + email: 'valid@gmail.com', + list_id: '123' + } + }) + ] + + const response = await testDestination.executeBatch('addProfileToList', { + 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('/profile-bulk-import-jobs/').reply(200, { success: true }) + + const events: SegmentEvent[] = [ + // Valid Event + createTestEvent({ + type: 'track', + timestamp, + properties: { + email: 'valid@gmail.com', + list_id: '123' + } + }), + // Event without any user identifier + createTestEvent({ + type: 'track', + properties: { + list_id: '123' + } + }) + ] + + const response = await testDestination.executeBatch('addProfileToList', { + 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, 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/profiles/data/0/attributes/email' + }, + links: {}, + meta: {} + } + ] + } + nock(API_URL).post('/profile-bulk-import-jobs/').reply(400, mockResponse) + + const events: SegmentEvent[] = [ + // Invalid Event + createTestEvent({ + type: 'track', + timestamp, + properties: { + email: 'invalid_email', + list_id: '123' + } + }), + // Valid Event + createTestEvent({ + type: 'track', + timestamp, + properties: { + external_id: 'Xi1234', + list_id: '123' + } + }) + ] + + const response = await testDestination.executeBatch('addProfileToList', { + 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: { + email: 'invalid_email', + list_id: '123', + properties: { + email: 'invalid_email', + list_id: '123' + } + }, + body: '{"id":"752f7ece-af20-44e0-aa3a-b13290d98e72","status":400,"code":"invalid","title":"Invalid input.","detail":"Invalid email address","source":{"pointer":"/data/attributes/profile/data/0/attributes/email"},"links":{},"meta":{}}' + }) + + // The second event fails as Braze API reports an error + expect(response[1]).toMatchObject({ + status: 429, + sent: { + list_id: '123', + external_id: 'Xi1234', + properties: { + external_id: 'Xi1234' + } + }, + 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', + properties: { + country_code: 'IN', + phone_number: '701271', + email: 'valid@gmail.com', + list_id: '123' + } + }), + // Event without any user identifier + createTestEvent({ + type: 'track', + properties: { list_id: '123' } + }) + ] + + const response = await testDestination.executeBatch('addProfileToList', { + 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, Phone Number or Email is required.', + errorreporter: 'DESTINATION' + }) + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/klaviyo/addProfileToList/__tests__/index.test.ts b/packages/destination-actions/src/destinations/klaviyo/addProfileToList/__tests__/index.test.ts index e55076b35d..3a4567affb 100644 --- a/packages/destination-actions/src/destinations/klaviyo/addProfileToList/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/klaviyo/addProfileToList/__tests__/index.test.ts @@ -3,9 +3,6 @@ import { createTestEvent, createTestIntegration } from '@segment/actions-core' import Definition from '../../index' import { API_URL } from '../../config' import { AggregateAjvError } from '@segment/ajv-human-errors' -import { Mock } from 'jest-mock' - -import * as Functions from '../../functions' jest.mock('../../functions', () => ({ ...jest.requireActual('../../functions'), @@ -40,30 +37,6 @@ const profileData = { } } -const importJobPayload = { - data: { - type: 'profile-bulk-import-job', - attributes: { - profiles: { - data: [ - { - type: 'profile', - attributes: { - email: 'valid@example.com', - external_id: 'fake-external-id' - } - } - ] - } - }, - relationships: { - lists: { - data: [{ type: 'list', id: listId }] - } - } - } -} - const profileProperties = { first_name: 'John', last_name: 'Doe', @@ -428,235 +401,3 @@ describe('Add Profile To List', () => { ).resolves.not.toThrowError() }) }) - -describe('Add Profile To List Batch', () => { - beforeEach(() => { - nock.cleanAll() - jest.resetAllMocks() - }) - afterEach(() => { - jest.resetAllMocks() - }) - - it('should filter out profiles without email, phone number or external_id', async () => { - const events = [ - createTestEvent({ - context: { personas: { list_id: '123' }, traits: { email: 'valid@example.com' } } - }), - createTestEvent({ - context: { personas: {}, traits: {} } - }) - ] - - const mapping = { - list_id: listId, - email: { - '@path': '$.context.traits.email' - } - } - - nock(API_URL).post('/profile-bulk-import-jobs/').reply(200, { success: true }) - - await testDestination.testBatchAction('addProfileToList', { - settings, - events, - mapping, - useDefaultMappings: true - }) - - // Check if createImportJobPayload was called with only the valid profile - expect(Functions.createImportJobPayload).toHaveBeenCalledWith( - [ - { - batch_size: 10000, - list_id: listId, - email: 'valid@example.com', - enable_batching: true, - location: {} - } - ], - listId - ) - }) - - it('should filter out invalid phone numbers', async () => { - const events = [ - createTestEvent({ - context: { personas: { list_id: '123' }, traits: { email: 'valid@example.com' } } - }), - createTestEvent({ - context: { personas: {}, traits: { phone: 'invalid-phone-number' } } - }) - ] - - const mapping = { - list_id: listId, - email: { - '@path': '$.context.traits.email' - }, - phone_number: { - '@path': '$.context.traits.phone' - }, - country_code: 'US' - } - - nock(API_URL).post('/profile-bulk-import-jobs/').reply(200, { success: true }) - - await testDestination.testBatchAction('addProfileToList', { - settings, - events, - mapping, - useDefaultMappings: true - }) - - // Check if createImportJobPayload was called with only the valid profile - expect(Functions.createImportJobPayload).toHaveBeenCalledWith( - [ - { - batch_size: 10000, - list_id: listId, - email: 'valid@example.com', - enable_batching: true, - location: {}, - country_code: 'US' - } - ], - listId - ) - }) - - it('should convert a phone number to E.164 format if country code is provided', async () => { - const events = [ - createTestEvent({ - context: { personas: { list_id: listId }, traits: { phone: '8448309211' } } - }) - ] - const mapping = { - list_id: listId, - external_id: 'fake-external-id', - phone: { - '@path': '$.context.traits.phone' - }, - country_code: 'IN' - } - - nock(API_URL).post('/profile-bulk-import-jobs/').reply(200, { success: true }) - - await testDestination.testBatchAction('addProfileToList', { - settings, - events, - mapping, - useDefaultMappings: true - }) - - expect(Functions.createImportJobPayload).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - phone_number: '+918448309211', - list_id: listId - }) - ]), - listId - ) - }) - - it('should create an import job payload with the correct listId', async () => { - const events = [ - createTestEvent({ - context: { personas: { list_id: listId }, traits: { email: 'valid@example.com' } } - }) - ] - const mapping = { - list_id: listId, - external_id: 'fake-external-id', - email: { - '@path': '$.context.traits.email' - } - } - - nock(API_URL).post('/profile-bulk-import-jobs/').reply(200, { success: true }) - - await testDestination.testBatchAction('addProfileToList', { - settings, - events, - mapping, - useDefaultMappings: true - }) - - expect(Functions.createImportJobPayload).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ - email: 'valid@example.com', - external_id: 'fake-external-id', - list_id: listId - }) - ]), - listId - ) - }) - - it('should send an import job request with the generated payload and profile properties', async () => { - const events = [ - createTestEvent({ - context: { personas: { list_id: listId }, traits: { email: 'valid@example.com' } } - }) - ] - const mapping = { - list_id: listId, - external_id: 'fake-external-id', - email: { - '@path': '$.context.traits.email' - }, - ...profileProperties - } - - nock(API_URL).post('/profile-bulk-import-jobs/', importJobPayload).reply(200, { success: true }) - - await testDestination.testBatchAction('addProfileToList', { - events, - settings, - mapping, - useDefaultMappings: true - }) - expect(Functions.createImportJobPayload).toBeCalledWith( - [ - { - batch_size: 10000, - email: 'valid@example.com', - enable_batching: true, - external_id: 'fake-external-id', - list_id: listId, - ...profileProperties - } - ], - listId - ) - }) - - it('should send an import job request with the generated payload', async () => { - const events = [ - createTestEvent({ - context: { personas: { list_id: listId }, traits: { email: 'valid@example.com' } } - }) - ] - const mapping = { - list_id: listId, - external_id: 'fake-external-id', - email: { - '@path': '$.context.traits.email' - } - } - - ;(Functions.createImportJobPayload as Mock).mockImplementation(() => importJobPayload) - nock(API_URL).post('/profile-bulk-import-jobs/', importJobPayload).reply(200, { success: true }) - - await testDestination.testBatchAction('addProfileToList', { - events, - settings, - mapping, - useDefaultMappings: true - }) - - expect(Functions.sendImportJobRequest).toHaveBeenCalledWith(expect.anything(), importJobPayload) - }) -}) diff --git a/packages/destination-actions/src/destinations/klaviyo/addProfileToList/index.ts b/packages/destination-actions/src/destinations/klaviyo/addProfileToList/index.ts index bdc5487de6..21e1b33800 100644 --- a/packages/destination-actions/src/destinations/klaviyo/addProfileToList/index.ts +++ b/packages/destination-actions/src/destinations/klaviyo/addProfileToList/index.ts @@ -1,14 +1,7 @@ import { ActionDefinition, PayloadValidationError } from '@segment/actions-core' import type { Settings } from '../generated-types' import { Payload } from './generated-types' -import { - createProfile, - addProfileToList, - createImportJobPayload, - sendImportJobRequest, - validateAndConvertPhoneNumber, - processPhoneNumber -} from '../functions' +import { createProfile, addProfileToList, processPhoneNumber, sendBatchedProfileImportJobRequest } from '../functions' import { email, external_id, @@ -65,24 +58,7 @@ const action: ActionDefinition = { return await addProfileToList(request, profileId, list_id) }, performBatch: async (request, { payload }) => { - // Filtering out profiles that do not contain either an email, external_id or valid phone number. - payload = payload.filter((profile) => { - // Validate and convert the phone number using the provided country code - const validPhoneNumber = validateAndConvertPhoneNumber(profile.phone_number, profile.country_code) - - // If the phone number is valid, update the profile's phone number with the validated format - if (validPhoneNumber) { - profile.phone_number = validPhoneNumber - } - // If the phone number is invalid (null), exclude this profile - else if (validPhoneNumber === null) { - return false - } - return profile.email || profile.external_id || profile.phone_number - }) - const listId = payload[0]?.list_id - const importJobPayload = createImportJobPayload(payload, listId) - return sendImportJobRequest(request, importJobPayload) + return sendBatchedProfileImportJobRequest(request, payload) } } diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index 2d9f2365b9..4ee4a5e020 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -3,7 +3,9 @@ import { RequestClient, DynamicFieldResponse, IntegrationError, - PayloadValidationError + PayloadValidationError, + MultiStatusResponse, + HTTPError } from '@segment/actions-core' import { API_URL, REVISION_DATE } from './config' import { Settings } from './generated-types' @@ -20,10 +22,16 @@ import { UnsubscribeProfile, UnsubscribeEventData, GroupedProfiles, - AdditionalAttributes + AdditionalAttributes, + KlaviyoAPIErrorResponse, + KlaviyoProfile } from './types' import { Payload } from './upsertProfile/generated-types' import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber' +import { profileBulkImportRegex } from './properties' +import { Payload as AddProfileToListPayload } from './addProfileToList/generated-types' +import { JSONLikeObject } from '@segment/actions-core' +import { ActionDestinationErrorResponseType } from '@segment/actions-core/destination-kittypes' const phoneUtil = PhoneNumberUtil.getInstance() @@ -148,8 +156,22 @@ export const createImportJobPayload = (profiles: Payload[], listId?: string): { } }) +export const constructBulkProfileImportPayload = ( + profiles: KlaviyoProfile[], + listId?: string +): { data: ImportJobPayload } => ({ + data: { + type: 'profile-bulk-import-job', + attributes: { + profiles: { + data: profiles + } + }, + ...(listId ? { relationships: { lists: { data: [{ type: 'list', id: listId }] } } } : {}) + } +}) export const sendImportJobRequest = async (request: RequestClient, importJobPayload: { data: ImportJobPayload }) => { - return await request(`${API_URL}/profile-bulk-import-jobs/`, { + await request(`${API_URL}/profile-bulk-import-jobs/`, { method: 'POST', headers: { revision: '2023-10-15.pre' @@ -451,3 +473,139 @@ export function processPhoneNumber(initialPhoneNumber?: string, country_code?: s return phone_number } + +function handleKlaviyoAPIErrorResponse( + payloads: JSONLikeObject[], + response: any, + multiStatusResponse: MultiStatusResponse, + validPayloadIndicesBitmap: number[], + regex: RegExp +) { + if (response?.errors && Array.isArray(response.errors)) { + const invalidIndexSet = new Set() + response.errors.forEach((error: KlaviyoAPIErrorResponse) => { + const indexInOriginalPayload = getIndexFromErrorPointer(error.source.pointer, validPayloadIndicesBitmap, regex) + 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[], regex: RegExp) { + const match = regex.exec(pointer) + if (match && match[1]) { + const index = parseInt(match[1], 10) + return validPayloadIndicesBitmap[index] !== undefined ? validPayloadIndicesBitmap[index] : -1 + } + return -1 +} + +function validateAndConstructProfilePayload(payload: AddProfileToListPayload): { + validPayload?: JSONLikeObject + error?: ActionDestinationErrorResponseType +} { + const { phone_number, email, external_id } = payload + const response: { validPayload?: JSONLikeObject; error?: ActionDestinationErrorResponseType } = {} + if (!email && !phone_number && !external_id) { + response.error = { + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'One of External ID, Phone Number or Email is required.' + } + return response + } + + if (phone_number) { + const validPhoneNumber = validateAndConvertPhoneNumber(phone_number, payload.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.phone_number = validPhoneNumber + delete payload.country_code + } + + const { list_id, enable_batching, batch_size, country_code, ...attributes } = payload + + response.validPayload = { type: 'profile', attributes: attributes as JSONLikeObject } + return response +} +function validateAndPrepareBatchedProfileImportPayloads( + payloads: AddProfileToListPayload[], + multiStatusResponse: MultiStatusResponse +) { + const filteredPayloads: JSONLikeObject[] = [] + const validPayloadIndicesBitmap: number[] = [] + + payloads.forEach((payload, originalBatchIndex) => { + const { validPayload, error } = validateAndConstructProfilePayload(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 { filteredPayloads, validPayloadIndicesBitmap } +} + +export async function sendBatchedProfileImportJobRequest(request: RequestClient, payloads: AddProfileToListPayload[]) { + const multiStatusResponse = new MultiStatusResponse() + const { filteredPayloads, validPayloadIndicesBitmap } = validateAndPrepareBatchedProfileImportPayloads( + payloads, + multiStatusResponse + ) + + if (!filteredPayloads.length) { + return multiStatusResponse + } + const importJobPayload = constructBulkProfileImportPayload( + filteredPayloads as unknown as KlaviyoProfile[], + payloads[0]?.list_id + ) + try { + await sendImportJobRequest(request, importJobPayload) + } catch (err) { + if (err instanceof HTTPError) { + const errorResponse = await err?.response?.json() + handleKlaviyoAPIErrorResponse( + payloads as object as JSONLikeObject[], + errorResponse, + multiStatusResponse, + validPayloadIndicesBitmap, + profileBulkImportRegex + ) + } else { + throw err // Bubble up the error + } + } + return multiStatusResponse +} diff --git a/packages/destination-actions/src/destinations/klaviyo/properties.ts b/packages/destination-actions/src/destinations/klaviyo/properties.ts index f2f440a40b..11b5bb9ba5 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 profileBulkImportRegex = /\/data\/attributes\/profiles\/data\/(\d+)/ diff --git a/packages/destination-actions/src/destinations/klaviyo/types.ts b/packages/destination-actions/src/destinations/klaviyo/types.ts index fc645ad0c5..f3abf2eca0 100644 --- a/packages/destination-actions/src/destinations/klaviyo/types.ts +++ b/packages/destination-actions/src/destinations/klaviyo/types.ts @@ -219,3 +219,21 @@ 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 + } +} +export interface KlaviyoAPIErrorResponse { + errors: KlaviyoAPIError[] +} +export interface KlaviyoProfile { + type: string + attributes: ProfileAttributes +} From 3bcfe9e1c3c99adc5173efdec61c26a4ab1af7f5 Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Tue, 22 Oct 2024 18:45:12 +0530 Subject: [PATCH 2/9] Unit test case --- .../src/destinations/klaviyo/__tests__/multistatus.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts b/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts index 4fe943a97f..e8a702efdc 100644 --- a/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts +++ b/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts @@ -199,7 +199,7 @@ describe('MultiStatus', () => { list_id: '123' } }, - body: '{"id":"752f7ece-af20-44e0-aa3a-b13290d98e72","status":400,"code":"invalid","title":"Invalid input.","detail":"Invalid email address","source":{"pointer":"/data/attributes/profile/data/0/attributes/email"},"links":{},"meta":{}}' + body: '{"id":"752f7ece-af20-44e0-aa3a-b13290d98e72","status":400,"code":"invalid","title":"Invalid input.","detail":"Invalid email address","source":{"pointer":"/data/attributes/profiles/data/0/attributes/email"},"links":{},"meta":{}}' }) // The second event fails as Braze API reports an error From 3a5977c00a4003f0114afcbcd8cf626c60e4831d Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Tue, 29 Oct 2024 16:26:10 +0530 Subject: [PATCH 3/9] Worked on adding Email pre validation and handle klaviyo API error response --- .../klaviyo/__tests__/multistatus.test.ts | 48 ++++++++--- .../src/destinations/klaviyo/functions.ts | 79 +++++++------------ .../src/destinations/klaviyo/properties.ts | 2 + 3 files changed, 67 insertions(+), 62 deletions(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts b/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts index 2483e6e91a..6da0a38947 100644 --- a/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts +++ b/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts @@ -115,6 +115,15 @@ describe('MultiStatus', () => { properties: { list_id: '123' } + }), + //Event with invalid Email + createTestEvent({ + type: 'track', + timestamp, + properties: { + email: 'invalid_email@gmail..com', + list_id: '123' + } }) ] @@ -137,6 +146,14 @@ describe('MultiStatus', () => { errormessage: 'One of External ID, Phone Number or Email is required.', errorreporter: 'DESTINATION' }) + + // The third event fails as pre-request validation fails for having invalid email + expect(response[2]).toMatchObject({ + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'Email format is invalid.Please ensure it follows the standard format', + errorreporter: 'DESTINATION' + }) }) it('should successfully handle a batch of events with failure response from Klaviyo API', async () => { @@ -148,7 +165,7 @@ describe('MultiStatus', () => { status: 400, code: 'invalid', title: 'Invalid input.', - detail: 'Invalid email address', + detail: 'Invalid input', source: { pointer: '/data/attributes/profiles/data/0/attributes/email' }, @@ -165,7 +182,7 @@ describe('MultiStatus', () => { type: 'track', timestamp, properties: { - email: 'invalid_email', + email: 'gk@gmail.com', list_id: '123' } }), @@ -189,30 +206,35 @@ describe('MultiStatus', () => { // The first doesn't fail as there is no error reported by Braze API expect(response[0]).toMatchObject({ status: 400, - errortype: 'BAD_REQUEST', - errormessage: 'Invalid email address', + errormessage: 'Bad Request', sent: { - email: 'invalid_email', + email: 'gk@gmail.com', list_id: '123', properties: { - email: 'invalid_email', + email: 'gk@gmail.com', list_id: '123' } }, - body: '{"id":"752f7ece-af20-44e0-aa3a-b13290d98e72","status":400,"code":"invalid","title":"Invalid input.","detail":"Invalid email address","source":{"pointer":"/data/attributes/profiles/data/0/attributes/email"},"links":{},"meta":{}}' + body: '{"errors":[{"id":"752f7ece-af20-44e0-aa3a-b13290d98e72","status":400,"code":"invalid","title":"Invalid input.","detail":"Invalid input","source":{"pointer":"/data/attributes/profiles/data/0/attributes/email"},"links":{},"meta":{}}]}', + errortype: 'BAD_REQUEST', + errorreporter: 'DESTINATION' }) - // The second event fails as Braze API reports an error + // // The second event fails as Braze API reports an error expect(response[1]).toMatchObject({ - status: 429, + status: 400, + errormessage: 'Bad Request', sent: { list_id: '123', - external_id: 'Xi1234', properties: { - external_id: 'Xi1234' - } + external_id: 'Xi1234', + list_id: '123' + }, + external_id: 'Xi1234' }, - body: 'Retry' + body: '{"errors":[{"id":"752f7ece-af20-44e0-aa3a-b13290d98e72","status":400,"code":"invalid","title":"Invalid input.","detail":"Invalid input","source":{"pointer":"/data/attributes/profiles/data/0/attributes/email"},"links":{},"meta":{}}]}', + errortype: 'BAD_REQUEST', + errorreporter: 'DESTINATION' }) }) diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index bb3451ec40..4f8c925f53 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -23,12 +23,11 @@ import { UnsubscribeEventData, GroupedProfiles, AdditionalAttributes, - KlaviyoAPIErrorResponse, KlaviyoProfile } from './types' import { Payload } from './upsertProfile/generated-types' import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber' -import { profileBulkImportRegex } from './properties' +import { emailRegex } from './properties' import { Payload as AddProfileToListPayload } from './addProfileToList/generated-types' import { JSONLikeObject } from '@segment/actions-core' import { ActionDestinationErrorResponseType } from '@segment/actions-core/destination-kittypes' @@ -473,49 +472,22 @@ export function processPhoneNumber(initialPhoneNumber?: string, country_code?: s return phone_number } - -function handleKlaviyoAPIErrorResponse( +async function updateMultiStatusWithKlaviyoErrors( payloads: JSONLikeObject[], - response: any, + err: any, multiStatusResponse: MultiStatusResponse, - validPayloadIndicesBitmap: number[], - regex: RegExp + 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, regex) - if (indexInOriginalPayload !== -1 && !multiStatusResponse.isErrorResponseAtIndex(indexInOriginalPayload)) { - multiStatusResponse.setErrorResponseAtIndex(indexInOriginalPayload, { - status: error.status, - // errortype will be inferred from the error.response.status - errormessage: error.detail, - sent: payloads[indexInOriginalPayload], - body: JSON.stringify(error) - }) - invalidIndexSet.add(indexInOriginalPayload) - } + const errorResponse = await err?.response?.json() + payloads.forEach((payload, index) => { + multiStatusResponse.setErrorResponseAtIndex(validPayloadIndicesBitmap[index], { + status: err?.response?.status || 400, + // errortype will be inferred from status + errormessage: err?.response?.statusText, + sent: payload, + body: JSON.stringify(errorResponse) }) - - 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[], regex: RegExp) { - const match = regex.exec(pointer) - if (match && match[1]) { - const index = parseInt(match[1], 10) - return validPayloadIndicesBitmap[index] !== undefined ? validPayloadIndicesBitmap[index] : -1 - } - return -1 + }) } function validateAndConstructProfilePayload(payload: AddProfileToListPayload): { @@ -546,12 +518,23 @@ function validateAndConstructProfilePayload(payload: AddProfileToListPayload): { payload.phone_number = validPhoneNumber delete payload.country_code } + if (email) { + if (!emailRegex.test(email)) { + response.error = { + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'Email format is invalid.Please ensure it follows the standard format' + } + return response + } + } const { list_id, enable_batching, batch_size, country_code, ...attributes } = payload response.validPayload = { type: 'profile', attributes: attributes as JSONLikeObject } return response } + function validateAndPrepareBatchedProfileImportPayloads( payloads: AddProfileToListPayload[], multiStatusResponse: MultiStatusResponse @@ -593,18 +576,16 @@ export async function sendBatchedProfileImportJobRequest(request: RequestClient, ) try { await sendImportJobRequest(request, importJobPayload) - } catch (err) { - if (err instanceof HTTPError) { - const errorResponse = await err?.response?.json() - handleKlaviyoAPIErrorResponse( + } catch (error) { + if (error instanceof HTTPError) { + await updateMultiStatusWithKlaviyoErrors( payloads as object as JSONLikeObject[], - errorResponse, + error, multiStatusResponse, - validPayloadIndicesBitmap, - profileBulkImportRegex + validPayloadIndicesBitmap ) } else { - throw err // Bubble up the error + throw error // Bubble up the error } } return multiStatusResponse diff --git a/packages/destination-actions/src/destinations/klaviyo/properties.ts b/packages/destination-actions/src/destinations/klaviyo/properties.ts index 11b5bb9ba5..5133664894 100644 --- a/packages/destination-actions/src/destinations/klaviyo/properties.ts +++ b/packages/destination-actions/src/destinations/klaviyo/properties.ts @@ -163,3 +163,5 @@ export const country_code: InputField = { } } export const profileBulkImportRegex = /\/data\/attributes\/profiles\/data\/(\d+)/ +export const emailRegex = + /^(?!.*\.\.)(?!.*\.$)(?!.*@{2,})(?!.*@\.)(?!.*\.$)(?=.{1,256})[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ From 11a10400363066fb62656da2ba24362350781af7 Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Mon, 4 Nov 2024 19:15:29 +0530 Subject: [PATCH 4/9] documentation js --- .../klaviyo/__tests__/multistatus.test.ts | 6 ++---- .../src/destinations/klaviyo/functions.ts | 12 ++++++++++++ .../src/destinations/klaviyo/properties.ts | 1 - 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts b/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts index 6da0a38947..fc8e4e4640 100644 --- a/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts +++ b/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts @@ -1,11 +1,11 @@ import { SegmentEvent, createTestEvent, createTestIntegration } from '@segment/actions-core' import nock from 'nock' import { API_URL } from '../config' -import Braze from '../index' +import Klaviyo from '../index' beforeEach(() => nock.cleanAll()) -const testDestination = createTestIntegration(Braze) +const testDestination = createTestIntegration(Klaviyo) const settings = { api_key: 'my-api-key' @@ -203,7 +203,6 @@ describe('MultiStatus', () => { mapping }) - // The first doesn't fail as there is no error reported by Braze API expect(response[0]).toMatchObject({ status: 400, errormessage: 'Bad Request', @@ -220,7 +219,6 @@ describe('MultiStatus', () => { errorreporter: 'DESTINATION' }) - // // The second event fails as Braze API reports an error expect(response[1]).toMatchObject({ status: 400, errormessage: 'Bad Request', diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index 4f8c925f53..19886bd833 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -472,6 +472,18 @@ export function processPhoneNumber(initialPhoneNumber?: string, country_code?: s return phone_number } +/** + * Updates the multi-status response with error information from Klaviyo for a batch of payloads. + * + * This function is designed to handle errors returned by a bulk operation in Klaviyo. + * It marks the entire batch as failed since granular retries are not supported in bulk operations. + * + * @param {JSONLikeObject[]} payloads - An array of payloads that were sent in the bulk operation. + * @param {any} err - The error object received from the Klaviyo API response. + * @param {MultiStatusResponse} multiStatusResponse - The object responsible for storing the status of each payload. + * @param {number[]} validPayloadIndicesBitmap - An array of indices indicating which payloads were valid. + * + */ async function updateMultiStatusWithKlaviyoErrors( payloads: JSONLikeObject[], err: any, diff --git a/packages/destination-actions/src/destinations/klaviyo/properties.ts b/packages/destination-actions/src/destinations/klaviyo/properties.ts index 5133664894..618899926f 100644 --- a/packages/destination-actions/src/destinations/klaviyo/properties.ts +++ b/packages/destination-actions/src/destinations/klaviyo/properties.ts @@ -162,6 +162,5 @@ export const country_code: InputField = { ] } } -export const profileBulkImportRegex = /\/data\/attributes\/profiles\/data\/(\d+)/ export const emailRegex = /^(?!.*\.\.)(?!.*\.$)(?!.*@{2,})(?!.*@\.)(?!.*\.$)(?=.{1,256})[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ From 33bdd5a6813b93759e5bdb005c8b7200f3317662 Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Thu, 7 Nov 2024 14:16:23 +0530 Subject: [PATCH 5/9] email validation --- .../__tests__/__snapshots__/snapshot.test.ts.snap | 2 +- .../klaviyo/__tests__/multistatus.test.ts | 4 ++-- .../src/destinations/klaviyo/functions.ts | 11 ----------- .../src/destinations/klaviyo/properties.ts | 5 ++--- 4 files changed, 5 insertions(+), 17 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 fb456e1b0c..5ada4043b7 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 @@ -4,7 +4,7 @@ exports[`Testing snapshot for actions-klaviyo destination: addProfileToList acti Object { "data": Object { "attributes": Object { - "email": "mudwoz@zo.ad", + "email": "odewaclon@fesijeho.hr", "external_id": "E3nNk", "first_name": "E3nNk", "image": "E3nNk", diff --git a/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts b/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts index fc8e4e4640..8362c7231c 100644 --- a/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts +++ b/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts @@ -151,8 +151,8 @@ describe('MultiStatus', () => { expect(response[2]).toMatchObject({ status: 400, errortype: 'PAYLOAD_VALIDATION_FAILED', - errormessage: 'Email format is invalid.Please ensure it follows the standard format', - errorreporter: 'DESTINATION' + errormessage: 'Email must be a valid email address string but it was not.', + errorreporter: 'INTEGRATIONS' }) }) diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index 19886bd833..79cc8355c2 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -27,7 +27,6 @@ import { } from './types' import { Payload } from './upsertProfile/generated-types' import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber' -import { emailRegex } from './properties' import { Payload as AddProfileToListPayload } from './addProfileToList/generated-types' import { JSONLikeObject } from '@segment/actions-core' import { ActionDestinationErrorResponseType } from '@segment/actions-core/destination-kittypes' @@ -530,16 +529,6 @@ function validateAndConstructProfilePayload(payload: AddProfileToListPayload): { payload.phone_number = validPhoneNumber delete payload.country_code } - if (email) { - if (!emailRegex.test(email)) { - response.error = { - status: 400, - errortype: 'PAYLOAD_VALIDATION_FAILED', - errormessage: 'Email format is invalid.Please ensure it follows the standard format' - } - return response - } - } const { list_id, enable_batching, batch_size, country_code, ...attributes } = payload diff --git a/packages/destination-actions/src/destinations/klaviyo/properties.ts b/packages/destination-actions/src/destinations/klaviyo/properties.ts index 618899926f..a1d2f8ef1c 100644 --- a/packages/destination-actions/src/destinations/klaviyo/properties.ts +++ b/packages/destination-actions/src/destinations/klaviyo/properties.ts @@ -18,7 +18,8 @@ export const email: InputField = { type: 'string', default: { '@path': '$.context.traits.email' - } + }, + format: 'email' } export const external_id: InputField = { @@ -162,5 +163,3 @@ export const country_code: InputField = { ] } } -export const emailRegex = - /^(?!.*\.\.)(?!.*\.$)(?!.*@{2,})(?!.*@\.)(?!.*\.$)(?=.{1,256})[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ From 61374eeec62613d4f29dcb3a803fcc8bf1f5e9f9 Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Mon, 18 Nov 2024 20:59:14 +0530 Subject: [PATCH 6/9] updateMultiStatusWithSuccessData after klaviyo API response --- .../klaviyo/__tests__/multistatus.test.ts | 44 +++++++++++++++---- .../src/destinations/klaviyo/functions.ts | 25 ++++++++--- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts b/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts index 8362c7231c..7d78dc6bbe 100644 --- a/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts +++ b/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts @@ -48,9 +48,35 @@ describe('MultiStatus', () => { '@path': '$.properties.external_id' } } + const responseData = { + data: { + type: 'profile-bulk-import-job', + id: 'ZXo4dlZ1X21haW50ZW5hbmNlLnRNMm5PYy4xNzMxOTQxODcyLmhibkMzZw', + attributes: { + status: 'queued', + created_at: '2024-11-18T14:57:52.454354+00:00', + total_count: 1, + completed_count: 0, + failed_count: 0, + completed_at: null, + expires_at: '2024-11-25T14:57:52.454354+00:00', + started_at: null + }, + relationships: { + lists: { + data: [ + { + type: 'list', + id: 'WNyUbB' + } + ] + } + } + } + } it("should successfully handle those payload where phone_number is invalid and couldn't be converted to E164 format", async () => { - nock(API_URL).post('/profile-bulk-import-jobs/').reply(202, {}) + nock(API_URL).post('/profile-bulk-import-jobs/').reply(202, responseData) const events: SegmentEvent[] = [ // Event with invalid phone_number @@ -61,7 +87,7 @@ describe('MultiStatus', () => { country_code: 'IN', phone_number: '701271', email: 'valid@gmail.com', - list_id: '123' + list_id: 'WNyUbB' } }), // Valid Event @@ -70,7 +96,7 @@ describe('MultiStatus', () => { timestamp, properties: { email: 'valid@gmail.com', - list_id: '123' + list_id: 'WNyUbB' } }) ] @@ -92,12 +118,12 @@ describe('MultiStatus', () => { // The Second event doesn't fail as there is no error reported by Klaviyo API expect(response[1]).toMatchObject({ status: 200, - body: 'success' + body: JSON.stringify(responseData) }) }) it('should successfully handle a batch of events with complete success response from Klaviyo API', async () => { - nock(API_URL).post('/profile-bulk-import-jobs/').reply(200, { success: true }) + nock(API_URL).post('/profile-bulk-import-jobs/').reply(200, responseData) const events: SegmentEvent[] = [ // Valid Event @@ -106,14 +132,14 @@ describe('MultiStatus', () => { timestamp, properties: { email: 'valid@gmail.com', - list_id: '123' + list_id: 'WNyUbB' } }), // Event without any user identifier createTestEvent({ type: 'track', properties: { - list_id: '123' + list_id: 'WNyUbB' } }), //Event with invalid Email @@ -122,7 +148,7 @@ describe('MultiStatus', () => { timestamp, properties: { email: 'invalid_email@gmail..com', - list_id: '123' + list_id: 'WNyUbB' } }) ] @@ -136,7 +162,7 @@ describe('MultiStatus', () => { // The first event doesn't fail as there is no error reported by Klaviyo API expect(response[0]).toMatchObject({ status: 200, - body: 'success' + body: JSON.stringify(responseData) }) // The second event fails as pre-request validation fails for not having any user identifier diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index 79cc8355c2..a1a61082b1 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -169,7 +169,7 @@ export const constructBulkProfileImportPayload = ( } }) export const sendImportJobRequest = async (request: RequestClient, importJobPayload: { data: ImportJobPayload }) => { - await request(`${API_URL}/profile-bulk-import-jobs/`, { + return await request(`${API_URL}/profile-bulk-import-jobs/`, { method: 'POST', headers: { revision: '2023-10-15.pre' @@ -550,11 +550,6 @@ function validateAndPrepareBatchedProfileImportPayloads( } else { filteredPayloads.push(validPayload as JSONLikeObject) validPayloadIndicesBitmap.push(originalBatchIndex) - multiStatusResponse.setSuccessResponseAtIndex(originalBatchIndex, { - status: 200, - sent: validPayload as JSONLikeObject, - body: 'success' - }) } }) @@ -576,7 +571,8 @@ export async function sendBatchedProfileImportJobRequest(request: RequestClient, payloads[0]?.list_id ) try { - await sendImportJobRequest(request, importJobPayload) + const response = await sendImportJobRequest(request, importJobPayload) + updateMultiStatusWithSuccessData(filteredPayloads, validPayloadIndicesBitmap, multiStatusResponse, response) } catch (error) { if (error instanceof HTTPError) { await updateMultiStatusWithKlaviyoErrors( @@ -591,3 +587,18 @@ export async function sendBatchedProfileImportJobRequest(request: RequestClient, } return multiStatusResponse } + +export function updateMultiStatusWithSuccessData( + filteredPayloads: JSONLikeObject[], + validPayloadIndicesBitmap: number[], + multiStatusResponse: MultiStatusResponse, + response: any +) { + filteredPayloads.forEach((payload, index) => { + multiStatusResponse.setSuccessResponseAtIndex(validPayloadIndicesBitmap[index], { + status: 200, + sent: payload, + body: JSON.stringify(response?.data) + }) + }) +} From 55949b0101e9915638a173feaf170fe148ca59a7 Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Fri, 6 Dec 2024 14:52:06 +0530 Subject: [PATCH 7/9] updated errorreporter for failure --- .../destinations/klaviyo/__tests__/multistatus.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts b/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts index 7d78dc6bbe..2242f71525 100644 --- a/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts +++ b/packages/destination-actions/src/destinations/klaviyo/__tests__/multistatus.test.ts @@ -112,7 +112,7 @@ describe('MultiStatus', () => { status: 400, errortype: 'PAYLOAD_VALIDATION_FAILED', errormessage: 'Phone number could not be converted to E.164 format.', - errorreporter: 'DESTINATION' + errorreporter: 'INTEGRATIONS' }) // The Second event doesn't fail as there is no error reported by Klaviyo API @@ -170,7 +170,7 @@ describe('MultiStatus', () => { status: 400, errortype: 'PAYLOAD_VALIDATION_FAILED', errormessage: 'One of External ID, Phone Number or Email is required.', - errorreporter: 'DESTINATION' + errorreporter: 'INTEGRATIONS' }) // The third event fails as pre-request validation fails for having invalid email @@ -292,7 +292,7 @@ describe('MultiStatus', () => { status: 400, errortype: 'PAYLOAD_VALIDATION_FAILED', errormessage: 'Phone number could not be converted to E.164 format.', - errorreporter: 'DESTINATION' + errorreporter: 'INTEGRATIONS' }) // The second event fails as pre-request validation fails for not having any user identifier @@ -300,7 +300,7 @@ describe('MultiStatus', () => { status: 400, errortype: 'PAYLOAD_VALIDATION_FAILED', errormessage: 'One of External ID, Phone Number or Email is required.', - errorreporter: 'DESTINATION' + errorreporter: 'INTEGRATIONS' }) }) }) From 7cc57d12be26dd153709fb1fdb6b4327212e6719 Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Thu, 12 Dec 2024 20:13:18 +0530 Subject: [PATCH 8/9] updated with main --- .../sendgrid-audiences/syncAudience/fields.ts | 34 ++++++++--------- .../sendEmail/__tests__/index.test.ts | 1 + .../__tests__/template-variables.test.ts | 2 +- .../sendgrid/sendEmail/dynamic-fields.ts | 17 ++++++--- .../forwardAudienceEvent/functions.ts | 2 +- .../common_fields.ts | 5 +-- .../twilio-messaging/sendMessage/constants.ts | 6 +-- .../sendMessage/dynamic-fields.ts | 37 +++++++++---------- .../twilio-messaging/sendMessage/fields.ts | 12 ++---- .../twilio-messaging/sendMessage/index.ts | 2 +- .../twilio-messaging/sendMessage/utils.ts | 8 +--- 11 files changed, 59 insertions(+), 67 deletions(-) diff --git a/packages/destination-actions/src/destinations/sendgrid-audiences/syncAudience/fields.ts b/packages/destination-actions/src/destinations/sendgrid-audiences/syncAudience/fields.ts index 913be9e1fb..d0397ff12d 100644 --- a/packages/destination-actions/src/destinations/sendgrid-audiences/syncAudience/fields.ts +++ b/packages/destination-actions/src/destinations/sendgrid-audiences/syncAudience/fields.ts @@ -128,7 +128,7 @@ export const fields: Record = { description: `The contact's city.`, type: 'string' }, - state_province_region: { + state_province_region: { label: 'State/Province/Region', description: `The contact's state, province, or region.`, type: 'string' @@ -145,61 +145,61 @@ export const fields: Record = { } }, default: { - first_name: { + first_name: { '@if': { exists: { '@path': '$.traits.first_name' }, then: { '@path': '$.traits.first_name' }, else: { '@path': '$.properties.first_name' } - } + } }, - last_name: { + last_name: { '@if': { exists: { '@path': '$.traits.last_name' }, then: { '@path': '$.traits.last_name' }, else: { '@path': '$.properties.last_name' } - } + } }, - address_line_1: { + address_line_1: { '@if': { exists: { '@path': '$.traits.street' }, then: { '@path': '$.traits.street' }, else: { '@path': '$.properties.street' } - } + } }, - address_line_2: { + address_line_2: { '@if': { exists: { '@path': '$.traits.address_line_2' }, then: { '@path': '$.traits.address_line_2' }, else: { '@path': '$.properties.address_line_2' } - } + } }, - city: { + city: { '@if': { exists: { '@path': '$.traits.city' }, then: { '@path': '$.traits.city' }, else: { '@path': '$.properties.city' } - } + } }, - state_province_region: { + state_province_region: { '@if': { exists: { '@path': '$.traits.state' }, then: { '@path': '$.traits.state' }, else: { '@path': '$.properties.state' } - } + } }, - country: { + country: { '@if': { exists: { '@path': '$.traits.country' }, then: { '@path': '$.traits.country' }, else: { '@path': '$.properties.country' } - } + } }, - postal_code: { + postal_code: { '@if': { exists: { '@path': '$.traits.postal_code' }, then: { '@path': '$.traits.postal_code' }, else: { '@path': '$.properties.postal_code' } - } + } } } }, diff --git a/packages/destination-actions/src/destinations/sendgrid/sendEmail/__tests__/index.test.ts b/packages/destination-actions/src/destinations/sendgrid/sendEmail/__tests__/index.test.ts index 19cadf9b7f..bebd18185e 100644 --- a/packages/destination-actions/src/destinations/sendgrid/sendEmail/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/sendgrid/sendEmail/__tests__/index.test.ts @@ -184,6 +184,7 @@ describe('Sendgrid.sendEmail', () => { expect(parseIntFromString('')).toBe(undefined) }) + it('send_at date should be 10 char epoch', async () => { const date = '2024-01-08T13:52:50.212Z' const epoch = toUnixTS(date) diff --git a/packages/destination-actions/src/destinations/sendgrid/sendEmail/__tests__/template-variables.test.ts b/packages/destination-actions/src/destinations/sendgrid/sendEmail/__tests__/template-variables.test.ts index 9f6d6256b3..7064fbb60b 100644 --- a/packages/destination-actions/src/destinations/sendgrid/sendEmail/__tests__/template-variables.test.ts +++ b/packages/destination-actions/src/destinations/sendgrid/sendEmail/__tests__/template-variables.test.ts @@ -180,7 +180,7 @@ describe('Sendgrid.sendEmail', () => { expect(tokens).toMatchObject([ 'user', 'name', - 'businessName', + "businessName", 'login_url', 'supportPhone', 'scoreOne', diff --git a/packages/destination-actions/src/destinations/sendgrid/sendEmail/dynamic-fields.ts b/packages/destination-actions/src/destinations/sendgrid/sendEmail/dynamic-fields.ts index 57ec3a2ceb..216aec3553 100644 --- a/packages/destination-actions/src/destinations/sendgrid/sendEmail/dynamic-fields.ts +++ b/packages/destination-actions/src/destinations/sendgrid/sendEmail/dynamic-fields.ts @@ -45,15 +45,20 @@ export function extractVariables(content: string | undefined): string[] { const removeIfStartsWith = ['if', 'unless', 'and', 'or', 'equals', 'notEquals', 'lessThan', 'greaterThan', 'each'] const removeIfMatch = ['else', 'if', 'this', 'insert', 'default', 'length', 'formatDate'] - const regex1 = /{{(.*?)}}/g // matches handlebar expressions. e.g. {{root.user.username | default: "Unknown"}} + const regex1 = /{{(.*?)}}/g // matches handlebar expressions. e.g. {{root.user.username | default: "Unknown"}} const regex2 = /(["']).*?\1/g // removes anything between quotes. e.g. {{root.user.username | default: }} const regex3 = /[#/]?[\w.]+/g // matches words only. e.g. root.user.username , default - const words = - [...new Set([...content.matchAll(regex1)].map((match) => match[1]))] - .map((word) => word.replace(regex2, '').trim()) - .join(' ') - .match(regex3) ?? [] + const words = [ + ...new Set( + [...content.matchAll(regex1)].map(match => match[1]) + ) + ] + .map(word => + word.replace(regex2, '').trim() + ) + .join(' ') + .match(regex3) ?? [] const variables = [ ...new Set( diff --git a/packages/destination-actions/src/destinations/stackadapt-audiences/forwardAudienceEvent/functions.ts b/packages/destination-actions/src/destinations/stackadapt-audiences/forwardAudienceEvent/functions.ts index 979561bd52..47fc4a85d8 100644 --- a/packages/destination-actions/src/destinations/stackadapt-audiences/forwardAudienceEvent/functions.ts +++ b/packages/destination-actions/src/destinations/stackadapt-audiences/forwardAudienceEvent/functions.ts @@ -83,4 +83,4 @@ export async function performForwardAudienceEvents(request: RequestClient, event return await request(GQL_ENDPOINT, { body: JSON.stringify({ query: mutation }) }) -} +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/tiktok-offline-conversions/common_fields.ts b/packages/destination-actions/src/destinations/tiktok-offline-conversions/common_fields.ts index 63d7d2378a..cdc9990a7e 100644 --- a/packages/destination-actions/src/destinations/tiktok-offline-conversions/common_fields.ts +++ b/packages/destination-actions/src/destinations/tiktok-offline-conversions/common_fields.ts @@ -200,10 +200,7 @@ export const commonFields: Record = { description: 'Type of the product item. When the `content_id` in the `Contents` field is specified as a `sku_id`, set this field to `product`. When the `content_id` in the `Contents` field is specified as an `item_group_id`, set this field to `product_group`.', type: 'string', - choices: [ - { label: 'product', value: 'product' }, - { label: 'product_group', value: 'product_group' } - ], + choices: [ { label: 'product', value: 'product' }, { label: 'product_group', value: 'product_group' }], default: 'product' }, currency: { diff --git a/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/constants.ts b/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/constants.ts index f92f1420cb..c12b638632 100644 --- a/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/constants.ts +++ b/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/constants.ts @@ -108,12 +108,10 @@ export const ALL_CONTENT_TYPES = { ...INLINE_CONTENT_TYPES } -export const CONTENT_TYPE_FRIENDLY_NAMES_SUPPORTING_MEDIA = Object.values(ALL_CONTENT_TYPES) - .filter((t) => t.supports_media) - .map((t) => t.friendly_name) +export const CONTENT_TYPE_FRIENDLY_NAMES_SUPPORTING_MEDIA = Object.values(ALL_CONTENT_TYPES).filter((t) => t.supports_media).map((t) => t.friendly_name) export const SENDER_TYPE = { PHONE_NUMBER: 'Phone number', MESSENGER_SENDER_ID: 'Messenger Sender ID', MESSAGING_SERVICE: 'Messaging Service' -} +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/dynamic-fields.ts b/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/dynamic-fields.ts index 4e327a27c3..b19c6693ed 100644 --- a/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/dynamic-fields.ts +++ b/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/dynamic-fields.ts @@ -68,14 +68,15 @@ export async function dynamicSenderType(payload: Payload): Promise { + interface PhoneNumResponseType { data: { incoming_phone_numbers: Array<{ @@ -115,11 +117,11 @@ export async function dynamicFromPhoneNumber( const numbers: string[] = [] const supportsShortCodes = channel === CHANNELS.SMS || channel === CHANNELS.MMS - if (channel === CHANNELS.MESSENGER) { + if(channel === CHANNELS.MESSENGER) { return createErrorResponse("Use 'From Messenger Sender ID' field for specifying the sender ID.") } - if (supportsShortCodes) { + if(supportsShortCodes) { const shortCodeResp = await getData( request, GET_INCOMING_SHORT_CODES_URL.replace(ACCOUNT_SID_TOKEN, settings.accountSID) @@ -128,9 +130,9 @@ export async function dynamicFromPhoneNumber( if (isErrorResponse(shortCodeResp)) { return shortCodeResp } - - const shortCodes: string[] = shortCodeResp.data?.short_codes - .filter((s) => (channel === CHANNELS.MMS && s.sms_url !== null) || channel === CHANNELS.SMS) + + const shortCodes: string[] = (shortCodeResp.data?.short_codes + .filter((s) => (channel === CHANNELS.MMS && s.sms_url !== null) || channel === CHANNELS.SMS)) .map((s) => s.short_code) numbers.push(...shortCodes) @@ -153,20 +155,17 @@ export async function dynamicFromPhoneNumber( numbers.sort((a, b) => a.length - b.length || a.localeCompare(b)) if (numbers.length === 0) { - return createErrorResponse( - `No phone numbers ${supportsShortCodes ? 'or short codes' : ''} found. Please create a phone number ${ - supportsShortCodes ? 'or short code' : '' - } in your Twilio account.` - ) + return createErrorResponse(`No phone numbers ${supportsShortCodes ? 'or short codes' : ''} found. Please create a phone number ${supportsShortCodes ? 'or short code' : ''} in your Twilio account.`) } return { - choices: numbers.map((n) => { - return { - label: n, - value: n - } - }) + choices: numbers + .map((n) => { + return { + label: n, + value: n + } + }) } } @@ -217,7 +216,7 @@ export async function dynamicContentTemplateType(payload: Payload): Promise t.supported_channels.includes(channel as Channel)) .map((t) => ({ label: t.friendly_name, diff --git a/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/fields.ts b/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/fields.ts index ba408250f3..1cd8bb58f1 100644 --- a/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/fields.ts +++ b/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/fields.ts @@ -18,16 +18,14 @@ export const fields: Record = { }, senderType: { label: 'Sender Type', - description: - "The Sender type to use for the message. Depending on the selected 'Channel' this can be a phone number, messaging service, or Messenger sender ID.", + description: "The Sender type to use for the message. Depending on the selected 'Channel' this can be a phone number, messaging service, or Messenger sender ID.", type: 'string', required: true, dynamic: true }, contentTemplateType: { label: 'Content Template Type', - description: - 'The Content Template type to use for the message. Selecting "Inline" will allow you to define the message body directly. For all other options a Content Template must be pre-defined in Twilio.', + description: 'The Content Template type to use for the message. Selecting "Inline" will allow you to define the message body directly. For all other options a Content Template must be pre-defined in Twilio.', type: 'string', required: true, dynamic: true @@ -68,8 +66,7 @@ export const fields: Record = { }, fromPhoneNumber: { label: 'From Phone Number', - description: - "The Twilio phone number (E.164 format) or Short Code. If not in the dropdown, enter it directly. Please ensure the number supports the selected 'Channel' type.", + description: "The Twilio phone number (E.164 format) or Short Code. If not in the dropdown, enter it directly. Please ensure the number supports the selected 'Channel' type.", type: 'string', dynamic: true, required: false, @@ -87,8 +84,7 @@ export const fields: Record = { }, fromMessengerSenderId: { label: 'From Messenger Sender ID', - description: - 'The unique identifier for your Facebook Page, used to send messages via Messenger. You can find this in your Facebook Page settings.', + description: "The unique identifier for your Facebook Page, used to send messages via Messenger. You can find this in your Facebook Page settings.", type: 'string', required: false, default: undefined, diff --git a/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/index.ts b/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/index.ts index 542732ca05..feda0021df 100644 --- a/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/index.ts +++ b/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/index.ts @@ -44,7 +44,7 @@ const action: ActionDefinition = { } } }, - perform: async (request, { payload, settings }) => { + perform: async (request, { payload, settings }) => { return await send(request, payload, settings) } } diff --git a/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/utils.ts b/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/utils.ts index b5605630d7..6018ac3fdd 100644 --- a/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/utils.ts +++ b/packages/destination-actions/src/destinations/twilio-messaging/sendMessage/utils.ts @@ -40,9 +40,7 @@ export async function send(request: RequestClient, payload: Payload, settings: S case 'MMS': { toPhoneNumber = toPhoneNumber?.trim() ?? '' if (!(E164_REGEX.test(toPhoneNumber) || TWILIO_SHORT_CODE_REGEX.test(toPhoneNumber))) { - throw new PayloadValidationError( - "'To' field should be a valid phone number in E.164 format or a Twilio Short Code" - ) + throw new PayloadValidationError("'To' field should be a valid phone number in E.164 format or a Twilio Short Code"); } return toPhoneNumber } @@ -86,9 +84,7 @@ export async function send(request: RequestClient, payload: Payload, settings: S if (senderType === SENDER_TYPE.MESSENGER_SENDER_ID) { fromMessengerSenderId = fromMessengerSenderId?.trim() if (!fromMessengerSenderId) { - throw new PayloadValidationError( - "'From Messenger Sender ID' field is required when sending from a Messenger Sender ID." - ) + throw new PayloadValidationError("'From Messenger Sender ID' field is required when sending from a Messenger Sender ID.") } return { From: `messenger:${fromMessengerSenderId}` } } From 5c3ab4df40331eb1d35373f413a226520dbc5f22 Mon Sep 17 00:00:00 2001 From: Gaurav Kochar Date: Thu, 12 Dec 2024 20:15:00 +0530 Subject: [PATCH 9/9] updated with main --- .../src/destinations/sendgrid/sendEmail/utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/destination-actions/src/destinations/sendgrid/sendEmail/utils.ts b/packages/destination-actions/src/destinations/sendgrid/sendEmail/utils.ts index 969918225f..a9671a69cf 100644 --- a/packages/destination-actions/src/destinations/sendgrid/sendEmail/utils.ts +++ b/packages/destination-actions/src/destinations/sendgrid/sendEmail/utils.ts @@ -108,11 +108,11 @@ function validate(payload: Payload) { } }) - if (typeof payload.cc === 'object' && Object.keys(payload.cc).length === 0) { + if(typeof payload.cc === 'object' && Object.keys(payload.cc).length === 0){ delete payload.cc } - if (typeof payload.bcc === 'object' && Object.keys(payload.bcc).length === 0) { + if(typeof payload.bcc === 'object' && Object.keys(payload.bcc).length === 0){ delete payload.bcc } @@ -134,4 +134,4 @@ function validate(payload: Payload) { } return -} +} \ No newline at end of file