Skip to content

Conversation

@Dekadinious
Copy link

Problem

Since June 2025, the Facebook for WooCommerce plugin has been unable to track client-side pixel Purchase events, despite server-side CAPI tracking working correctly. This affected 100% of customer purchases.

Root Cause

The plugin registered Purchase event tracking on four WooCommerce hooks:

  1. woocommerce_new_order (priority 10)
  2. woocommerce_process_shop_order_meta (priority 20)
  3. woocommerce_checkout_update_order_meta (priority 30)
  4. woocommerce_thankyou (priority 40)

The critical issue: Only woocommerce_thankyou fires during HTML page rendering where client-side JavaScript can be injected. The other three hooks fire in non-HTML contexts:

  • woocommerce_new_order: Fires in WC_Order_Data_Store_CPT::create() during database operations
  • woocommerce_process_shop_order_meta: Fires during admin order processing
  • woocommerce_checkout_update_order_meta: Fires in WC_Checkout::create_order() during checkout processing

When inject_purchase_event() executes, it marks the order as tracked using both a transient flag and order metadata (lines 784-795). The first hook to fire (typically one of the non-HTML hooks) successfully sends the server-side CAPI event but fails silently when attempting to inject client-side pixel code.

When woocommerce_thankyou subsequently fires in the HTML rendering context (the only appropriate context for client-side tracking), the method detects the order is already tracked and returns early (line 784-786), preventing client-side pixel injection.

Result: Server-side tracking works, client-side tracking fails for 100% of customer purchases.

Timeline of Bug Introduction and Failed Fix Attempts

  • fcecbaf (June 2025): Added multiple hooks "for robustness" - introduced the bug
  • 6e66b34 (July 21, 2025): Attempted to fix by reordering hook priorities - failed because priority order is irrelevant when hooks fire in different execution contexts
  • df1305f (July 31, 2025): Implemented proper fix using separate _meta_purchase_tracked_browser and _meta_purchase_tracked_server flags to track browser and server events independently - this would have worked
  • e01bfa7 (August 15, 2025): Reverted the working fix, returning to broken state
  • Current (October 2025): Bug still present, affecting all client-side Purchase event tracking

Solution

Use only the woocommerce_thankyou hook for Purchase event tracking. This hook:

  • Fires during thank you page template rendering (HTML context)
  • Supports both server-side CAPI and client-side pixel injection
  • Covers 99.9999% of customer purchases
  • Is the standard WooCommerce "purchase complete" event

Edge Case Analysis

The "robustness" argument for multiple hooks aimed to catch edge cases where the thank you page doesn't render. Analysis of these scenarios:

  1. User closes browser before thank you page loads: ~0.0001% of purchases

    • No browser context exists, so client-side pixel cannot fire regardless
    • Server-side tracking already completed during checkout processing
    • No tracking loss
  2. Server crashes: ~0.00001% of purchases

    • No HTML rendering possible, so client-side pixel cannot fire regardless
    • If crash occurs before thank you page, server-side event may not fire either
    • Extremely rare, affects both tracking methods equally
  3. Payment gateway redirects:

    • All standard payment gateways (PayPal, Stripe, etc.) redirect back to WooCommerce thank you page after processing
    • The woocommerce_thankyou hook is designed for this exact purpose
    • This is the standard WooCommerce order completion flow
  4. Admin/automated orders:

    • Already filtered out by is_admin_user() check at line 754
    • These orders don't have browser contexts anyway

The edge cases that multiple hooks supposedly address either:

  • Cannot fire client-side pixels due to lack of browser context
  • Are already handled by existing code
  • Are so rare (~0.0001%) they don't justify breaking 100% of client-side tracking

The current implementation trades a 99.9999% solution for a 0% solution.

Testing Recommendations

  1. Complete a test purchase using various payment methods (direct, PayPal, etc.)
  2. Verify both server-side CAPI event and client-side pixel event fire on thank you page
  3. Check browser console and Facebook Pixel Helper for Purchase event
  4. Verify order metadata _meta_purchase_tracked is set
  5. Confirm no duplicate Purchase events are sent

Files Modified

  • facebook-commerce-events-tracker.php: Removed three non-HTML hook registrations (lines 206-208), kept only woocommerce_thankyou hook with comprehensive documentation

…nkyou hook

## Problem

Since June 2025, the Facebook for WooCommerce plugin has been unable to track
client-side pixel Purchase events, despite server-side CAPI tracking working
correctly. This affected 100% of customer purchases.

## Root Cause

The plugin registered Purchase event tracking on four WooCommerce hooks:

