Skip to content

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

Open
jcpsimmons wants to merge 11 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 11 commits intosegmentio:mainfrom
customerio:cdp-4750-cio-track-api-multistatus

Conversation

@jcpsimmons
Copy link
Copy Markdown
Contributor

@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
Contributor 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
Contributor 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
Contributor 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
Contributor 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
Contributor 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
Contributor 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
Contributor 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
Contributor 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
Contributor 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
Contributor Author

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

@jcpsimmons
Copy link
Copy Markdown
Contributor Author

@joe-ayoub-segment friendly nudge - I've addressed all your feedback from the last review. Could you take another look? Happy to hop on a call if anything needs discussion. Thanks!

Copilot AI review requested due to automatic review settings April 14, 2026 09:15
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR enhances the Customer.io destination’s batch sender to interpret Customer.io Track API batch responses (including HTTP 207 Multi-Status) into per-item MultiStatusResponse entries, so partial failures can be surfaced correctly by the Actions framework.

Changes:

  • Updates sendBatch to parse Track API batch responses and return a MultiStatusResponse when per-item errors are present.
  • Adds parsing utilities (parseTrackApiErrors, parseTrackApiMultiStatusResponse) plus response-body extraction logic to support multiple response shapes.
  • Adds unit tests validating 207 and 200-with-errors batch behaviors and parser edge cases.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/destination-actions/src/destinations/customerio/utils.ts Implements Track API batch multi-status parsing and updates sendBatch/types to use RequestClient.
packages/destination-actions/src/destinations/customerio/__tests__/utils.test.ts Adds unit tests covering multi-status parsing and sendBatch behavior.

Comment thread packages/destination-actions/src/destinations/customerio/utils.ts
Comment on lines +306 to +313
const multiStatusResponse = new MultiStatusResponse()
const errorMap = new Map<number, TrackApiError>()

for (const error of errors) {
if (typeof error.batch_index === 'number') {
errorMap.set(error.batch_index, error)
}
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

parseTrackApiErrors stores errors in a Map<number, TrackApiError>, so if the Track API returns multiple error entries for the same batch_index, earlier errors will be overwritten and lost. Consider grouping errors per index (e.g., Map<number, TrackApiError[]>) and combining messages/fields so the per-item response preserves all reported issues.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

@joe-ayoub-segment
Copy link
Copy Markdown
Contributor

Hi @jcpsimmons,

I was on PTO the last 2 weeks so just looking at this now.

Thanks for applying the changes. Copilot is now also set up to review PRs. Can you respond to the comments it left please?

Also I can see that CI is failing. Can you take a look please?

Finally, would it be possible to include some proof of testing. This can be in the form of screenshots ro videos.

The main things I'd be looking for are:

  1. Sending a batch where all events succeed
  2. Sending a batch where some events fail, some succeed.
  3. Sending a batch where all events fail.

It looks like there are 2 types of responses which can come back from Customer.io. One with a string value which needds to be parsed into JSON, and another which is already JSON.

Could you also demonstrate that you have tested these two different scenarios please?
Best regards,
Joe

@joe-ayoub-segment
Copy link
Copy Markdown
Contributor

Hi @jcpsimmons - I took another look at this. I think it would be worth making a few changes.

1 Current code catches everything:

    } catch (err) {
      const error = err as { message?: string; response?: { status?: number } }
      const status = error.response?.status ?? 500
      const message = error.message ?? 'Unknown error'

      const multiStatusResponse = new MultiStatusResponse()
      for (let i = 0; i < options.length; i++) {
        multiStatusResponse.setErrorResponseAtIndex(i, {
          status,
          errormessage: message,
          body: options[i].payload,
          sent: batch[i]
        })
      }
      return multiStatusResponse
    }

This catches everything — HTTPError, RetryableError, TypeError, you name it — and silently converts it all into a MultiStatusResponse. The framework never sees a thrown error, so it never retries.

