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.

Hey @Anton, perfect, thanks for confirming - I’ll set up a DM with you here to grab that additional info.

Hey @Alan_G,

We’re currently seeing the same bug on one of our client sites, and it’s starting to impact performance with our ads.

Is there any update on what’s causing it or when it might be fixed by any chance? If not, are there any workarounds we can put in place on the store in the meantime?

I’d greatly appreciate your help. Thanks!

Hey @nawzad - thanks for checking in, just wanted to share some info from my DMs with Anton here. The TL;DR is that this does line up with a known issue we’re tracking on our end.

The bulk /cart/add.js request sends multiple component lines, while the Cart Transform returns a smaller number of merged bundle lines. That mismatch causes shop_events_listener.js to stop before product_added_to_cart is emitted, even though the cart add itself succeeds unfortunately.

I don’t have a fix timeline to share yet. The current workaround is to avoid sending the bundle components in one bulk request if possible, for example, using separate /cart/add.js requests so each request and response stays aligned. I definitely get that it’s not super ideal for larger bundles because it can add latency, lose atomicity, and emit multiple add-to-cart events though.

If you’re seeing the same Payload body and response have different number of items warning, this is very likely the same issue. I’ll keep this thread updated if I get anything concrete back on our end.

Hi Alan,

Thanks very much for getting back to me.

We’re looking into this now and I’m passing it over to our developer to see if it’s something we can test and whether it resolves the issue.

Thanks again, and fingers crossed Shopify share a fix soon.

Unfortunately, many stores using our app, which bundles products, are experiencing the following issue:

Error in handleFetchRequest: Payload body and response have different number of items

Because of this issue, when a bundle is added to the cart, merchants are unable to use web pixels correctly. This negatively affects their advertising campaigns and, ultimately, their sales.

Please resolve this issue as soon as possible, as many merchants are very frustrated by it.

Hey @Dmitri_Pavlutin - thanks for flagging the wider impact here. This may be related to an issue I’ve seen before.

Could you share:

  • Approximately how many stores are affected.
  • Whether they consistently show the Payload body and response have different number of items warning.
  • A sanitized failing /cart/add.js request and response pair.

If you’d rather share any of that privately, I can set up a DM with you here. I’ll pass the additional impact along on our end and keep this thread updated if I get anything concrete back.

Hi @Alan_G,

Thank you for looking into this.

  • Approximately how many stores are affected.

Unfortunately I don’t have an exact number at hands, but in the last months we had many customer support questions on why the meta pixel wasn’t working when bundling was enabled. Just recently a merchant stated he will uninstall the app if the issue is not fixed since correctly working Facebook ads are important for his store.

  • Whether they consistently show the Payload body and response have different number of items warning.
  • A sanitized failing /cart/add.js request and response pair.

Yes the error is permanent when bundling is enabled. Please open a DM and I will send you a link to the merchant’s bundle builder where the problem can be reproduced.

Thanks!

Hey @Dmitri_Pavlutin - sending you a DM!

I am just now running into this issue now. I moved a client away from custom faux-bundles linked via line item property with parallel items over into a cart transformation system and the ads / gtm side of the business inquired about a complete drop off in tracking for those products.

If I have a merge operation, I can now see added to cart come in for each component after a single request. When the merged item enters the cart and I remove it, no product_removed_from_cart fires off at all. In this case I really would want events about that final merged single item.

If I send an item to the cart that would be an expand operation, that seems to get me events for that single item that expands into many, but of course none of the events have the expanded items. In this case, I would like a little bit of information about those expanded items.

My go-to right now is to just dispatch a custom event up to my custom pixel that sends some DataLayer event as needed, of course that only helps that particular tracker which is okay for now.

I noticed that when I use the Shopify Standard Actions and Events, I will get cart line updates for the bundle components as I would expect to see the Shopify Pixel Events reciprocate.

Hey @Matthew_Crigger - thanks for reaching out and the extra context here. Seeing add events for the source components but no product_removed_from_cart when the merged line is removed seems like an additional bundle mapping issue beyond the original bulk-add mismatch in this thread.

Could you share:

  • Whether the original Payload body and response have different number of items warning still appears during the merge add?
  • Whether removal uses /cart/change.js or /cart/update.js, along with a sanitized response body?

Happy to pass the removal and expand cases along separately and keep this thread updated if I get any further info I can share.

Hey Alan, I apologize for the delay in getting back to you and sharing some info with the class. I sleep about 2-3 hours a day and my sense of time is so distorted you could tell me that I left my comment on Monday.

I did notice that change.js vs update.js really changes things up. If you use update.js you tend to be out of luck of triggering events. When you use change.js you will trigger the pixel events (not the exact ones I want for “complex” situations, but for everything else that is good!)

I feel like maybe I placebo’d myself but using the standard actions system seems to feel more reliable than the unified cart manager I made that uses the AJAX API. I do tie in a request to the section rendering API typically to fetch “enriched JSON” for more complex data for client needs that the regular cart.json won’t return (some are really custom, others are compilations of various discount totals, combined compare at pricing across all line items, de-duped discount information to collapse discount codes, line item discounts applied etc so I have a single source to evaluate, and others).

Like I had said, when I send an expand based add to cart I send a single item, I get the add to cart for that initial item, and nothing for the expanded items, thus taking me from 1 item added now to 2 items added. If I send a merge based, I now get two separate add to cart events when previously I did not which is why I think whatever I did to move to the standard actions may have helped me. In that case my add to cart events will represent the individual products, but not the merged parent item (the bundle I actually want to track in addition to the components).

Now the cartLine we have access to has very minimal data, so unfortunately I can’t access line item properties that would contain a property I have of a JSON string that I could parse and derive everything I need to manipulate the data and push events up into GTM / GA4 as needed. In the meantime, I can await my enriched JSON or pre-interaction events in my custom state and just dispatch a fake event “bundle_added_to_cart” “bundle_removed_from_cart” (maybe take note on that and just make a separate event for these) that pushes up a mock build of the cartLine object.

However, even if I hack this via theoretical hope of line item property access OR custom analytics event I am out of luck for any pixel that is NOT custom that I haven’t added support for.

This makes me feel a bit… unwell about how apps like Google & Youtube are tracking add to carts. In my custom pixel (which I only want to use for GTM ideally, or anything the Google & Youtube app does not have a map for) I can handle it for now.

If it helps, who knows, I can DM you a link to a preview store for the client along with the particular products I need to track. My goal is to drop custom theme events and move into pixels, but I do feel like my hands are tied to retain theme level only dispatches.

I just really don’t want to have to maintain a GTM script in the theme, and also in a pixel.

Hey @Alan_G I realize via the community forums I have no idea how to directly send you the URLs I am testing events with. Can I send via email or something?

Hey @Matthew_Crigger - absolutely, I’ll send you a DM here so you can share the preview store and product URLs privately. If you can include the exact steps for the merge, expand, and removal cases, plus whether each removal uses /cart/change.js or /cart/update.js, that’ll help me separate the behaviours and pass along clear repros on our end. Thanks!