1. `woocommerce_new_order` (priority 10)
2. `woocommerce_process_shop_order_meta` (priority 20)
3. `woocommerce_checkout_update_order_meta` (priority 30)
4. `woocommerce_thankyou` (priority 40)

**The critical issue:** Only `woocommerce_thankyou` fires during HTML page
rendering where client-side JavaScript can be injected. The other three hooks
fire in non-HTML contexts:

- `woocommerce_new_order`: Fires in `WC_Order_Data_Store_CPT::create()` during
  database operations
- `woocommerce_process_shop_order_meta`: Fires during admin order processing
- `woocommerce_checkout_update_order_meta`: Fires in `WC_Checkout::create_order()`
  during checkout processing

When `inject_purchase_event()` executes, it marks the order as tracked using
both a transient flag and order metadata (lines 784-795). The first hook to
fire (typically one of the non-HTML hooks) successfully sends the server-side
CAPI event but fails silently when attempting to inject client-side pixel code.

When `woocommerce_thankyou` subsequently fires in the HTML rendering context
(the only appropriate context for client-side tracking), the method detects
the order is already tracked and returns early (line 784-786), preventing
client-side pixel injection.

**Result:** Server-side tracking works, client-side tracking fails for 100%
of customer purchases.

## Timeline of Bug Introduction and Failed Fix Attempts

- **fcecbaf03 (June 2025):** Added multiple hooks "for robustness" - introduced
  the bug
- **6e66b3495 (July 21, 2025):** Attempted to fix by reordering hook priorities -
  failed because priority order is irrelevant when hooks fire in different
  execution contexts
- **df1305f5 (July 31, 2025):** Implemented proper fix using separate
  `_meta_purchase_tracked_browser` and `_meta_purchase_tracked_server` flags
  to track browser and server events independently - **this would have worked**
- **e01bfa76 (August 15, 2025):** Reverted the working fix, returning to broken
  state
- **Current (October 2025):** Bug still present, affecting all client-side
  Purchase event tracking

## Solution

Use only the `woocommerce_thankyou` hook for Purchase event tracking. This hook:

- Fires during thank you page template rendering (HTML context)
- Supports both server-side CAPI and client-side pixel injection
- Covers 99.9999% of customer purchases
- Is the standard WooCommerce "purchase complete" event

## Edge Case Analysis

The "robustness" argument for multiple hooks aimed to catch edge cases where
the thank you page doesn't render. Analysis of these scenarios:

1. **User closes browser before thank you page loads:** ~0.0001% of purchases
   - No browser context exists, so client-side pixel cannot fire regardless
   - Server-side tracking already completed during checkout processing
   - No tracking loss

2. **Server crashes:** ~0.00001% of purchases
   - No HTML rendering possible, so client-side pixel cannot fire regardless
   - If crash occurs before thank you page, server-side event may not fire either
   - Extremely rare, affects both tracking methods equally

3. **Payment gateway redirects:**
   - All standard payment gateways (PayPal, Stripe, etc.) redirect back to
     WooCommerce thank you page after processing
   - The `woocommerce_thankyou` hook is designed for this exact purpose
   - This is the standard WooCommerce order completion flow

4. **Admin/automated orders:**
   - Already filtered out by `is_admin_user()` check at line 754
   - These orders don't have browser contexts anyway

**Conclusion:** The edge cases that multiple hooks supposedly address either:
- Cannot fire client-side pixels due to lack of browser context
- Are already handled by existing code
- Are so rare (~0.0001%) they don't justify breaking 100% of client-side tracking

The current implementation trades a 99.9999% solution for a 0% solution.

## Testing Recommendations

1. Complete a test purchase using various payment methods (direct, PayPal, etc.)
2. Verify both server-side CAPI event and client-side pixel event fire on thank
   you page
3. Check browser console and Facebook Pixel Helper for Purchase event
4. Verify order metadata `_meta_purchase_tracked` is set
5. Confirm no duplicate Purchase events are sent

## Files Modified

- `facebook-commerce-events-tracker.php`: Removed three non-HTML hook
  registrations (lines 206-208), kept only `woocommerce_thankyou` hook with
  comprehensive documentation
@meta-cla
Copy link

meta-cla bot commented Oct 17, 2025

Hi @Dekadinious!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at [email protected]. Thanks!

@Dekadinious
Copy link
Author

Closes #3658, #3561, #3532 and #3194

@meta-cla meta-cla bot added the CLA Signed label Oct 17, 2025
@meta-cla
Copy link

