You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Ship the single-product data in a span so the block add-to-cart stays interactive
WooCommerce's blockified Add to Cart + Options block buffers the output of the
classic add-to-cart hooks and renders a legacy full-page POST form (no
Interactivity API submit handler) as soon as that buffer contains any INPUT,
TEXTAREA, SELECT, BUTTON or FORM tag. The hidden
<input name="gtm4wp_product_data"> printed on woocommerce_after_add_to_cart_button
is exactly such a tag, so with e-commerce tracking enabled every block-theme
product page lost its interactive add to cart and did a full page reload
(GitHub #462, verified against the WC 10.9.1 source).
The product data now travels in a hidden
<span class="gtm4wp_single_productdata" data-gtm4wp_product_data="..."> instead,
which the has_form_elements() scan ignores. The class deliberately differs from
the .gtm4wp_productdata list markup, which the tracker sweeps page-wide into
view_item_list impressions a product detail page must not join. Both JS readers
(the simple-product click path and the found_variation handler) select the span
first and keep reading the old input as a fallback, so a cached page rendered by
an older plugin version keeps tracking through the upgrade. Nothing server-side
ever read the posted input value, so dropping the form field loses nothing.
Regression tests pin both directions: the span markup is asserted present and
form elements asserted absent on the PHP side, and the tracker suite drives the
span-only markup for both the add_to_cart click and the variation view_item.
The has_form_elements() coupling is registered as upstream row U114.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: .upstream/upstream-review-checklist.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -265,6 +265,7 @@ have none, which is exactly why they are `every-run`.
265
265
| U30 | Cross-sell block selectors `.wp-block-woocommerce-cart-cross-sells-block`, `.wc-block-cart-cross-sells-product`|`js/frontend/gtm4wp-woocommerce-blocks.js`| WC Blocks markup | U77 | silent-missing | on-WC-release |[x] 2026-08-06 (S1a) D12 fixed — prefix corrected and pinned by a regression test that fails on the old value |
266
266
| U97 | **The cart-fragments delivery channel** (cache-safe data layer only). Three claims, all silent if wrong: (a) the `woocommerce_add_to_cart_fragments` filter still exists and its selector⇒HTML contract is unchanged — applied in `WC_AJAX::get_refreshed_fragments()`, reached by the `get_refreshed_fragments`, `add_to_cart` and `remove_from_cart` endpoints; (b) WC still re-applies fragments from its own `sessionStorage` cache on a normal page load, which is what delivers the block when no cart mutation happened; (c) **the handle `wc-cart-fragments` is still registered under that name** — we now `wp_enqueue_script()` it ourselves, unguarded, and WP drops an unregistered queued handle with no notice. Covered by the L-1 support policy that `WC_Frontend_Scripts`' own docblock grants third-party enqueueing. **Do not "fix" (c) by adding a `wp_script_is( …, 'registered' )` guard** — our callback and WC's `load_scripts()` share `wp_enqueue_scripts` priority 10, so the guard would be a race that silently skips the enqueue; queue resolution happens at print time | `src/Modules/WooCommerce/PageDataLayer.php` (`add_visitor_cart_fragment`, `output_visitor_cart_placeholder`), `src/Modules/WooCommerce/WooCommerceModule.php` (`enqueue_scripts`), `js/frontend/gtm4wp-visitor-data.js` | WC `includes/class-wc-frontend-scripts.php`, `includes/widgets/class-wc-widget-cart.php`, `includes/class-wc-ajax.php`, `client/legacy/js/frontend/cart-fragments.js` | U77, U70 | silent-missing | on-WC-release | [x] 2026-08-06 read on `trunk` (11.1.0-dev); `cart-fragments.js` byte-identical on tags `11.0.0` and `10.4.0`. **Measured, not assumed:** the handle is registered unconditionally by `load_scripts()` (sole gate `did_action('before_woocommerce_init')`) but WC enqueues it from exactly **one** frontend path, `WC_Widget_Cart::widget()`, which returns early when `woocommerce_widget_cart_is_hidden` is true — **default `is_cart() \|\| is_checkout()`** — and the Mini-Cart *block* never enqueues it (it uses a parallel Store API layer on `localStorage.storeApiCartHash`). That is why (c) was failing in the field. `wc_load_cart_fragments()`, `woocommerce_enqueue_cart_fragments` and `woocommerce_cart_fragments_refresh` **do not exist** — do not look for them. Cost claim behind the changelog wording: the refresh XHR fires once per *tab* (its cache is `sessionStorage`), and on **every** page load when Web Storage is blocked (`cart-fragments.js` has no guard for that) |
267
267
| U99 | **Legacy product grid block container classes → GA4 list identity.** The 8 `wp-block-{block_name}` classes `AbstractProductGrid::get_container_classes()` emits, each mapped to a hardcoded `item_list_name` **and** `item_list_id`, plus the `.wc-block-grid` / `.wc-block-grid__products` wrapper shape the lookup walks. WC's `woocommerce_blocks_product_grid_item_html` carries no block context, so PHP writes a generic placeholder pair and the browser resolves the real identity from the container class. The 8 ids are the same literals `list_identity()` gives those lists on the Product Collection path (U26) — one list, two files, and they must not drift (UC-6) | `js/frontend/gtm4wp-woocommerce.js` (`gtm4wp_product_block_names` + its three `gtm4wp_update_json_in_node` calls); placeholder pair from `src/Modules/WooCommerce/ListTracking.php` | `AbstractProductGrid::get_container_classes()` + each subclass's `$block_name`, via `gh api repos/woocommerce/woocommerce/contents/…` | U77 — **soft-deprecated since WC 9.5: every legacy grid `block.json` carries `"supports": { "inserter": false }` (confirmed at 11.0.0), the blocks stay registered and server-rendered for existing content, and the successor is `woocommerce/product-collection`. Removal, not renaming, is the expected end state; watch WC deprecation notes and the subclass count, and do NOT drop our support — widget areas and saved content still render these** | silent-wrong | on-WC-release | [x] 2026-08-06 measured on the **11.0.0 tag**: `search/code repo:woocommerce/woocommerce "extends AbstractProductGrid"` → `total_count: 8` (`ProductNew, ProductOnSale, ProductTopRated, ProductCategory, ProductTag, HandpickedProducts, ProductBestSellers, ProductsByAttribute`), map keys = `wp-block-` + each `$block_name`, so the map is exactly complete: 8 keys vs 8 subclasses. `get_container_classes()` verified to put `wc-block-grid` and `wp-block-{name}` on the same element. Pinned by the 8-row table in `js/frontend/test/woocommerce-tracker.test.js` |
268
+
| U114 | **`AddToCartWithOptions::has_form_elements()` legacy-mode scan** (blockified Add to Cart + Options block, WC 10.0+). Two claims, both silent if wrong: (a) the block buffers the output of the classic `woocommerce_before/after_add_to_cart_*` hooks and renders a classic POST form (no `data-wp-on--submit`, interactive add to cart off) when a buffer contains an `INPUT`, `TEXTAREA`, `SELECT`, `BUTTON` or `FORM` tag — so the single-product tracking markup must stay a non-form element (hidden `span`, #462); (b) a `SPAN` does not trip the scan. If the tag list widens, the interactive add to cart dies again on block themes with no error and a green suite | `src/Modules/WooCommerce/ListTracking.php` `single_add_to_cart_tracking()` | `plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php` (`has_form_elements()`, `$legacy_mode`), via `gh api repos/woocommerce/woocommerce/contents/…` | U77 | silent-wrong | on-WC-release | [x] 2026-08-13 read at tag 10.9.1: tag list `INPUT, TEXTAREA, SELECT, BUTTON, FORM`; `$hooks_after` buffers `woocommerce_after_add_to_cart_button`; legacy mode omits `data-wp-on--submit`. Our side pinned by `ListTrackingTest::test_single_add_to_cart_tracking_output_contains_no_form_elements()` |
268
269
269
270
| U113 | **The visitor checks WooCommerce's `order_received()` applies once it has resolved an order**, which return before `woocommerce_thankyou` and are therefore invisible to this plugin. Read so the data layer includes `orderData.customer`, `new_customer`/`customer_type` and the purchase event's `user_data` exactly when WooCommerce itself renders the order — parity in **both** directions, because wherever upstream renders, the page body is already showing this visitor the order, so withholding more than upstream deletes tracking data without hiding anything. Five couplings, measured at release tags (not trunk): **(a) `WC_Shortcode_Checkout::guest_should_verify_email( WC_Order, string ): bool`, `private static` — the feature-detect symbol.** Both gates and this member shipped together in **7.9.0** (absent at 7.8.0, present 7.9.0→11.0.0); `method_exists()` sees private members and triggers `WC_Autoloader` (`wc_shortcode_` → `shortcodes/`, mapped since ≤5.0.0), so the probe needs no load-order luck. Absent ⇒ a 5.0–7.8.x WooCommerce that renders the order to any valid key holder ⇒ nothing to mirror, **nothing withheld, no version compared anywhere**. **(b)** the known-shopper login gate: **behaviour 7.9.0 (unconditional); its filter `woocommerce_order_received_verify_known_shoppers` (default `true`, 1 arg) only 8.4.0** — one upstream line, two `@since` values; with the filter off upstream still routes a non-owner into the guest email verification, whose only customer-id term is the logged-in-owner short-circuit. **(c)** `Automattic\WooCommerce\Internal\Utilities\Users::should_user_verify_order_email( $order_id, $supplied_email = null, $context = 'view' )` — `public static`, order **id** not object; **the METHOD is 8.6.0+ while the CLASS exists from ≥7.7.0 — re-verify with `method_exists`; `class_exists` alone reports the wrong half**. `Internal` namespace, no compatibility promise (UC-2), hence the guard. **(d)** `woocommerce_order_email_verification_grace_period` — **`@since` 8.0.0, 3 args**, homed in the shortcode 8.0.0–8.5.x and in the helper from 8.6.0; upstream compares elapsed `<=` grace (mirrored exactly); **7.9.x has no grace at all** — the one window where the mirror is laxer than upstream, bounded by the order max-age gate. **(e)** `woocommerce_order_email_verification_required` — **`@since` 7.9.0, 3 args**, upstream's final say and documented opt-out, applied only past the grace short-circuit (both facts mirrored). On 7.9.0–8.5.x (helper absent) the fallback mirrors (b)–(e) term by term for **every order shape reaching it**; the three request-identity terms — session email match, the POSTed-email escape hatch (`check_submission` / nonce `wc_verify_email`), `read_private_shop_orders` — are deliberately not modelled and each fails **closed** | `src/Modules/WooCommerce/PageDataLayer.php` `woocommerce_hides_order_from_visitor()` | `includes/shortcodes/class-wc-shortcode-checkout.php` `order_received()` + `guest_should_verify_email()`; `src/Internal/Utilities/Users.php` | U77 | silent-wrong | on-WC-release | [x] 2026-08-12 read at tags 5.0.0 / 7.8.0 / 7.9.0 / 8.0.0 / 8.4.0 / 8.5.2 / 8.6.0 / 11.0.0; an executable old/new/upstream decision table swept every version window × order shape × filter state with zero unexplained divergences |
Copy file name to clipboardExpand all lines: CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -119,6 +119,7 @@ Major rewrite of the plugin - please read the announcement post on gtm4wp.com be
119
119
* Fixed: when "Exclude tax from revenue" is enabled, per-item prices on the `purchase` event are now also reported excluding tax. Previously the transaction `value` was tax-exclusive while the item prices followed the shop's display setting (often tax-inclusive), so GA4 item-level revenue (product performance) did not reconcile with the transaction total (sales performance).
120
120
* Fixed: variable **subscription** products now keep their variant data in ecommerce tracking. Variations were detected by an exact `variation` product-type match, but WooCommerce Subscriptions variations report `subscription_variation`, so their `item_variant`, `item_group_id` and parent-derived `item_category`/`item_brand` were dropped (most visibly on the `purchase` event). Variations are now detected structurally (any `WC_Product_Variation`), covering subscriptions and similar extensions.
121
121
* Fixed: the dynamic-remarketing "Product ID prefix" is now kept on variations. When a variation was selected on a variable product page, the browser swapped in the variation id and dropped the configured prefix from the `id` field used for Google/Meta catalog matching; the prefix is now re-applied to the variation's `id` (the unprefixed `item_id` is unchanged).
122
+
* Fixed: adding a product to the cart no longer forces a full page reload on stores using WooCommerce's blockified **Add to Cart + Options** block (block themes, WooCommerce 10.9+). With e-commerce tracking enabled, the plugin printed a hidden input field into the add-to-cart form, and WooCommerce deliberately renders a classic full-page POST form instead of the interactive one when a plugin prints form fields into it - so the interactive add to cart was disabled on every product page while GTM4WP was active. The product data now travels in a hidden span's data attribute (the same pattern product lists already use), which WooCommerce's check ignores. Tracking itself is unchanged: classic (non-block) product templates keep working exactly as before, and the tracker still reads the old hidden input as a fallback so cached pages keep tracking through the upgrade. As reported on GitHub (#462).
122
123
* Fixed: the `[add_to_cart]` shortcode button now fires an `add_to_cart` event. A standalone shortcode button is rendered outside a product loop, so it never received the hidden product-data markup that product-list items get; the GA4 item data is now attached to the button itself so a click can be tracked. Product lists are unaffected — they already carry the data.
123
124
* Fixed: the product-page `add_to_cart` event is no longer fired when the browser blocks the add-to-cart form submit because a required field is empty (e.g. a required Product Add-ons field). The click now respects the form's HTML5 validity, so a rejected add no longer produces a false `add_to_cart`.
124
125
* Fixed: `add_to_cart` and `remove_from_cart` now always report `quantity` as a number, and report it the same way on every surface. Three symptoms of one cause: a product form with no quantity field at all - some themes and product add-on plugins render none - emitted `quantity: null` and `value: 0`; the cart page reported a string where the mini-cart reported a number for the very same product; and a cart line set to zero fired a removal event on the cart page while the mini-cart correctly suppressed it. Every quantity now goes through one parser, so the type and the zero handling are identical everywhere. **Check your GTM setup if a trigger or variable compares `quantity` against a string.**
0 commit comments