Skip to content

Commit 8623abc

Browse files
committed
add multistatus TTD, update tests
:q
1 parent b1e555e commit 8623abc

File tree

2 files changed

+214
-43
lines changed

2 files changed

+214
-43
lines changed

packages/destination-actions/src/destinations/the-trade-desk-crm/functions.ts

Lines changed: 82 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RequestClient, ModifiedResponse, PayloadValidationError } from '@segment/actions-core'
1+
import { RequestClient, ModifiedResponse, PayloadValidationError, MultiStatusResponse } from '@segment/actions-core'
22
import { Settings } from './generated-types'
33
import { Payload } from './syncAudience/generated-types'
44
// eslint-disable-next-line no-restricted-syntax
@@ -62,8 +62,18 @@ export async function processPayload(input: ProcessPayloadInput) {
6262
crmID = input.payloads[0].external_id
6363
}
6464

65+
const multiStatusResponse = new MultiStatusResponse()
66+
// mark all payloads as successful and then filter out payloads with invalid emails
67+
for (let i = 0; i < input.payloads.length; i++) {
68+
const payload = input.payloads[i]
69+
multiStatusResponse.setSuccessResponseAtIndex(i, {
70+
sent: { ...payload },
71+
status: 200,
72+
body: 'Successfully uploaded to S3'
73+
})
74+
}
6575
// Get user emails from the payloads
66-
const [usersFormatted, rowCount] = extractUsers(input.payloads)
76+
const [usersFormatted, rowCount] = extractUsers(input.payloads, multiStatusResponse)
6777

6878
// Overwrite to Legacy Flow if feature flag is enabled
6979
if (input.features && input.features[TTD_LEGACY_FLOW_FLAG_NAME]) {
@@ -72,43 +82,90 @@ export async function processPayload(input: ProcessPayloadInput) {
7282
// -----------
7383

7484
if (input.payloads.length < TTD_MIN_RECORD_COUNT) {
75-
throw new PayloadValidationError(
76-
`received payload count below The Trade Desk's ingestion minimum. Expected: >=${TTD_MIN_RECORD_COUNT} actual: ${input.payloads.length}`
77-
)
85+
for (let i = 0; i < input.payloads.length; i++) {
86+
multiStatusResponse.setErrorResponseAtIndex(i, {
87+
status: 400,
88+
errortype: 'PAYLOAD_VALIDATION_FAILED',
89+
errormessage: `received payload count below The Trade Desk's ingestion minimum. Expected: >=${TTD_MIN_RECORD_COUNT} actual: ${input.payloads.length}`,
90+
sent: { ...input.payloads[i] },
91+
body: `received payload count below The Trade Desk's ingestion minimum. Expected: >=${TTD_MIN_RECORD_COUNT} actual: ${input.payloads.length}`
92+
})
93+
}
94+
return multiStatusResponse
7895
}
7996

80-
// Create a new TTD Drop Endpoint
81-
const dropEndpoint = await getCRMDataDropEndpoint(input.request, input.settings, input.payloads[0], crmID)
82-
83-
// Upload CRM Data to Drop Endpoint
84-
return uploadCRMDataToDropEndpoint(input.request, dropEndpoint, usersFormatted)
97+
try {
98+
// Create a new TTD Drop Endpoint
99+
const dropEndpoint = await getCRMDataDropEndpoint(input.request, input.settings, input.payloads[0], crmID)
100+
101+
// Upload CRM Data to Drop Endpoint
102+
await uploadCRMDataToDropEndpoint(input.request, dropEndpoint, usersFormatted)
103+
104+
return multiStatusResponse
105+
} catch (error) {
106+
for (let i = 0; i < input.payloads.length; i++) {
107+
if (multiStatusResponse.isSuccessResponseAtIndex(i)) {
108+
multiStatusResponse.setErrorResponseAtIndex(i, {
109+
status: 500,
110+
errortype: 'RETRYABLE_ERROR',
111+
errormessage: `Failed to upload to The Trade Desk Drop Endpoint: ${(error as Error).message}`,
112+
sent: { ...input.payloads[i] },
113+
body: `The Trade Desk Drop Endpoint upload failed: ${(error as Error).message}`
114+
})
115+
}
116+
}
117+
return multiStatusResponse
118+
}
85119
} else {
86120
//------------
87121
// AWS FLOW
88122
// -----------
89123

90-
// Send request to AWS to be processed
91-
return sendEventToAWS({
92-
TDDAuthToken: input.settings.auth_token,
93-
AdvertiserId: input.settings.advertiser_id,
94-
CrmDataId: crmID,
95-
UsersFormatted: usersFormatted,
96-
RowCount: rowCount,
97-
DropOptions: {
98-
PiiType: input.payloads[0].pii_type,
99-
MergeMode: 'Replace',
100-
RetentionEnabled: true
124+
try {
125+
// Send request to AWS to be processed
126+
await sendEventToAWS({
127+
TDDAuthToken: input.settings.auth_token,
128+
AdvertiserId: input.settings.advertiser_id,
129+
CrmDataId: crmID,
130+
UsersFormatted: usersFormatted,
131+
RowCount: rowCount,
132+
DropOptions: {
133+
PiiType: input.payloads[0].pii_type,
134+
MergeMode: 'Replace',
135+
RetentionEnabled: true
136+
}
137+
})
138+
} catch (error) {
139+
// Mark all remaining success payloads as failed if AWS upload fails
140+
for (let i = 0; i < input.payloads.length; i++) {
141+
if (multiStatusResponse.isSuccessResponseAtIndex(i)) {
142+
multiStatusResponse.setErrorResponseAtIndex(i, {
143+
status: 500,
144+
errortype: 'RETRYABLE_ERROR',
145+
errormessage: `Failed to upload to AWS: ${(error as Error).message}`,
146+
sent: { ...input.payloads[i] },
147+
body: `AWS upload failed: ${(error as Error).message}`
148+
})
149+
}
101150
}
102-
})
151+
}
152+
return multiStatusResponse
103153
}
104154
}
105155

106-
function extractUsers(payloads: Payload[]): [string, number] {
156+
function extractUsers(payloads: Payload[], multiStatusResponse: MultiStatusResponse): [string, number] {
107157
let users = ''
108158
let rowCount = 0
109159

110-
payloads.forEach((payload: Payload) => {
160+
payloads.forEach((payload: Payload, index: number) => {
111161
if (!payload.email || !validateEmail(payload.email, payload.pii_type)) {
162+
multiStatusResponse.setErrorResponseAtIndex(index, {
163+
status: 400,
164+
errortype: 'PAYLOAD_VALIDATION_FAILED',
165+
errormessage: `Invalid email: ${payload.email}`,
166+
sent: { ...payload },
167+
body: `Invalid email: ${payload.email}`
168+
})
112169
return
113170
}
114171

@@ -196,7 +253,7 @@ async function getCRMDataDropEndpoint(request: RequestClient, settings: Settings
196253

197254
// Uploads CRM Data to Drop Endpoint (Legacy Flow)
198255
async function uploadCRMDataToDropEndpoint(request: RequestClient, endpoint: string, users: string) {
199-
return await request(endpoint, {
256+
await request(endpoint, {
200257
method: 'PUT',
201258
headers: {
202259
'Content-Type': 'text/plain'

packages/destination-actions/src/destinations/the-trade-desk-crm/syncAudience/__tests__/index.test.ts

Lines changed: 132 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@ import nock from 'nock'
22
import { createTestEvent, createTestIntegration, SegmentEvent } from '@segment/actions-core'
33
import Destination from '../../index'
44

5+
// Type for MultiStatus error response node
6+
interface MultiStatusErrorNode {
7+
status: number
8+
errortype: string
9+
errormessage: string
10+
sent: any
11+
body: string
12+
errorreporter: string
13+
}
14+
515
import { TTD_LEGACY_FLOW_FLAG_NAME } from '../../functions'
616

717
import { getAWSCredentialsFromEKS, AWSCredentials } from '../../../../lib/AWS/sts'
@@ -103,24 +113,40 @@ describe('TheTradeDeskCrm.syncAudience', () => {
103113
.get(/.*/)
104114
.reply(200, { Segments: [], PagingToken: null })
105115

106-
await expect(
107-
testDestination.testAction('syncAudience', {
108-
event,
109-
settings: {
110-
advertiser_id: 'advertiser_id',
111-
auth_token: 'test_token',
112-
__segment_internal_engage_force_full_sync: true,
113-
__segment_internal_engage_batch_sync: true
114-
},
115-
features: { 'actions-the-trade-desk-crm-legacy-flow': true },
116-
useDefaultMappings: true,
117-
mapping: {
118-
name: 'test_audience',
119-
region: 'US',
120-
pii_type: 'Email'
121-
}
122-
})
123-
).rejects.toThrow(`received payload count below The Trade Desk's ingestion minimum. Expected: >=1500 actual: 1`)
116+
const response = await testDestination.testBatchAction('syncAudience', {
117+
events: [event],
118+
settings: {
119+
advertiser_id: 'advertiser_id',
120+
auth_token: 'test_token',
121+
__segment_internal_engage_force_full_sync: true,
122+
__segment_internal_engage_batch_sync: true
123+
},
124+
features: {
125+
[TTD_LEGACY_FLOW_FLAG_NAME]: true
126+
},
127+
useDefaultMappings: true,
128+
mapping: {
129+
name: 'test_audience',
130+
region: 'US',
131+
pii_type: 'Email'
132+
}
133+
})
134+
135+
const multiStatusResponse = testDestination.results?.[0]?.multistatus
136+
137+
expect(multiStatusResponse).toBeDefined()
138+
if (multiStatusResponse) {
139+
expect(multiStatusResponse.length).toBe(1)
140+
expect(multiStatusResponse[0].status).toBe(400)
141+
142+
// Type-safe access to error properties
143+
const errorResponse = multiStatusResponse[0] as MultiStatusErrorNode
144+
expect(errorResponse.errortype).toBe('PAYLOAD_VALIDATION_FAILED')
145+
expect(errorResponse.errormessage).toContain('received payload count below')
146+
}
147+
148+
// No HTTP requests should be made when validation fails early
149+
expect(response.length).toBe(0)
124150
})
125151

126152
it('should execute legacy flow if flagon override is defined', async () => {
@@ -231,6 +257,94 @@ describe('TheTradeDeskCrm.syncAudience', () => {
231257
).rejects.toThrow(`No external_id found in payload.`)
232258
})
233259

260+
it('should mark the payload with invalid email as failed in multistatus response', async () => {
261+
const dropReferenceId = 'aabbcc5b01-c9c7-4000-9191-000000000000'
262+
const dropEndpoint = `https://thetradedesk-crm-data.s3.us-east-1.amazonaws.com/data/advertiser/advertiser-id/drop/${dropReferenceId}/pii?X-Amz-Security-Token=token&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=date&X-Amz-SignedHeaders=host&X-Amz-Expires=3600&X-Amz-Credential=credentials&X-Amz-Signature=signature&`
263+
264+
nock(`https://api.thetradedesk.com/v3/crmdata/segment/advertiser_id/personas_test_audience`)
265+
.post(/.*/, { PiiType: 'Email', MergeMode: 'Replace', RetentionEnabled: true })
266+
.reply(200, { ReferenceId: dropReferenceId, Url: dropEndpoint })
267+
268+
nock(dropEndpoint).put(/.*/).reply(200)
269+
const events: SegmentEvent[] = []
270+
for (let index = 1; index <= 1500; index++) {
271+
events.push(
272+
createTestEvent({
273+
event: 'Audience Entered',
274+
type: 'track',
275+
properties: {
276+
audience_key: 'personas_test_audience'
277+
},
278+
context: {
279+
device: {
280+
advertisingId: '123'
281+
},
282+
traits: {
283+
email: `testing${index}@testing.com`
284+
},
285+
personas: {
286+
external_audience_id: 'external_audience_id'
287+
}
288+
}
289+
})
290+
)
291+
}
292+
events.push(
293+
createTestEvent({
294+
event: 'Audience Entered',
295+
type: 'track',
296+
properties: {
297+
audience_key: 'personas_test_audience'
298+
},
299+
context: {
300+
device: {
301+
advertisingId: '123'
302+
},
303+
traits: {
304+
email: `invalid-email-address`
305+
},
306+
personas: {
307+
external_audience_id: 'external_audience_id'
308+
}
309+
}
310+
})
311+
)
312+
313+
const responses = await testDestination.testBatchAction('syncAudience', {
314+
events,
315+
settings: {
316+
advertiser_id: 'advertiser_id',
317+
auth_token: 'test_token',
318+
__segment_internal_engage_force_full_sync: true,
319+
__segment_internal_engage_batch_sync: true
320+
},
321+
features: {
322+
[TTD_LEGACY_FLOW_FLAG_NAME]: true
323+
},
324+
useDefaultMappings: true,
325+
mapping: {
326+
name: 'test_audience',
327+
region: 'US',
328+
pii_type: 'Email'
329+
}
330+
})
331+
332+
expect(responses.length).toBe(2)
333+
const multiStatusResponse = testDestination.results?.[0]?.multistatus
334+
expect(multiStatusResponse).toBeDefined()
335+
if (multiStatusResponse) {
336+
const length = multiStatusResponse.length
337+
expect(length).toBe(1501)
338+
const invalidEmailResponse = multiStatusResponse[length - 1]
339+
expect(invalidEmailResponse.status).toBe(400)
340+
341+
// Type-safe access to error properties
342+
const errorResponse = invalidEmailResponse as MultiStatusErrorNode
343+
expect(errorResponse.errortype).toBe('PAYLOAD_VALIDATION_FAILED')
344+
expect(errorResponse.errormessage).toContain('Invalid email: invalid-email-address')
345+
}
346+
})
347+
234348
it('should not double hash an email that is already base64 encoded', async () => {
235349
const dropReferenceId = 'aabbcc5b01-c9c7-4000-9191-000000000000'
236350
const dropEndpoint = `https://thetradedesk-crm-data.s3.us-east-1.amazonaws.com/data/advertiser/advertiser-id/drop/${dropReferenceId}/pii?X-Amz-Security-Token=token&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=date&X-Amz-SignedHeaders=host&X-Amz-Expires=3600&X-Amz-Credential=credentials&X-Amz-Signature=signature&`

0 commit comments

Comments
 (0)