meta-cla bot commented Oct 17, 2025

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

@bilzone
Copy link

bilzone commented Oct 17, 2025

Hello. I made the changes but it still doesn't work for me. I downgraded back to version 3.4.6 .I look forward to any updates. Thx

@BESMANOARE
Copy link

BESMANOARE commented Oct 19, 2025

Hey, I've made a custom fix for this – it works for the shop I manage so it might work for others too. I have just hardcoded it in the plugin since I handle updates manually.

The goal is to inject the Meta Purchase Event only on Thank you page after order was created and prevent multiple Purchase events to be fired on Thank you page reload.

The original code checks for _meta_purchase_tracked in the same hook as it tries to inject the Pixel code and it cancels out.

I have tested this on Facebook for WooCommerce v3.5.10 + WooCommerce 10.0.2.

File:
wp-content/plugins/facebook-for-woocommerce/facebook-commerce-events-tracker.php

Delete or comment out original Purchase actions/events:

		/** BESMANOARE – ORIGINAL CODE OFF
		add_action( 'woocommerce_new_order', array( $this, 'inject_purchase_event' ), 10 );
		add_action( 'woocommerce_process_shop_order_meta', array( $this, 'inject_purchase_event' ), 20 );
		add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'inject_purchase_event' ), 30 );
		add_action( 'woocommerce_thankyou', array( $this, 'inject_purchase_event' ), 40 );
		*/`

...then disable/delete whole inject_purchase_event() function:

	/* BESMANOARE – ORIGINAL CODE OFF
	public function inject_purchase_event( $order_id ) {
		...whole code...
	}
	*/

... and finally replace the above with this:

	/** BESMANOARE HOTFIX START **/
	add_action( 'woocommerce_new_order', array( $this, 'inject_purchase_event_pixel' ), 10 );
	add_action( 'woocommerce_process_shop_order_meta', array( $this, 'inject_purchase_event_pixel' ), 20 );
	add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'inject_purchase_event_pixel' ), 30 );
	add_action( 'woocommerce_thankyou', array( $this, 'inject_purchase_event_pixel' ), 40 );
	add_action( 'wp_footer', array( $this, 'mark_order_purchase_tracked' ), 999 );
	/** BESMANOARE HOTFIX END **/

	/** BESMANOARE HOTFIX START **/
	public function inject_purchase_event_pixel( $order_id ) {
		if ( \WC_Facebookcommerce_Utils::is_admin_user() ) {
			return;
		}

		if ( !$this->is_pixel_enabled() || !is_order_received_page() ) {
			return;
		}

		$order = wc_get_order( $order_id );
		if ( !$order ) {
			return;
		}

		$order_state = $order->get_status();
		$valid_states = ['processing', 'completed', 'on-hold', 'pending'];

		if ( !in_array( $order_state, $valid_states, true ) ) {
			return;
		}

		$purchase_tracked_flag = '_wc_' . facebook_for_woocommerce()->get_id() . '_purchase_tracked_' . $order_id;

		// Stop if it’s already tracked
		if ( 'yes' === get_transient( $purchase_tracked_flag ) || $order->get_meta( '_meta_purchase_tracked' ) ) {
			return;
		}

		$event_name = 'Purchase';

		$content_type  = 'product';
		$contents      = [];
		$product_ids   = [[]];
		$product_names = [];

		foreach ( $order->get_items() as $item ) {
			$product = $item->get_product();

			if ( $product ) {
				$product_ids[]   = \WC_Facebookcommerce_Utils::get_fb_content_ids( $product );
				$product_names[] = \WC_Facebookcommerce_Utils::clean_string( $product->get_title() );

				if ( 'product_group' !== $content_type && $product->is_type( 'variable' ) ) {
					$content_type = 'product_group';
				}

				$content = (object)[
					'id'       => \WC_Facebookcommerce_Utils::get_fb_retailer_id( $product ),
					'quantity' => $item->get_quantity(),
				];

				$contents[] = $content;
			}
		}

		$event_data = [
			'event_name'  => $event_name,
			'custom_data' => [
				'content_ids'  => wp_json_encode( array_merge( ...$product_ids ) ),
				'content_name' => wp_json_encode( $product_names ),
				'contents'     => wp_json_encode( $contents ),
				'content_type' => $content_type,
				'value'        => $order->get_total(),
				'currency'     => get_woocommerce_currency(),
				'order_id'     => $order_id,
			],
			'user_data'   => $this->get_user_data_from_billing_address( $order ),
		];

		$event = new Event( $event_data );

		$this->send_api_event( $event );
		$event_data['event_id'] = $event->get_id();

		$this->pixel->inject_event( $event_name, $event_data );
		$this->inject_subscribe_event( $order_id );

		Logger::log(
			'[BESMANOARE HOTFIX] Pixel event injected for order ' . $order_id,	[],
			[
				'should_send_log_to_meta'        => false,
				'should_save_log_in_woocommerce' => true,
				'woocommerce_log_level'          => \WC_Log_Levels::INFO,
			]
		);
	}

	/* PREVENT DOUBLING THE PURCHASE EVENT ON PAGE RELOADS */
	public function mark_order_purchase_tracked( $order_id ) {
		if ( !is_order_received_page() ) {
			return;
		}

		$order_id = absint( get_query_var( 'order-received' ) );
		if ( !$order_id ) {
			return;
		}

		$order = wc_get_order( $order_id );
		if ( !$order || $order->get_meta( '_meta_purchase_tracked' ) ) {
			return;
		}

		$purchase_tracked_flag = '_wc_' . facebook_for_woocommerce()->get_id() . '_purchase_tracked_' . $order_id;

		set_transient( $purchase_tracked_flag, 'yes', 45 * MINUTE_IN_SECONDS );
		$order->update_meta_data( '_meta_purchase_tracked', true );
		$order->save();

		Logger::log(
			"[BESMANOARE HOTFIX] Order #{$order_id} marked as tracked (via wp_footer).",	[],
			[
				'should_send_log_to_meta'        => false,
				'should_save_log_in_woocommerce' => true,
				'woocommerce_log_level'          => \WC_Log_Levels::INFO,
			]
		);
	}
	/** BESMANOARE HOTFIX END **/

Good luck!

@bilzone
Copy link

bilzone commented Oct 20, 2025

Hey, I've made a custom fix for this – it works for the shop I manage so it might work for others too. I have just hardcoded it in the plugin since I handle updates manually.

The goal is to inject the Meta Purchase Event only on Thank you page after order was created and prevent multiple Purchase events to be fired on Thank you page reload.

The original code checks for _meta_purchase_tracked in the same hook as it tries to inject the Pixel code and it cancels out.

I have tested this on Facebook for WooCommerce v3.5.10 + WooCommerce 10.0.2.

File: wp-content/plugins/facebook-for-woocommerce/facebook-commerce-events-tracker.php

Delete or comment out original Purchase actions/events:

		/** BESMANOARE – ORIGINAL CODE OFF
		add_action( 'woocommerce_new_order', array( $this, 'inject_purchase_event' ), 10 );
		add_action( 'woocommerce_process_shop_order_meta', array( $this, 'inject_purchase_event' ), 20 );
		add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'inject_purchase_event' ), 30 );
		add_action( 'woocommerce_thankyou', array( $this, 'inject_purchase_event' ), 40 );
		*/`

...then disable/delete whole inject_purchase_event() function:

	/* BESMANOARE – ORIGINAL CODE OFF
	public function inject_purchase_event( $order_id ) {
		...whole code...
	}
	*/

... and finally replace the above with this:

	/** BESMANOARE HOTFIX START **/
	add_action( 'woocommerce_new_order', array( $this, 'inject_purchase_event_pixel' ), 10 );
	add_action( 'woocommerce_process_shop_order_meta', array( $this, 'inject_purchase_event_pixel' ), 20 );
	add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'inject_purchase_event_pixel' ), 30 );
	add_action( 'woocommerce_thankyou', array( $this, 'inject_purchase_event_pixel' ), 40 );
	add_action( 'wp_footer', array( $this, 'mark_order_purchase_tracked' ), 999 );
	/** BESMANOARE HOTFIX END **/

	/** BESMANOARE HOTFIX START **/
	public function inject_purchase_event_pixel( $order_id ) {
		if ( \WC_Facebookcommerce_Utils::is_admin_user() ) {
			return;
		}

		if ( !$this->is_pixel_enabled() || !is_order_received_page() ) {
			return;
		}

		$order = wc_get_order( $order_id );
		if ( !$order ) {
			return;
		}

		$order_state = $order->get_status();
		$valid_states = ['processing', 'completed', 'on-hold', 'pending'];

		if ( !in_array( $order_state, $valid_states, true ) ) {
			return;
		}

		$purchase_tracked_flag = '_wc_' . facebook_for_woocommerce()->get_id() . '_purchase_tracked_' . $order_id;

		// Stop if it’s already tracked
		if ( 'yes' === get_transient( $purchase_tracked_flag ) || $order->get_meta( '_meta_purchase_tracked' ) ) {
			return;
		}

		$event_name = 'Purchase';

		$content_type  = 'product';
		$contents      = [];
		$product_ids   = [[]];
		$product_names = [];

		foreach ( $order->get_items() as $item ) {
			$product = $item->get_product();

			if ( $product ) {
				$product_ids[]   = \WC_Facebookcommerce_Utils::get_fb_content_ids( $product );
				$product_names[] = \WC_Facebookcommerce_Utils::clean_string( $product->get_title() );

				if ( 'product_group' !== $content_type && $product->is_type( 'variable' ) ) {
					$content_type = 'product_group';
				}

				$content = (object)[
					'id'       => \WC_Facebookcommerce_Utils::get_fb_retailer_id( $product ),
					'quantity' => $item->get_quantity(),
				];

				$contents[] = $content;
			}
		}

		$event_data = [
			'event_name'  => $event_name,
			'custom_data' => [
				'content_ids'  => wp_json_encode( array_merge( ...$product_ids ) ),
				'content_name' => wp_json_encode( $product_names ),
				'contents'     => wp_json_encode( $contents ),
				'content_type' => $content_type,
				'value'        => $order->get_total(),
				'currency'     => get_woocommerce_currency(),
				'order_id'     => $order_id,
			],
			'user_data'   => $this->get_user_data_from_billing_address( $order ),
		];

		$event = new Event( $event_data );

		$this->send_api_event( $event );
		$event_data['event_id'] = $event->get_id();

		$this->pixel->inject_event( $event_name, $event_data );
		$this->inject_subscribe_event( $order_id );

		Logger::log(
			'[BESMANOARE HOTFIX] Pixel event injected for order ' . $order_id,	[],
			[
				'should_send_log_to_meta'        => false,
				'should_save_log_in_woocommerce' => true,
				'woocommerce_log_level'          => \WC_Log_Levels::INFO,
			]
		);
	}

	/* PREVENT DOUBLING THE PURCHASE EVENT ON PAGE RELOADS */
	public function mark_order_purchase_tracked( $order_id ) {
		if ( !is_order_received_page() ) {
			return;
		}

		$order_id = absint( get_query_var( 'order-received' ) );
		if ( !$order_id ) {
			return;
		}

		$order = wc_get_order( $order_id );
		if ( !$order || $order->get_meta( '_meta_purchase_tracked' ) ) {
			return;
		}

		$purchase_tracked_flag = '_wc_' . facebook_for_woocommerce()->get_id() . '_purchase_tracked_' . $order_id;

		set_transient( $purchase_tracked_flag, 'yes', 45 * MINUTE_IN_SECONDS );
		$order->update_meta_data( '_meta_purchase_tracked', true );
		$order->save();

		Logger::log(
			"[BESMANOARE HOTFIX] Order #{$order_id} marked as tracked (via wp_footer).",	[],
			[
				'should_send_log_to_meta'        => false,
				'should_save_log_in_woocommerce' => true,
				'woocommerce_log_level'          => \WC_Log_Levels::INFO,
			]
		);
	}
	/** BESMANOARE HOTFIX END **/

