Enhance WooCommerce version checking for remote logging reliability (#51009)

* Enhance WooCommerce version checking using get_plugin_updates()

* Update remote logger tool to toggle remote logging feature properly

* Add changelog
This commit is contained in:
Chi-Hsuan Huang 2024-08-30 20:25:52 +08:00 committed by GitHub
parent 44b5f54d08
commit 7971df1d28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 141 additions and 71 deletions

View File

@ -74,6 +74,7 @@ function toggle_remote_logging( $request ) {
update_option( 'woocommerce_feature_remote_logging_enabled', 'yes' ); update_option( 'woocommerce_feature_remote_logging_enabled', 'yes' );
update_option( 'woocommerce_allow_tracking', 'yes' ); update_option( 'woocommerce_allow_tracking', 'yes' );
update_option( 'woocommerce_remote_variant_assignment', 1 ); update_option( 'woocommerce_remote_variant_assignment', 1 );
set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, WC()->version );
} else { } else {
update_option( 'woocommerce_feature_remote_logging_enabled', 'no' ); update_option( 'woocommerce_feature_remote_logging_enabled', 'no' );
} }

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Update remote logger tool to toggle remote logging feature properly

View File

@ -0,0 +1,4 @@
Significance: patch
Type: enhancement
Enhance WooCommerce version checking for remote logging reliability

View File