How it should be done (following the Braze pattern at braze/utils.ts:625-658):

    } catch (err) {
      if (err instanceof HTTPError) {
        const status = err.response.status
        const message = err.message ?? 'Unknown error'

        const multiStatusResponse = new MultiStatusResponse()
        for (let i = 0; i < options.length; i++) {
          multiStatusResponse.setErrorResponseAtIndex(i, {
            status,
            errormessage: message,
            body: options[i].payload,
            sent: batch[i]
          })
        }
        return multiStatusResponse
      }

      throw err
    }
  1. getResponseBody has unnecessary complexity

The getResponseBody function (in the diff) tries response.data, response.content, and response.body, then attempts JSON parse, then even base64 decode. But RequestClient (from @segment/actions-core) always returns a typed response where response.data is already parsed JSON when the response has a JSON content-type. The content/body fallbacks and the base64 decoding path are dead code — they'll never be hit with a RequestClient. This is speculative defensive coding that adds confusion. It should just use response.data.

  1. parseTrackApiMultiStatusResponse returns null when errors is an empty array

If the API returns { errors: [] } (200 with an empty errors array), parseTrackApiMultiStatusResponse returns null, which means sendBatch falls through to returning the raw response. That's fine in practice (no errors = success), but it means a 207 with { errors: [] } also falls through to returning the raw response instead of a MultiStatusResponse with all successes. This is an inconsistence - a 207 with a valid errors array should probably always return a MultiStatusResponse, even if every item succeeded.

  1. mapTrackApiReasonToErrorCode defaults to undefined

When the reason string from Customer.io doesn't match 'invalid' or 'required', errortype is set to undefined. This means the framework has no error code to work with. It would be safer to default to a generic error code like ErrorCodes.UNKNOWN_ERROR or ErrorCodes.INTEGRATION_ERROR rather than leaving it blank.

  1. Return type is inconsistent

sendBatch can now return undefined (empty options), a MultiStatusResponse, or the raw Response object (when no errors are parsed). Can you just return MultiStatusResponse when a batch has been sent (via performBatch), and a raw response object when a single event is sent (via perform)? Everything else should be a thrown error.

Copilot AI review requested due to automatic review settings April 20, 2026 17:36
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@jcpsimmons
Copy link
Copy Markdown
Contributor Author

@sydneycollins-cio handing this PR to you along with CDP-4750 if you're able to take it over.

Status from my side: latest review still has substantive follow-up. Joe requested proof-of-testing/screenshots plus the code-review follow-through, and the current batch-response behavior likely still needs one more correctness pass before this is ready.

Copilot AI review requested due to automatic review settings April 20, 2026 21:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment on lines +246 to +252
multiStatusResponse.setErrorResponseAtIndex(i, {
status,
errortype: ErrorCodes.INTEGRATION_ERROR,
errormessage: message,
body: options[i].payload,
sent: batch[i]
})
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

In the non-retryable HTTPError path, each per-item error response is forced to errortype: ErrorCodes.INTEGRATION_ERROR regardless of the HTTP status. This will misclassify authentication/authorization failures (401/403) and other status-derived categories, since the framework only auto-inferrs errortype when it’s omitted. Consider omitting errortype here (so it can be inferred) or mapping it based on the status code (e.g., invalid auth vs validation vs unknown).