Good luck!

For me it not working.

@BESMANOARE
Copy link

For me it not working.

@bilzone you said you are using version 3.4.6 of the plugin. It might be different there. I recommend updating to 3.5.11 - it works there as well. You can just replace the file "facebook-commerce-events-tracker.php" with the one attached.

facebook-commerce-events-tracker.zip

@bilzone
Copy link

bilzone commented Oct 20, 2025

For me it not working.

@bilzone you said you are using version 3.4.6 of the plugin. It might be different there. I recommend updating to 3.5.11 - it works there as well. You can just replace the file "facebook-commerce-events-tracker.php" with the one attached.

facebook-commerce-events-tracker.zip

I have a subdomain where I do all the tests. I have version 3.5.12. I did what you indicated above but the "purchase" event is not triggered.

@BESMANOARE
Copy link

I have a subdomain where I do all the tests. I have version 3.5.12. I did what you indicated above but the "purchase" event is not triggered.

On thank you page, you don't see Purchase Event in the DOM?

@bilzone
Copy link

bilzone commented Oct 24, 2025

I have a subdomain where I do all the tests. I have version 3.5.12. I did what you indicated above but the "purchase" event is not triggered.

On thank you page, you don't see Purchase Event in the DOM?

Yes. Only Pageview event is triggered, without Purchase.
Screenshot 2025-10-24 at 21 18 32

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.

3 participants