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();
+ }
}