Commit 96ccc0a5484 for woocommerce

commit 96ccc0a54840bbb08e1e828753a0d9968eea1efa
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date:   Tue Feb 24 12:25:55 2026 +0100

    [Performance] Customer: guarded updates for `wc_last_active` user meta (#63355)

    BFCM/Christmas shopping spikes friendly meta update: the updates trigger cache re-read and analytics customer record sync. When amplified by the spikes, that impacts database perfromance.

diff --git a/plugins/woocommerce/changelog/performance-63118-reduce-sqls-number-iteration-4 b/plugins/woocommerce/changelog/performance-63118-reduce-sqls-number-iteration-4
new file mode 100644
index 00000000000..7eeee3994b0
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-63118-reduce-sqls-number-iteration-4
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Customers: guarded updates of the last activity timestamp.
diff --git a/plugins/woocommerce/includes/wc-user-functions.php b/plugins/woocommerce/includes/wc-user-functions.php
index 7040f54db43..ee38d176600 100644
--- a/plugins/woocommerce/includes/wc-user-functions.php
+++ b/plugins/woocommerce/includes/wc-user-functions.php
@@ -1081,9 +1081,6 @@ add_action( 'wp_login', 'wc_user_logged_in', 10, 2 );
  * @since 3.4.0
  */
 function wc_current_user_is_active() {
-	if ( ! is_user_logged_in() ) {
-		return;
-	}
 	wc_update_user_last_active( get_current_user_id() );
 }
 add_action( 'wp', 'wc_current_user_is_active', 10 );
@@ -1095,10 +1092,35 @@ add_action( 'wp', 'wc_current_user_is_active', 10 );
  * @param int $user_id User ID to mark active.
  */
 function wc_update_user_last_active( $user_id ) {
-	if ( ! $user_id ) {
-		return;
+	if ( $user_id ) {
+		// Optimization note: meta write invalidates caches and triggers repopulating them, hence guarded updates.
+		// Default threshold: order placement, automation - guard against repetitive updates in short period of time.
+		$threshold = MINUTE_IN_SECONDS;
+		if ( doing_action( 'wp_login' ) ) {
+			// When user logged in, execute update right away.
+			$threshold = 0;
+		} elseif ( doing_action( 'wp' ) ) {
+			// When navigating the store pages, track online presence (5 minutes works for this sort of tracking).
+			$threshold = 5 * MINUTE_IN_SECONDS;
+		}
+
+		/**
+		 * Enables customization of the update frequency for user last active meta by adjusting the threshold from default to custom values.
+		 * Note that a lower threshold results in more frequent database writes by Woo core and, if enabled, Analytics.
+		 *
+		 * @param int $threshold The threshold in seconds to be applied (1 minute, 5 minutes and 0 for immediate update).
+		 * @return int
+		 *
+		 * @since 10.7.0
+		 */
+		$threshold = (int) apply_filters( 'woocommerce_update_user_last_active_threshold', $threshold );
+
+		$now         = time();
+		$last_active = get_user_meta( $user_id, 'wc_last_active', true );
+		if ( ! $last_active || ( $now - $last_active ) > $threshold ) {
+			update_user_meta( $user_id, 'wc_last_active', (string) $now, (string) $last_active );
+		}
 	}
-	update_user_meta( $user_id, 'wc_last_active', (string) time() );
 }

 /**
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php
index a60857162fb..57e9772dd8d 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Customers/DataStore.php
@@ -931,6 +931,9 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
 	 */
 	public static function update_registered_customer_via_last_active( $meta_id, $user_id, $meta_key ) {
 		if ( 'wc_last_active' === $meta_key ) {
+			// Optimization note related to guarded updates in `wc_update_user_last_active`: the meta update will trigger
+			// this method execution. We evaluated adding `! doing_action( 'wp' )` here, but the performance gain lays
+			// in the micro-optimization area while exposing the Analytics to certain edge-cases.
 			self::update_registered_customer( $user_id );
 		}
 	}
diff --git a/plugins/woocommerce/tests/php/includes/wc-user-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-user-functions-test.php
index 839ff088196..8303b5dc615 100644
--- a/plugins/woocommerce/tests/php/includes/wc-user-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-user-functions-test.php
@@ -16,7 +16,7 @@ class WC_User_Functions_Tests extends WC_Unit_Test_Case {
 	public function setUp(): void {
 		parent::setUp();
 		$this->setup_cot();
-		$this->toggle_cot_feature_and_usage( false );
+		$this->toggle_cot_feature_and_usage( true );
 	}

 	/**
@@ -25,13 +25,16 @@ class WC_User_Functions_Tests extends WC_Unit_Test_Case {
 	public function tearDown(): void {
 		parent::tearDown();
 		$this->clean_up_cot_setup();
+
+		// In case `wc_update_user_last_active` test fail, clean the global state.
+		global $wp_current_filter;
+		$wp_current_filter = array(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
 	}

 	/**
 	 * Test wc_get_customer_order_count. Borrowed from `WC_Tests_Customer_Functions` class for COT.
 	 */
 	public function test_hpos_wc_customer_bought_product() {
-		$this->toggle_cot_feature_and_usage( true );
 		$customer_id_1 = wc_create_new_customer( 'test@example.com', 'testuser', 'testpassword' );
 		$customer_id_2 = wc_create_new_customer( 'test2@example.com', 'testuser2', 'testpassword2' );
 		$product_1     = new WC_Product_Simple();
@@ -78,6 +81,8 @@ class WC_User_Functions_Tests extends WC_Unit_Test_Case {
 	 * @since 9.3
 	 */
 	public function test_wc_get_customer_available_downloads_for_partial_refunds(): void {
+		$this->toggle_cot_feature_and_usage( false );
+
 		/** @var Download_Directories $download_directories */
 		$download_directories = wc_get_container()->get( Download_Directories::class );
 		$download_directories->set_mode( Download_Directories::MODE_ENABLED );
@@ -149,4 +154,41 @@ class WC_User_Functions_Tests extends WC_Unit_Test_Case {
 		$this->assertEquals( $order->get_id(), $download['order_id'] );
 		$this->assertEquals( $product2->get_id(), $download['product_id'] );
 	}
+
+	/**
+	 * Test `wc_update_user_last_active`: verify the applied thresholds.
+	 */
+	public function test_wc_update_user_last_active(): void {
+		global $wp_current_filter;
+		$customer    = WC_Helper_Customer::create_customer();
+		$customer_id = $customer->get_id();
+
+		// Verify threshold crossing is handled as intended.
+		$original = time() - 30;
+		update_user_meta( $customer_id, 'wc_last_active', (string) $original );
+		wc_update_user_last_active( $customer_id );
+		$this->assertSame( (string) $original, get_user_meta( $customer_id, 'wc_last_active', true ) );
+
+		// Verify fallback of one-minute update interval.
+		$original = time() - MINUTE_IN_SECONDS - 1;
+		update_user_meta( $customer_id, 'wc_last_active', (string) $original );
+		wc_update_user_last_active( $customer_id );
+		$this->assertGreaterThan( $original, get_user_meta( $customer_id, 'wc_last_active', true ) );
+
+		// Verify immediate update after logging in.
+		$original = time() - 1;
+		update_user_meta( $customer_id, 'wc_last_active', (string) $original );
+		$wp_current_filter = array( 'wp_login' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+		wc_update_user_last_active( $customer_id );
+		$this->assertGreaterThan( $original, get_user_meta( $customer_id, 'wc_last_active', true ) );
+
+		// Verify five minutes update interval for pages navigation use-case.
+		$original = time() - ( 5 * MINUTE_IN_SECONDS ) - 1;
+		update_user_meta( $customer_id, 'wc_last_active', (string) $original );
+		$wp_current_filter = array( 'wp' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+		wc_update_user_last_active( $customer_id );
+		$this->assertGreaterThan( $original, get_user_meta( $customer_id, 'wc_last_active', true ) );
+
+		$customer->delete();
+	}
 }