Skip to content

Parse Customer.io Track API batch multi-status responses#3657

Open
jcpsimmons wants to merge 3 commits intosegmentio:mainfrom
customerio:cdp-4750-cio-track-api-multistatus
Open

Parse Customer.io Track API batch multi-status responses#3657
jcpsimmons wants to merge 3 commits intosegmentio:mainfrom
customerio:cdp-4750-cio-track-api-multistatus

Conversation

@jcpsimmons
Copy link
Copy Markdown

@jcpsimmons jcpsimmons commented Mar 10, 2026

Parses Customer.io Track API batch responses into per-item MultiStatusResponse results so Segment can surface partial failures correctly.

Testing

Verified locally under Node 22.13.1:

  • TZ=UTC yarn cloud test --testPathPattern=src/destinations/customerio/__tests__/utils.test.ts

  • ./bin/run serve --destination=customerio -n boots successfully and exposes the Customer.io action routes ✅

  • End-to-end testing using the local server was completed ✅

  • Added unit tests for new functionality

  • Tested end-to-end using the local server

  • [If destination is already live] Tested for backward compatibility of destination. Note: New required fields are a breaking change.

  • [Segmenters] Tested in the staging environment

  • [If applicable for this change] Tested for regression with Hadron.

Security Review

No field definitions or credential handling were changed in this PR.

  • Reviewed all field definitions for sensitive data (API keys, tokens, passwords, client secrets) and confirmed they use type password

New Destination Checklist

Not applicable.

  • Extracted all action API versions to verioning-info.ts file. example

@jcpsimmons
Copy link
Copy Markdown
Author

This draft is 1 of 3 upstream Customer.io PRs for CDP-4750.

Scope here: only Track API batch multi-status parsing in customerio/utils.ts plus focused parsing tests.

Not in this PR:

  • decimal timestamp normalization
  • preset/default-subscription parity

Companion drafts:

This supersedes the Track API parsing portion of the older combined draft #3654.

@jcpsimmons
Copy link
Copy Markdown
Author

Hi @joe-ayoub-segment could I please have review on this and the other two PRs 🙏

Copy link
Copy Markdown
Contributor

@joe-ayoub-segment joe-ayoub-segment left a comment

Choose a reason for hiding this comment

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

Hi @jcpsimmons,

Thanks for raising the PR for this change. It will really help customers with observability.

I left some comments for you to look at.
I didn't review all of the PR as i found it difficult to follow. Adding the types (see comments) will help me better understand the logic.

One final thing - please add proof of testing to the PR description.

  • show batch payloads being delivered
  • show error responses being returned correctly

Let me know if you have any questions or would like assistance.

best regards,
Joe

Comment on lines +220 to +232
if (response?.status === 207 && responseBody) {
const parsedResults = parseTrackApiMultiStatusResponse(responseBody, batch.length)
if (parsedResults) {
return parsedResults
}
}

if (response?.status === 200 && responseBody) {
const parsedResults = parseTrackApiMultiStatusResponse(responseBody, batch.length)
if (parsedResults) {
return parsedResults
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Can we reduce the redundant code here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, collapsed the two blocks into a single OR condition.

const batch = options.map((opts) => buildPayload(opts))

return request(`${trackApiEndpoint(settings)}/api/${CUSTOMERIO_TRACK_API_VERSION}/batch`, {
const response = await request(`${trackApiEndpoint(settings)}/api/${CUSTOMERIO_TRACK_API_VERSION}/batch`, {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we add an expected response type to this?
For example:

    const response = await request<CustomerIOBatchResponse>(`${trackApiEndpoint(settings)}/api/${CUSTOMERIO_TRACK_API_VERSION}/batch`, {
      method: 'POST',
      json { batch }
    })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also, consider wrapping this request in a try catch so that you can properly populate errors in the multistatusResponse when the entire payload fails.

try {
const response = await request(${trackApiEndpoint(settings)}/api/${CUSTOMERIO_TRACK_API_VERSION}/batch, {
method: 'POST',
json { batch }
})
}
catch(err) {
const error as CustomerIOErrorResponse
// for each payload in the batch, do setErrorResponseAtIndex
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added. Renamed TrackApiResponse to CustomerIOBatchResponse and passed it as the generic: request<CustomerIOBatchResponse>(...). Also wrapped the whole thing in a try/catch per your other comment so failed requests populate every index with the error details.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added. The catch block extracts the status and message from the error, then iterates over every item in the batch and calls setErrorResponseAtIndex with the original payload and sent JSON. If the whole request blows up, every item in the multi-status response reflects the failure.

}
})

const responseBody = getResponseBody(response)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we add a type to this too please?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Covered by the same rename. getResponseBody now returns CustomerIOBatchResponse | string | undefined and that flows through to parseTrackApiMultiStatusResponse.

const error = errorMap.get(i)

if (!error) {
multiStatusResponse.setSuccessResponseAtIndex(i, {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The contents of each multiStatusResponse item is passed to the UI for observability purposes.
So if possible we should populate body and sent values correctly:

    msResponse.setSuccessResponseAtIndex(index, {
      status: 200,
      body: // this should be the original payload object sent to the perform function,
      sent: // this should be the actual JSON which was posted to your platform
    })

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good call. Threaded the original options array and the built batch array through to parseTrackApiErrors. Success responses now get body: options[i].payload (original payload from perform) and sent: batch[i] (the transformed JSON we posted).

continue
}

multiStatusResponse.setErrorResponseAtIndex(i, {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similarly to successful responses, we should try and populate error responses with correct details.

msResponse.setErrorResponseAtIndex(index, {
  status: 400,
  errortype,
  errormessage,
  body: // this should be the original payload object sent to the perform function,
  sent: // this should be the actual JSON which was posted to your platform
})

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Same fix, error responses now also get body: options[i].payload and sent: batch[i].

}
}

function getResponseBody(response: RequestResponse): unknown {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please define a proper response type for this function.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. Returns CustomerIOBatchResponse | string | undefined now.

@joe-ayoub-segment
Copy link
Copy Markdown
Contributor

joe-ayoub-segment commented Mar 25, 2026 via email

… try/catch

- Collapse duplicate 200/207 status blocks into single OR condition
- Rename TrackApiResponse to CustomerIOBatchResponse and add as request generic
- Add return type to getResponseBody
- Thread original payloads and built batch through to populate body/sent in multi-status responses
- Wrap batch request in try/catch to handle full payload failures
@jcpsimmons jcpsimmons force-pushed the cdp-4750-cio-track-api-multistatus branch from 2244302 to 8be62ad Compare March 30, 2026 21:34
@jcpsimmons
Copy link
Copy Markdown
Author

HI @joe-ayoub-segment thank you for that feedback - I've addressed could I please have a re-review?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants