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
Never arm the reliable-purchase fallback from a render that already fired the purchase
The standard order-received render consumes the pending-purchase session
marker while emitting the purchase inline (wp_head), and woocommerce_thankyou
fires remember_order() later in the same request - re-seeding the marker and
re-arming the fallback for the very order the page just tracked. With "Do not
flag orders as being tracked" on (which disables every tracked-order
suppressor by design), the session endpoint then delivered the same purchase
a second time after page load.
remember_order() now bails when the request already pushed the purchase, and
on_thankyou() raises that request-scoped flag after a successful push so the
customized-page path is covered by the same guard. Also corrects the
handlePendingPurchase comment that claimed the browser guard stops a re-push
in the do-not-flag case - that branch skips the guard by design.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -84,7 +84,7 @@ Major rewrite of the plugin - please read the announcement post on gtm4wp.com be
84
84
85
85
#### Purchase tracking
86
86
87
-
* Added: new "Reliable purchase tracking" option (WooCommerce → Purchase tracking) for the most common cause of a missing `purchase` event. When the customer lands on a heavily customized thank-you page, or on the order-pay page instead of the order received page, the purchase event is now emitted on the next page they view in the same browser session. The placed order is remembered server-side (on `woocommerce_payment_complete`, the order-status change and `woocommerce_thankyou`) and de-duplicated with the existing order-tracked flag, browser cookie and order-age guards, so it is never counted twice. Off by default (experimental). It cannot capture orders where the buyer pays via an asynchronous gateway and never returns to the site - that case needs server-side tracking.
87
+
* Added: new "Reliable purchase tracking" option (WooCommerce → Purchase tracking) for the most common cause of a missing `purchase` event. When the customer lands on a heavily customized thank-you page, or on the order-pay page instead of the order received page, the purchase event is now emitted on the next page they view in the same browser session. The placed order is remembered server-side (on `woocommerce_payment_complete`, the order-status change and `woocommerce_thankyou`) and de-duplicated with the existing order-tracked flag, browser cookie and order-age guards, so it is never counted twice - and a page view that already carries the purchase event never arms the fallback in the first place, so the two cannot fire together on the same thank-you page even when the "Do not flag orders as being tracked" testing option has switched those guards off. Off by default (experimental). It cannot capture orders where the buyer pays via an asynchronous gateway and never returns to the site - that case needs server-side tracking.
88
88
* Added: new "Order statuses that trigger the purchase event" setting. The purchase now fires at order *placement* for the configured statuses (default: Processing, On hold, Completed), so Cash on Delivery (Processing) and bank transfer (On hold) orders are tracked at checkout even though payment clears later. Filterable via the new `gtm4wp_purchase_trackable_statuses` filter.
89
89
* Added: new "Custom order received (thank-you) page" option that fires the purchase event on a bespoke confirmation page - resolving the order from the current session - for themes or plugins that do not use the standard WooCommerce order received endpoint.
90
90
* Added: GA4 / Google Ads Enhanced Conversions user data is now included on the `purchase` event - SHA-256 hashed email, phone and name plus the plaintext address components Google expects - built from the order so guest checkouts are covered too. The phone number is converted to E.164 against the order's billing country before hashing, as Google requires, and is left out entirely when it cannot be placed in E.164 with confidence rather than sent as a hash that could never match. Opt-in through the existing "Customer data in data layer" option.
Copy file name to clipboardExpand all lines: readme.txt
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -332,7 +332,7 @@ Major rewrite of the plugin. Please read the announcement post on gtm4wp.com bef
332
332
333
333
**WooCommerce - Purchase tracking**
334
334
335
-
* Added: new "Reliable purchase tracking" option (WooCommerce → Purchase tracking) for the most common cause of a missing `purchase` event. When the customer lands on a heavily customized thank-you page, or on the order-pay page instead of the order received page, the purchase event is now emitted on the next page they view in the same browser session. The placed order is remembered server-side (on `woocommerce_payment_complete`, the order-status change and `woocommerce_thankyou`) and de-duplicated with the existing order-tracked flag, browser cookie and order-age guards, so it is never counted twice. Off by default (experimental). It cannot capture orders where the buyer pays via an asynchronous gateway and never returns to the site - that case needs server-side tracking.
335
+
* Added: new "Reliable purchase tracking" option (WooCommerce → Purchase tracking) for the most common cause of a missing `purchase` event. When the customer lands on a heavily customized thank-you page, or on the order-pay page instead of the order received page, the purchase event is now emitted on the next page they view in the same browser session. The placed order is remembered server-side (on `woocommerce_payment_complete`, the order-status change and `woocommerce_thankyou`) and de-duplicated with the existing order-tracked flag, browser cookie and order-age guards, so it is never counted twice - and a page view that already carries the purchase event never arms the fallback in the first place, so the two cannot fire together on the same thank-you page even when the "Do not flag orders as being tracked" testing option has switched those guards off. Off by default (experimental). It cannot capture orders where the buyer pays via an asynchronous gateway and never returns to the site - that case needs server-side tracking.
336
336
* Added: new "Order statuses that trigger the purchase event" setting. The purchase now fires at order *placement* for the configured statuses (default: Processing, On hold, Completed), so Cash on Delivery (Processing) and bank transfer (On hold) orders are tracked at checkout even though payment clears later. Filterable via the new `gtm4wp_purchase_trackable_statuses` filter.
337
337
* Added: new "Custom order received (thank-you) page" option that fires the purchase event on a bespoke confirmation page - resolving the order from the current session - for themes or plugins that do not use the standard WooCommerce order received endpoint.
338
338
* Added: GA4 / Google Ads Enhanced Conversions user data is now included on the `purchase` event - SHA-256 hashed email, phone and name plus the plaintext address components Google expects - built from the order so guest checkouts are covered too. The phone number is converted to E.164 against the order's billing country before hashing, as Google requires, and is left out entirely when it cannot be placed in E.164 with confidence rather than sent as a hash that could never match. Opt-in through the existing "Customer data in data layer" option.
Copy file name to clipboardExpand all lines: tests/unit/Modules/PurchaseTrackingTest.php
+58Lines changed: 58 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -395,6 +395,64 @@ public function test_remember_order_seeds_a_trackable_order_into_the_session():
395
395
$this->assertSame( 1001, $session->sets[ ProductData::PENDING_PURCHASE_SESSION_KEY ] ?? null, 'A placed, trackable order must be remembered for the next-page fallback.' );
396
396
}
397
397
398
+
/**
399
+
* Double-purchase regression: the standard order-received render consumes the
400
+
* pending-purchase marker while emitting the purchase inline (wp_head), and
401
+
* woocommerce_thankyou fires remember_order() later in the same request (the
402
+
* template body). Re-seeding there re-armed the reliable-purchase fallback for
403
+
* the very order the page already tracked, and the session endpoint then
404
+
* delivered the same purchase a second time after page load whenever the
405
+
* "Do not flag orders as being tracked" option had disabled the tracked-order
406
+
* suppressors. A request that already pushed the purchase must never seed.
$this->assertArrayNotHasKey( ProductData::PENDING_PURCHASE_SESSION_KEY, $session->sets, 'A request that already emitted the purchase inline must not re-arm the reliable-purchase fallback.' );
417
+
}
418
+
419
+
/**
420
+
* The customized-page half of the same regression: on_thankyou() pushes the
421
+
* purchase inline, and remember_order() runs on the SAME woocommerce_thankyou
422
+
* hook right after it. The push must raise the request-scoped flag so the
423
+
* sequence cannot seed the fallback for the order it just tracked.
$this->assertStringContainsString( '"event":"purchase"', $output, 'Precondition: the thank-you fallback must have pushed the purchase.' );
437
+
438
+
$tracking->remember_order( 1001 );
439
+
440
+
$this->assertArrayNotHasKey( ProductData::PENDING_PURCHASE_SESSION_KEY, $session->sets, 'The thank-you render that pushed the purchase must not re-arm the fallback on the same hook.' );
441
+
}
442
+
443
+
/**
444
+
* The negative branch: when on_thankyou() bails without pushing (here: the
445
+
* eligibility gauntlet rejects an already-tracked order), the flag must stay
446
+
* down so the checkout-time seed hooks keep working unchanged.
$this->assertArrayNotHasKey( 'gtm4wp_woocommerce_purchase_data_pushed', $GLOBALS, 'A bailed thank-you fallback must not claim the purchase was pushed.' );
0 commit comments