Copilot uses AI. Check for mistakes.
Comment on lines +232 to +255
} catch (err) {
// Retryable HTTP errors (408 Request Timeout, 429 Too Many Requests, 5xx Server Errors)
// and unexpected non-HTTP errors should be rethrown so the framework's retry wrapper
// can handle them. Only convert to per-item errors for non-retryable HTTP failures.
if (err instanceof HTTPError) {
const status = err.response.status
if (status === 408 || status === 429 || status >= 500) {
throw err
}

const responseBody = err.response?.data as { message?: string } | undefined
const message = responseBody?.message ?? err.message ?? 'Unknown error'
const multiStatusResponse = new MultiStatusResponse()
for (let i = 0; i < options.length; i++) {
multiStatusResponse.setErrorResponseAtIndex(i, {
status,
errortype: ErrorCodes.INTEGRATION_ERROR,
errormessage: message,
body: options[i].payload,
sent: batch[i]
})
}
return multiStatusResponse
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

sendBatch adds new behavior to convert non-retryable HTTPError responses into a per-item MultiStatusResponse, but the test suite only covers retryable HTTP errors (429/5xx) and unexpected response shapes. Add a unit test that simulates a non-retryable HTTPError (e.g., 400/401) and asserts the returned MultiStatusResponse contains per-item errors with the expected status/message (and desired error typing).

Copilot generated this review using guidance from repository custom instructions.
@sydneycollins-cio
Copy link
Copy Markdown
Contributor

sydneycollins-cio commented Apr 21, 2026

Testing Evidence

Batch of 3 events sent to POST /v2/batch across all 6 response scenarios.

Note on Track API response format: The Track API only returns entries for failed items in the errors array. Items absent from that array are implicitly successful. Our parseTrackApiErrors infers a 200 OK for any batch index not listed in errors — no separate request is made for successful items.

Shared request payload:

{
  "batch": [
    {
      "type": "person", "action": "event",
      "identifiers": { "id": "usr_abc123" },
      "name": "Product Purchased",
      "timestamp": "2026-04-21T18:00:00.000Z",
      "data": { "product_id": "prod_999", "price": 49.99, "currency": "USD", "quantity": 2 }
    },
    {
      "type": "person", "action": "event",
      "identifiers": { "id": "usr_def456" },
      "name": "Page Viewed",
      "timestamp": "2026-04-21T18:00:01.000Z",
      "data": { "page": "/pricing", "referrer": "https://google.com", "notes": "xxx... [1001 chars]" }
    },
    {
      "type": "person", "action": "event",
      "identifiers": { "id": "usr_ghi789" },
      "name": "Email Opened",
      "timestamp": "2026-04-21T18:00:02.000Z",
      "data": { "campaign_id": "camp_123", "subject": "Your weekly digest" }
    }
  ]
}

Case 1 — HTTP 207 Multi-Status: partial failure

usr_def456 fails validation (oversized field). usr_abc123 and usr_ghi789 are absent from errors — inferred as 200 OK. All 3 handled in one request.

Track API → HTTP 207
{ "errors": [{ "batch_index": 1, "reason": "invalid", "message": "data.notes exceeds maximum length of 1000 characters" }] }

MultiStatusResponse (inferred from absence = success):
  [0] status: 200  OK — Product Purchased (usr_abc123)  ← not in errors array
  [1] status: 400  errortype: PAYLOAD_VALIDATION_FAILED  "data.notes exceeds maximum length of 1000 characters" (usr_def456)
  [2] status: 200  OK — Email Opened (usr_ghi789)        ← not in errors array

Case 2 — HTTP 200 with errors array: partial failure

Track API returns 200 but includes per-item errors for items 0 and 2.

Track API → HTTP 200
{ "errors": [
    { "batch_index": 0, "reason": "notFound", "message": "Identifier usr_abc123 not found" },
    { "batch_index": 2, "reason": "notFound", "message": "Identifier usr_ghi789 not found" }
  ]
}

MultiStatusResponse:
  [0] status: 400  errortype: INTEGRATION_ERROR  "Identifier usr_abc123 not found" (usr_abc123)
  [1] status: 200  OK — Page Viewed (usr_def456)  ← not in errors array
  [2] status: 400  errortype: INTEGRATION_ERROR  "Identifier usr_ghi789 not found" (usr_ghi789)

Case 3 — HTTP 200 empty errors: all success

Track API → HTTP 200  { "errors": [] }

MultiStatusResponse (all absent from errors = all success):
  [0] status: 200  OK — Product Purchased (usr_abc123)
  [1] status: 200  OK — Page Viewed (usr_def456)
  [2] status: 200  OK — Email Opened (usr_ghi789)

Case 4 — HTTP 429: retryable

Error rethrown — framework retries entire batch, no per-item conversion.

Track API → HTTP 429  { "meta": { "error": "Too many requests. Retry after 1000ms." } }
→ HTTPError(429) rethrown — framework retries entire batch

Case 5 — HTTP 500: retryable

Track API → HTTP 500  { "error": "Internal server error" }
→ HTTPError(500) rethrown — framework retries entire batch

Case 6 — HTTP 400: non-retryable flat error

Flat error propagated to all items as INTEGRATION_ERROR.

Track API → HTTP 400  { "message": "Request body missing required field: batch" }

MultiStatusResponse:
  [0] status: 400  errortype: INTEGRATION_ERROR  "Request body missing required field: batch" (usr_abc123)
  [1] status: 400  errortype: INTEGRATION_ERROR  "Request body missing required field: batch" (usr_def456)
  [2] status: 400  errortype: INTEGRATION_ERROR  "Request body missing required field: batch" (usr_ghi789)

Unit tests: 23/23 passing (sendBatch, parseTrackApiErrors, parseTrackApiMultiStatusResponse)

Copilot AI review requested due to automatic review settings April 21, 2026 19:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

errors?: TrackApiError[]
}

function mapHttpStatusToErrorCode(status: number): string {
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

mapHttpStatusToErrorCode is typed to return string, but getErrorCodeFromHttpStatus returns keyof typeof ErrorCodes and ActionDestinationErrorResponseType.errortype expects that key type. As written, this can cause a TypeScript type error (or weaken typing) when setting errortype on the MultiStatus entries. Update the return type (or inline getErrorCodeFromHttpStatus) so errortype remains keyof typeof ErrorCodes.

Suggested change
function mapHttpStatusToErrorCode(status: number): string {
function mapHttpStatusToErrorCode(status: number): keyof typeof ErrorCodes {

Copilot uses AI. Check for mistakes.
Comment on lines +295 to +306

for (const error of errors) {
if (typeof error.batch_index === 'number') {
const existing = errorMap.get(error.batch_index)
if (existing) {
existing.push(error)
} else {
errorMap.set(error.batch_index, [error])
}
}
}

Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

parseTrackApiErrors currently drops any error objects that don't include a numeric batch_index. If Customer.io returns an error entry without batch_index (or with an out-of-range index), the code will silently treat all items as success, which can hide real failures. Consider detecting unindexable errors and surfacing them (e.g., throw an IntegrationError, or apply a per-item error for the whole batch) rather than ignoring them.

Suggested change
for (const error of errors) {
if (typeof error.batch_index === 'number') {
const existing = errorMap.get(error.batch_index)
if (existing) {
existing.push(error)
} else {
errorMap.set(error.batch_index, [error])
}
}
}
const unindexableErrors: TrackApiError[] = []
for (const error of errors) {
const batchIndex = error.batch_index
if (!Number.isInteger(batchIndex) || batchIndex < 0 || batchIndex >= options.length) {
unindexableErrors.push(error)
continue
}
const existing = errorMap.get(batchIndex)
if (existing) {
existing.push(error)
} else {
errorMap.set(batchIndex, [error])
}
}
if (unindexableErrors.length > 0) {
const errormessage = unindexableErrors
.map((error) => {
const indexDescription =
typeof error.batch_index === 'number' ? `batch_index ${error.batch_index}` : 'missing batch_index'
return error.message || `${error.reason || 'ERROR'}: ${error.field || 'unknown field'} (${indexDescription})`
})
.join('; ')
throw new IntegrationError(`Customer.io returned batch errors that could not be mapped to request items: ${errormessage}`)
}

Copilot uses AI. Check for mistakes.
throw new IntegrationError(
'Customer.io Track API batch response did not include an errors array',
'INVALID_RESPONSE',
400
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

This IntegrationError is raised for an unexpected/invalid response shape from Customer.io, but it uses HTTP status 400 (client error). That can be misleading since it isn't caused by the input payload. Consider using a 5xx (commonly 500/502) status so the failure is classified as a dependency/partner response issue.

Suggested change
400
502

Copilot uses AI. Check for mistakes.
@sgwcollins
Copy link
Copy Markdown
Contributor

@joe-ayoub-segment ready for review when you have a chance!

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.

5 participants