Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SFMC] Add Multistatus Support for SFMC #2639

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
import { SegmentEvent, createTestEvent, createTestIntegration } from '@segment/actions-core'
import nock from 'nock'
import sfmc from '../index'
import { Settings } from '../generated-types'

beforeEach(() => nock.cleanAll())

const testDestination = createTestIntegration(sfmc)
const settings: Settings = {
subdomain: 'test123',
client_id: 'test123',
client_secret: 'test123',
account_id: 'test123'
}
const requestUrl = `https://${settings.subdomain}.rest.marketingcloudapis.com/hub/v1/dataevents/1234567890/rowset`
const mapping = {
id: { '@path': '$.properties.id' },
keys: { '@path': '$.properties.keys' },
values: { '@path': '$.properties.values' }
}

describe('Multistatus', () => {
describe('dataExtension', () => {
it('should successfully handle a batch of events with complete success response from SFMC API', async () => {
nock(requestUrl).post('').reply(200, {})

const events: SegmentEvent[] = [
// Valid Event
createTestEvent({
type: 'track',
userId: 'harry-1',
properties: {
id: '1234567890',
keys: {
id: 'HS1'
},
values: {
name: 'Harry Styles'
}
}
}),
// Valid Event
createTestEvent({
type: 'track',
userId: 'harry-1',
properties: {
id: '1234567890',
keys: {
id: 'HS1'
},
values: {
name: 'Harry Styles'
}
}
})
]

const response = await testDestination.executeBatch('dataExtension', {
events,
settings,
mapping
})

expect(response[0]).toMatchObject({
status: 200,
body: {}
})

expect(response[1]).toMatchObject({
status: 200,
body: {}
})
})
it('should handle the case where both key and id are missing', async () => {
const events: SegmentEvent[] = [
createTestEvent({
type: 'track',
userId: 'harry-1',
properties: {
keys: {
id: 'HS1'
},
values: {
name: 'Harry Styles'
}
}
}),
createTestEvent({
type: 'track',
userId: 'harry-2',
properties: {
keys: {
id: 'HP1'
},
values: {
name: 'Harry Potter'
}
}
}) // No key and id provided
]

const response = await testDestination.executeBatch('dataExtension', {
events,
settings,
mapping
})

expect(response[0]).toMatchObject({
status: 400,
errortype: 'PAYLOAD_VALIDATION_FAILED',
errormessage:
'In order to send an event to a data extension either Data Extension ID or Data Extension Key must be defined.',
errorreporter: 'INTEGRATIONS'
})

expect(response[1]).toMatchObject({
status: 400,
errortype: 'PAYLOAD_VALIDATION_FAILED',
errormessage:
'In order to send an event to a data extension either Data Extension ID or Data Extension Key must be defined.',
errorreporter: 'INTEGRATIONS'
})
})
it('should handle multistatus errors when some events fail with specific error messages', async () => {
const errorResponse = {
status: 400,
message: 'Invalid keys for ID: HS1',
additionalErrors: [
{
message: 'No record found for ID: HS2'
}
]
}

nock(requestUrl).post('').reply(400, errorResponse)

const events: SegmentEvent[] = [
createTestEvent({
type: 'track',
userId: 'harry-1',
properties: {
id: '1234567890',
keys: {
id: 'HS1' // Valid key
},
values: {
name: 'Harry Styles'
}
}
}),
createTestEvent({
type: 'track',
userId: 'harry-2',
properties: {
id: '1234567890',
keys: {
id: 'HS2' // Invalid key
},
values: {
name: 'Harry Potter'
}
}
})
]

const response = await testDestination.executeBatch('dataExtension', {
events,
settings,
mapping
})

expect(response[0]).toMatchObject({
status: 400,
errortype: 'BAD_REQUEST',
errormessage: 'No record found for ID: HS2',
errorreporter: 'DESTINATION'
})

expect(response[1]).toMatchObject({
status: 400,
errortype: 'BAD_REQUEST',
errormessage: 'No record found for ID: HS2',
errorreporter: 'DESTINATION'
})
})
})

describe('contactDataExtension', () => {
it('should successfully handle a batch of events with complete success response from SFMC API', async () => {
nock(requestUrl).post('').reply(200, {})

const events: SegmentEvent[] = [
createTestEvent({
type: 'track',
properties: {
id: '1234567890',
keys: {
contactKey: 'harry-1',
id: 'HS1'
},
values: {
name: 'Harry Styles'
}
}
}),
createTestEvent({
type: 'track',
properties: {
id: '1234567890',
keys: {
contactKey: 'harry-2',
id: 'HP1'
},
values: {
name: 'Harry Potter'
}
}
})
]

const response = await testDestination.executeBatch('contactDataExtension', {
events,
settings,
mapping
})

expect(response[0]).toMatchObject({
status: 200,
body: {}
})

expect(response[1]).toMatchObject({
status: 200,
body: {}
})
})

it('should handle the case where both key and id are missing', async () => {
const events: SegmentEvent[] = [
createTestEvent({
type: 'track',
userId: 'harry-1',
properties: {
keys: {
contactKey: 'harry-1',
id: 'HS1'
},
values: {
name: 'Harry Styles'
}
}
}),
createTestEvent({
type: 'track',
userId: 'harry-2',
properties: {
keys: {
contactKey: 'harry-2',
id: 'HP1'
},
values: {
name: 'Harry Potter'
}
}
}) // No key and id provided
]

const response = await testDestination.executeBatch('contactDataExtension', {
events,
settings,
mapping
})

expect(response[0]).toMatchObject({
status: 400,
errortype: 'PAYLOAD_VALIDATION_FAILED',
errormessage:
'In order to send an event to a data extension either Data Extension ID or Data Extension Key must be defined.',
errorreporter: 'INTEGRATIONS'
})

expect(response[1]).toMatchObject({
status: 400,
errortype: 'PAYLOAD_VALIDATION_FAILED',
errormessage:
'In order to send an event to a data extension either Data Extension ID or Data Extension Key must be defined.',
errorreporter: 'INTEGRATIONS'
})
})

it('should handle multistatus errors when some events fail with specific error messages', async () => {
const errorResponse = {
status: 400,
message: 'Invalid keys for ID: HS1',
additionalErrors: [
{
message: 'No record found for ID: HS2'
}
]
}

nock(requestUrl).post('').reply(400, errorResponse)

const events: SegmentEvent[] = [
createTestEvent({
type: 'track',
userId: 'harry-1',
properties: {
id: '1234567890',
keys: {
contactKey: 'harry-1',
id: 'HS1'
},
values: {
name: 'Harry Styles'
}
}
}),
createTestEvent({
type: 'track',
userId: 'harry-2',
properties: {
id: '1234567890',
keys: {
contactKey: 'harry-2',
id: 'HS2'
},
values: {
name: 'Harry Potter'
}
}
})
]

const response = await testDestination.executeBatch('contactDataExtension', {
events,
settings,
mapping
})

expect(response[0]).toMatchObject({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question for my understanding: Why would response[0] show an error related to ID: HS2 when events[0] is sending ID: HS1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SFMC will return an error if any event in the payload is invalid and will fail the entire batch. Therefore, the error message is returned for all events to indicate that at least one of them has an issue.
Also the error message here is a dummy example, SFMC error messages do not specify the id.

status: 400,
errortype: 'BAD_REQUEST',
errormessage: 'No record found for ID: HS2',
errorreporter: 'DESTINATION'
})

expect(response[1]).toMatchObject({
status: 400,
errortype: 'BAD_REQUEST',
errormessage: 'No record found for ID: HS2',
errorreporter: 'DESTINATION'
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ActionDefinition } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
import { key, id, keys, enable_batching, batch_size, values_contactFields } from '../sfmc-properties'
import { upsertRows } from '../sfmc-operations'
import { executeUpsertWithMultiStatus, upsertRows } from '../sfmc-operations'

const action: ActionDefinition<Settings, Payload> = {
title: 'Send Contact to Data Extension',
Expand Down Expand Up @@ -33,24 +33,8 @@ const action: ActionDefinition<Settings, Payload> = {
perform: async (request, { settings, payload }) => {
return upsertRows(request, settings.subdomain, [payload])
},
performBatch: async (request, { settings, payload, statsContext, features }) => {
if (features && features['enable-sfmc-id-key-stats']) {
const statsClient = statsContext?.statsClient
const tags = statsContext?.tags
const setKey = new Set()
const setId = new Set()
payload.forEach((profile) => {
if (profile.id != undefined && profile.id != null) {
setId.add(profile.id)
}
if (profile.key != undefined && profile.key != null) {
setKey.add(profile.key)
}
})
statsClient?.histogram(`sfmc_id`, setId.size, tags)
statsClient?.histogram(`sfmc_key`, setKey.size, tags)
}
return upsertRows(request, settings.subdomain, payload)
performBatch: async (request, { settings, payload }) => {
return executeUpsertWithMultiStatus(request, settings.subdomain, payload)
}
}

Expand Down
Loading
Loading