@ -20,11 +20,10 @@ use WC_Log_Levels;
* @package WooCommerce\Classes * @package WooCommerce\Classes
*/ */
class RemoteLogger extends \WC_Log_Handler { class RemoteLogger extends \WC_Log_Handler {
const LOG_ENDPOINT = 'https://public-api.wordpress.com/rest/v1.1/logstash'; const LOG_ENDPOINT = 'https://public-api.wordpress.com/rest/v1.1/logstash';
const RATE_LIMIT_ID = 'woocommerce_remote_logging'; const RATE_LIMIT_ID = 'woocommerce_remote_logging';
const RATE_LIMIT_DELAY = 60; // 1 minute. const RATE_LIMIT_DELAY = 60; // 1 minute.
const WC_LATEST_VERSION_TRANSIENT = 'latest_woocommerce_version'; const WC_NEW_VERSION_TRANSIENT = 'woocommerce_new_version';
const FETCH_LATEST_VERSION_RETRY = 'fetch_latest_woocommerce_version_retry';
/** /**
* Handle a log entry. * Handle a log entry.
@ -150,7 +149,7 @@ class RemoteLogger extends \WC_Log_Handler {
return false; return false;
} }
if ( ! $this->is_latest_woocommerce_version() ) { if ( ! $this->should_current_version_be_logged() ) {
return false; return false;
} }
@ -221,7 +220,7 @@ class RemoteLogger extends \WC_Log_Handler {
self::LOG_ENDPOINT, self::LOG_ENDPOINT,
array( array(
'body' => wp_json_encode( $body ), 'body' => wp_json_encode( $body ),
'timeout' => 2, 'timeout' => 3,
'headers' => array( 'headers' => array(
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
), ),
@ -256,14 +255,22 @@ class RemoteLogger extends \WC_Log_Handler {
* *
* @return bool * @return bool
*/ */
private function is_latest_woocommerce_version() { private function should_current_version_be_logged() {
$latest_wc_version = $this->fetch_latest_woocommerce_version(); $new_version = get_site_transient( self::WC_NEW_VERSION_TRANSIENT );
if ( is_null( $latest_wc_version ) ) { if ( false === $new_version ) {
return false; $new_version = $this->fetch_new_woocommerce_version();
// Cache the new version for a week since we want to keep logging in with the same version for a while even if the new version is available.
set_site_transient( self::WC_NEW_VERSION_TRANSIENT, $new_version, WEEK_IN_SECONDS );
} }
return version_compare( WC()->version, $latest_wc_version, '>=' ); if ( ! is_string( $new_version ) || '' === $new_version ) {
// If the new version is not available, we consider the current version to be the latest.
return true;
}
// If the current version is the latest, we don't want to log errors.
return version_compare( WC()->version, $new_version, '>=' );
} }
/** /**
@ -316,45 +323,34 @@ class RemoteLogger extends \WC_Log_Handler {
} }
/** /**
* Fetch the latest WooCommerce version using the WordPress API and cache it. * Fetch the new version of WooCommerce from the WordPress API.
* *
* @return string|null * @return string|null New version if an update is available, null otherwise.
*/ */
private function fetch_latest_woocommerce_version() { private function fetch_new_woocommerce_version() {
$cached_version = get_transient( self::WC_LATEST_VERSION_TRANSIENT ); if ( ! function_exists( 'get_plugins' ) ) {
if ( $cached_version ) { require_once ABSPATH . 'wp-admin/includes/plugin.php';
return $cached_version; }
if ( ! function_exists( 'get_plugin_updates' ) ) {
require_once ABSPATH . 'wp-admin/includes/update.php';
} }
$retry_count = get_transient( self::FETCH_LATEST_VERSION_RETRY ); $plugin_updates = get_plugin_updates();
if ( false === $retry_count || ! is_numeric( $retry_count ) ) {
$retry_count = 0;
}
if ( $retry_count >= 3 ) { // Check if WooCommerce plugin update information is available.
if ( ! is_array( $plugin_updates ) || ! isset( $plugin_updates[ WC_PLUGIN_BASENAME ] ) ) {
return null; return null;
} }
if ( ! function_exists( 'plugins_api' ) ) { $wc_plugin_update = $plugin_updates[ WC_PLUGIN_BASENAME ];
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
}
// Fetch the latest version from the WordPress API.
$plugin_info = plugins_api( 'plugin_information', array( 'slug' => 'woocommerce' ) );
if ( is_wp_error( $plugin_info ) ) { // Ensure the update object exists and has the required information.
++$retry_count; if ( ! $wc_plugin_update || ! isset( $wc_plugin_update->update->new_version ) ) {
set_transient( self::FETCH_LATEST_VERSION_RETRY, $retry_count, HOUR_IN_SECONDS );
return null; return null;
} }
if ( ! empty( $plugin_info->version ) ) { $new_version = $wc_plugin_update->update->new_version;
$latest_version = $plugin_info->version; return is_string( $new_version ) ? $new_version : null;
set_transient( self::WC_LATEST_VERSION_TRANSIENT, $latest_version, WEEK_IN_SECONDS );
delete_transient( self::FETCH_LATEST_VERSION_RETRY );
return $latest_version;
}
return null;
} }
/** /**

View File

@ -35,8 +35,7 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case {
public function tearDown(): void { public function tearDown(): void {
$this->cleanup_filters(); $this->cleanup_filters();
delete_option( 'woocommerce_feature_remote_logging_enabled' ); delete_option( 'woocommerce_feature_remote_logging_enabled' );
delete_transient( RemoteLogger::WC_LATEST_VERSION_TRANSIENT ); delete_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT );
delete_transient( RemoteLogger::FETCH_LATEST_VERSION_RETRY );
global $wpdb; global $wpdb;
$wpdb->query( "DELETE FROM {$wpdb->prefix}wc_rate_limits" ); $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_rate_limits" );
WC_Cache_Helper::invalidate_cache_group( WC_Rate_Limiter::CACHE_GROUP ); WC_Cache_Helper::invalidate_cache_group( WC_Rate_Limiter::CACHE_GROUP );
@ -56,6 +55,7 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case {
'plugins_api', 'plugins_api',
'pre_http_request', 'pre_http_request',
'woocommerce_remote_logger_formatted_log_data', 'woocommerce_remote_logger_formatted_log_data',
'pre_site_transient_update_plugins',
); );
foreach ( $filters as $filter ) { foreach ( $filters as $filter ) {
remove_all_filters( $filter ); remove_all_filters( $filter );
@ -90,18 +90,23 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case {
*/ */
public function remote_logging_disallowed_provider() { public function remote_logging_disallowed_provider() {
return array( return array(
'feature flag disabled' => array( 'feature flag disabled' => array(
'condition' => 'feature flag disabled', 'condition' => 'feature flag disabled',
'setup' => fn() => update_option( 'woocommerce_feature_remote_logging_enabled', 'no' ), 'setup' => fn() => update_option( 'woocommerce_feature_remote_logging_enabled', 'no' ),
), ),
'tracking opted out' => array( 'tracking opted out' => array(
'condition' => 'tracking opted out', 'condition' => 'tracking opted out',
'setup' => fn() => add_filter( 'option_woocommerce_allow_tracking', fn() => 'no' ), 'setup' => fn() => add_filter( 'option_woocommerce_allow_tracking', fn() => 'no' ),
), ),
'outdated version' => array( 'high variant assignment' => array(
'condition' => 'outdated version', 'condition' => 'high variant assignment',
'setup' => function () { 'setup' => fn() => add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 15 ),
),
'outdated version' => array(
'condition' => 'outdated version',
'setup' => function () {
$version = WC()->version; $version = WC()->version;
// Next major version. (e.g. 9.0.1 -> 10.0.0).
$next_version = implode( $next_version = implode(
'.', '.',
array_map( array_map(
@ -112,28 +117,79 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case {
array_keys( explode( '.', $version ) ) array_keys( explode( '.', $version ) )
) )
); );
set_transient( RemoteLogger::WC_LATEST_VERSION_TRANSIENT, $next_version );
set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, $next_version, WEEK_IN_SECONDS );
}, },
'high variant assignment' => array(
'condition' => 'high variant assignment',
'setup' => fn() => add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 15 ),
),
), ),
); );
} }
/**
* @testdox Fetch latest WooCommerce version retries on API failure
*/
public function test_fetch_latest_woocommerce_version_retry() {
$this->setup_remote_logging_conditions( true );
add_filter( 'plugins_api', fn() => new \WP_Error(), 10, 3 );
for ( $i = 1; $i <= 4; $i++ ) { /**
$this->sut->is_remote_logging_allowed(); * @testdox should_current_version_be_logged method behaves correctly
$retry_count = get_transient( RemoteLogger::FETCH_LATEST_VERSION_RETRY ); * @dataProvider should_current_version_be_logged_provider
$this->assertEquals( min( $i, 3 ), $retry_count ); *
* @param string $current_version The current WooCommerce version.
* @param string $new_version The new WooCommerce version.
* @param string $transient_value The value of the transient.
* @param bool $expected The expected result.
*/
public function test_should_current_version_be_logged( $current_version, $new_version, $transient_value, $expected ) {
$wc_version = WC()->version;
WC()->version = $current_version;
// Set up the transient.
if ( null !== $transient_value ) {
set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, $transient_value, WEEK_IN_SECONDS );
} else {
delete_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT );
$this->setup_mock_plugin_updates( $new_version );
} }
$result = $this->invoke_private_method( $this->sut, 'should_current_version_be_logged', array() );
$this->assertEquals( $expected, $result );
// Clean up.
delete_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT );
WC()->version = $wc_version;
}
/**
* Data provider for test_should_current_version_be_logged.
*/
public function should_current_version_be_logged_provider() {
return array(
'current version is latest (transient set)' => array( '9.2.0', '9.2.0', '9.2.0', true ),
'current version is newer (transient set)' => array( '9.3.0', '9.2.0', '9.2.0', true ),
'current version is older (transient set)' => array( '9.1.0', '9.2.0', '9.2.0', false ),
'new version is null (transient set)' => array( '9.2.0', null, null, true ),
'transient not set, current version is latest' => array( '9.2.0', '9.2.0', null, true ),
'transient not set, current version is newer' => array( '9.3.0', '9.2.0', null, true ),
'transient not set, current version is older' => array( '9.1.0', '9.2.0', null, false ),
'transient not set, new version is null' => array( '9.2.0', null, null, true ),
);
}
/**
* @testdox fetch_new_woocommerce_version method returns correct version
*/
public function test_fetch_new_woocommerce_version() {
$this->setup_mock_plugin_updates( '9.3.0' );
$result = $this->invoke_private_method( $this->sut, 'fetch_new_woocommerce_version', array() );
$this->assertEquals( '9.3.0', $result, 'The result should be the latest version when an update is available.' );
}
/**
* @testdox fetch_new_woocommerce_version method returns null when no update is available
*/
public function test_fetch_new_woocommerce_version_no_update() {
add_filter( 'pre_site_transient_update_plugins', fn() => array() );
$result = $this->invoke_private_method( $this->sut, 'fetch_new_woocommerce_version', array() );
$this->assertNull( $result, 'The result should be null when no update is available.' );
} }
/** /**
@ -421,17 +477,26 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case {
update_option( 'woocommerce_feature_remote_logging_enabled', $enabled ? 'yes' : 'no' ); update_option( 'woocommerce_feature_remote_logging_enabled', $enabled ? 'yes' : 'no' );
add_filter( 'option_woocommerce_allow_tracking', fn() => 'yes' ); add_filter( 'option_woocommerce_allow_tracking', fn() => 'yes' );
add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 5 ); add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 5 );
add_filter( $this->setup_mock_plugin_updates( $enabled ? WC()->version : '9.0.0' );
'plugins_api', }
function ( $result, $action, $args ) use ( $enabled ) {
if ( 'plugin_information' === $action && 'woocommerce' === $args->slug ) {
return (object) array( 'version' => $enabled ? WC()->version : '9.0.0' ); /**
} * Set up mock plugin updates.
return $result; *
}, * @param string $new_version The new version of WooCommerce to simulate.
10, */
3 private function setup_mock_plugin_updates( $new_version ) {
$update_plugins = (object) array(
'response' => array(
WC_PLUGIN_BASENAME => (object) array(
'new_version' => $new_version,
'package' => 'https://downloads.wordpress.org/plugin/woocommerce.zip',
'slug' => 'woocommerce',
),
),
); );
add_filter( 'pre_site_transient_update_plugins', fn() => $update_plugins );
} }
/** /**