Shop_events_listener.js drops add-to-cart analytics when a Cart Transform function merges line items

Short description of issue

A /cart/add request whose line items get merged by a Cart Transform function makes shop_events_listener.js throw Payload body and response have different number of items, so no product_added_to_cart event fires. Items still add to the cart, but Shopify Analytics and downstream pixels (Meta, GA4) miss the add-to-cart.

Reproduction steps

  1. Set up a Cart Transform function that merges line items (e.g. a bundles app that merges selected component variants into a single bundle line).
  2. POST multiple items to /cart/add in one request (e.g. 3 component variants).
  3. The Cart Transform merges them server-side, so the cart response contains fewer lines (e.g. 1 line).
  4. Observe the console warning: [shop_events_listener] ... Payload body and response have different number of items.
  5. No product_added_to_cart event fires, so the add-to-cart is missing from Shopify Analytics and all downstream pixels.

Additional info

Same root assumption as this existing report: Shop_events_listener.js — add-to-cart analytics broken when same variant appears twice with different properties

The cause is in handleBulkItemCartAddResponse, which assumes a 1:1 mapping between request payload items and response items:

if (parsedResponseItemsList.length != parsedPayloadBodyItemsList.length) {
    throw Error("Payload body and response have different number of items")
}

Request has 3 items, response has 1, so it throws before any handleItemAdded call and no event is emitted. The listener assumes response line count equals payload count, which breaks whenever a Cart Transform reshapes the cart (by design). Merchants running one lose add-to-cart analytics with no app-side workaround.

Could the listener tolerate a mismatch instead of throwing, e.g. emitting events from the response lines rather than zipping by index?

What type of topic is this

Bug report

Upload screenshot(s) of issue

Hey @Anton - thanks for the detailed repro. This seems to line up with a known gap around add-to-cart analytics when Cart Transform changes the number of lines returned from add.js. There’s a bit more info here:

The workaround we reccommend right now is to avoid sending the bundle/component items in a single bulk /cart/add.js request when you know a Cart Transform is going to merge those lines. Instead, send the items as separate /cart/add.js requests.

So instead of one request like:

await fetch('/cart/add.js', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    items: [
      { id: 111, quantity: 1 },
      { id: 222, quantity: 1 },
      { id: 333, quantity: 1 }
    ]
  })
});

you’d send each item separately:

for (const item of items) {
  await fetch('/cart/add.js', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(item)
  });
}

That keeps the request/response shape aligned for each add operation, which should allow the add-to-cart event to be emitted instead of being dropped due to the line count mismatch.

The tradeoff is that this may emit multiple product_added_to_cart events instead of one combined bundle-level event, so it’s not the most ideal workaround if you’re trying to track the bundle as a single unit. But it should help preserve the business-critical add-to-cart signal for Shopify Analytics and downstream pixels while this behavior is looked into on our end.

If you’re able to share the exact /cart/add.js payload and response from a failing case, plus whether the separate-request approach restores the event for your setup, that would be helpful for confirming this is the same behaviour though and if it does seem like different behaviour I can do a deeper dive for sure. Hope this helps a bit.

Thanks Alan, and thanks for linking the related thread. That is indeed the same issue.

The workaround works in principle, but sequential add requests are hard for us to adopt in practice. Bundles can have dozens of component variants selected as children, and adding them one at a time would serialize the whole add-to-cart flow. At roughly 200ms per request, 20 variants is around 4 seconds before the customer sees anything happen, which is a real hit to the add-to-cart experience.

Beyond the latency, firing 20+ rapid sequential /cart/add.js requests per click worries me on two fronts: it is more likely to trip bot or rate-limit protections, and it makes the add-to-cart far more brittle, since any single request failing partway through leaves the cart in an inconsistent state that we then have to detect and unwind.

The batched add is what keeps the bundle add-to-cart fast and atomic, so we would much rather not give that up. Would Shopify consider having shop_events_listener.js tolerate a line-count mismatch instead of throwing, and emit events based on the response rather than the request? That actually seems more correct in general, since it guarantees events reflect what truly ended up in the cart. For a bundle, the listener would emit the event for the merged bundle line. And even outside bundles, if some individual items failed to add, only the items that actually made it into the cart would fire events. It would fix every Cart Transform case at once, without each app having to degrade its add-to-cart flow.

Happy to help test any fix against a real bundle store if that is useful.

Hey @Anton, no worries, and yeah I agree the sequential-request workaround could potentially be not super ideal for larger bundles, especially when the batched add is what keeps the bundle add fast and atomic.

I’m going to pass this along internally, just to mention that one idea is that the listener should avoid dropping the event entirely on a count mismatch and should use the server-confirmed added lines where possible, so the event reflects what actually made it into the cart.

I don’t have a timeline to share yet, but if you can share a sanitized failing /cart/add.js request/response pair from a real bundle case, that would give us a concrete fixture to test against (let me know if you’d rather share that over DM and I can set that up)

Thanks @Alan_G, that is exactly the fix we were hoping for. Using the server-confirmed added lines so the event reflects what actually made it into the cart would cover every Cart Transform case at once.

Happy to provide a failing fixture. The request shape is something like this:

Request:

{
  "items": [
    { "id": 100000000001, "quantity": 1, "properties": { "_bundle_id": "abc123" } },
    { "id": 100000000002, "quantity": 1, "properties": { "_bundle_id": "abc123" } },
    { "id": 100000000003, "quantity": 1, "properties": { "_bundle_id": "abc123" } }
  ]
}

Response (after the Cart Transform merge):

{
  "items": [
    { "id": 100000000001, "quantity": 1, "properties": { "_bundle_id": "abc123" } }
  ]
}

I can capture a real sanitized pair from a live bundle store if you want the exact payloads. DM works well for that, so feel free to set one up.