Implement server-side remote error logging (#49599)
* Implement php remote logging * Log fatal errors remotly * Add tests * Add changelog * Improve code doc and type check * Bug fixs Fix Undefined variable $local_logger * Rebase * Fix tests Fix tests * Fix import path * Use WC_Site_Tracking::is_tracking_enabled() * Simplfy path checking/replace logic * Rename log method -> handle * Handle wp_safe_remote_post fails * Fix lint * Update plugins/woocommerce/includes/class-woocommerce.php Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com> * Update plugins/woocommerce/src/Internal/Logging/RemoteLogger.php Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com> * Use WC_Rate_Limiter * Update wc_doing_it_wrong log method name * Update is_third_party_error method signature in RemoteLogger.php * Revert changes * Update plugins/woocommerce/src/Internal/Logging/RemoteLogger.php Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com> * Update plugins/woocommerce/src/Internal/Logging/RemoteLogger.php Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com> * Update plugins/woocommerce/src/Internal/Logging/RemoteLogger.php Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com> * Update plugins/woocommerce/src/Internal/Logging/RemoteLogger.php Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com> * chore: Add 'php' tag to RemoteLogger in woocommerce * Fix test --------- Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com>
This commit is contained in:
parent
76e1761cf7
commit
e29d14b12e
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Implement server-side remote error logging
|
|
@ -29,6 +29,7 @@ use Automattic\WooCommerce\Internal\Admin\Marketplace;
|
|||
use Automattic\WooCommerce\Internal\McStats;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
use Automattic\WooCommerce\Utilities\{LoggingUtil, RestApiUtil, TimeUtil};
|
||||
use Automattic\WooCommerce\Internal\Logging\RemoteLogger;
|
||||
|
||||
/**
|
||||
* Main WooCommerce Class.
|
||||
|
@ -409,6 +410,9 @@ final class WooCommerce {
|
|||
$mc_stats->add( 'error', 'fatal-errors-during-shutdown' );
|
||||
$mc_stats->do_server_side_stats();
|
||||
|
||||
$remote_logger = $container->get( RemoteLogger::class );
|
||||
$remote_logger->handle( time(), WC_Log_Levels::CRITICAL, $message, $context );
|
||||
|
||||
/**
|
||||
* Action triggered when there are errors during shutdown.
|
||||
*
|
||||
|
|
|
@ -4,6 +4,8 @@ declare( strict_types = 1 );
|
|||
namespace Automattic\WooCommerce\Internal\Logging;
|
||||
|
||||
use Automattic\WooCommerce\Utilities\FeaturesUtil;
|
||||
use Automattic\WooCommerce\Utilities\StringUtil;
|
||||
use WC_Rate_Limiter;
|
||||
|
||||
/**
|
||||
* WooCommerce Remote Logger
|
||||
|
@ -16,10 +18,182 @@ use Automattic\WooCommerce\Utilities\FeaturesUtil;
|
|||
* @since 9.2.0
|
||||
* @package WooCommerce\Classes
|
||||
*/
|
||||
class RemoteLogger {
|
||||
class RemoteLogger extends \WC_Log_Handler {
|
||||
const LOG_ENDPOINT = 'https://public-api.wordpress.com/rest/v1.1/logstash';
|
||||
const RATE_LIMIT_ID = 'woocommerce_remote_logging';
|
||||
const RATE_LIMIT_DELAY = 60; // 1 minute.
|
||||
const WC_LATEST_VERSION_TRANSIENT = 'latest_woocommerce_version';
|
||||
const FETCH_LATEST_VERSION_RETRY = 'fetch_latest_woocommerce_version_retry';
|
||||
|
||||
/**
|
||||
* The logger instance.
|
||||
*
|
||||
* @var \WC_Logger_Interface|null
|
||||
*/
|
||||
private $local_logger;
|
||||
|
||||
/**
|
||||
* Remote logger constructor.
|
||||
*
|
||||
* @internal
|
||||
* @param \WC_Logger_Interface|null $logger Logger instance.
|
||||
*/
|
||||
public function __construct( \WC_Logger_Interface $logger = null ) {
|
||||
$this->local_logger = null === $logger
|
||||
? wc_get_logger()
|
||||
: $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a log entry.
|
||||
*
|
||||
* @param int $timestamp Log timestamp.
|
||||
* @param string $level emergency|alert|critical|error|warning|notice|info|debug.
|
||||
* @param string $message Log message.
|
||||
* @param array $context Additional information for log handlers.
|
||||
*
|
||||
* @throws \Exception If the remote logging fails. The error is caught and logged locally.
|
||||
*
|
||||
* @return bool False if value was not handled and true if value was handled.
|
||||
*/
|
||||
public function handle( $timestamp, $level, $message, $context ) {
|
||||
if ( ! \WC_Log_Levels::is_valid_level( $level ) ) {
|
||||
/* translators: 1: WC_Remote_Logger::log 2: level */
|
||||
wc_doing_it_wrong( __METHOD__, sprintf( __( '%1$s was called with an invalid level "%2$s".', 'woocommerce' ), '<code>WC_Remote_Logger::handle</code>', $level ), '9.2.0' );
|
||||
}
|
||||
|
||||
if ( ! $this->is_remote_logging_allowed() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->is_third_party_error( (string) $message, (array) $context ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( WC_Rate_Limiter::retried_too_soon( self::RATE_LIMIT_ID ) ) {
|
||||
$this->local_logger->info( 'Remote logging throttled.', array( 'source' => 'wc-remote-logger' ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$log_data = $this->get_formatted_log( $level, $message, $context );
|
||||
|
||||
// Ensure the log data is valid.
|
||||
if ( ! is_array( $log_data ) || empty( $log_data['message'] ) || empty( $log_data['feature'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = array(
|
||||
'params' => wp_json_encode( $log_data ),
|
||||
);
|
||||
|
||||
WC_Rate_Limiter::set_rate_limit( self::RATE_LIMIT_ID, self::RATE_LIMIT_DELAY );
|
||||
|
||||
$response = wp_safe_remote_post(
|
||||
self::LOG_ENDPOINT,
|
||||
array(
|
||||
'body' => wp_json_encode( $body ),
|
||||
'timeout' => 2,
|
||||
'headers' => array(
|
||||
'Content-Type' => 'application/json',
|
||||
),
|
||||
'blocking' => false,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
throw new \Exception( $response->get_error_message() );
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch ( \Exception $e ) {
|
||||
// Log the error locally if the remote logging fails.
|
||||
$this->local_logger->error( 'Remote logging failed: ' . $e->getMessage() );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get formatted log data to be sent to the remote logging service.
|
||||
*
|
||||
* This method formats the log data by sanitizing the message, adding default fields, and including additional context
|
||||
* such as backtrace, tags, and extra attributes. It also integrates with WC_Tracks to include blog and store details.
|
||||
* The formatted log data is then filtered before being sent to the remote logging service.
|
||||
*
|
||||
* @param string $level Log level (e.g., 'error', 'warning', 'info').
|
||||
* @param string $message Log message to be recorded.
|
||||
* @param array $context Optional. Additional information for log handlers, such as 'backtrace', 'tags', 'extra', and 'error'.
|
||||
*
|
||||
* @return array Formatted log data ready to be sent to the remote logging service.
|
||||
*/
|
||||
public function get_formatted_log( $level, $message, $context = array() ) {
|
||||
$log_data = array(
|
||||
// Default fields.
|
||||
'feature' => 'woocommerce_core',
|
||||
'severity' => $level,
|
||||
'message' => $this->sanitize( $message ),
|
||||
'host' => wp_parse_url( home_url(), PHP_URL_HOST ),
|
||||
'tags' => array( 'woocommerce', 'php' ),
|
||||
'properties' => array(
|
||||
'wc_version' => WC()->version,
|
||||
'php_version' => phpversion(),
|
||||
'wp_version' => get_bloginfo( 'version' ),
|
||||
),
|
||||
);
|
||||
|
||||
if ( isset( $context['backtrace'] ) ) {
|
||||
if ( is_array( $context['backtrace'] ) || is_string( $context['backtrace'] ) ) {
|
||||
$log_data['trace'] = $this->sanitize_trace( $context['backtrace'] );
|
||||
} elseif ( true === $context['backtrace'] ) {
|
||||
$log_data['trace'] = $this->sanitize_trace( self::get_backtrace() );
|
||||
}
|
||||
unset( $context['backtrace'] );
|
||||
}
|
||||
|
||||
if ( isset( $context['tags'] ) && is_array( $context['tags'] ) ) {
|
||||
$log_data['tags'] = array_merge( $log_data['tags'], $context['tags'] );
|
||||
unset( $context['tags'] );
|
||||
}
|
||||
|
||||
if ( class_exists( '\WC_Tracks' ) ) {
|
||||
$user = wp_get_current_user();
|
||||
$blog_details = \WC_Tracks::get_blog_details( $user->ID );
|
||||
|
||||
if ( is_numeric( $blog_details['blog_id'] ) && $blog_details['blog_id'] > 0 ) {
|
||||
$log_data['blog_id'] = $blog_details['blog_id'];
|
||||
}
|
||||
|
||||
if ( ! empty( $blog_details['store_id'] ) ) {
|
||||
$log_data['properties']['store_id'] = $blog_details['store_id'];
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $context['error'] ) && is_array( $context['error'] ) && ! empty( $context['error']['file'] ) ) {
|
||||
$context['error']['file'] = $this->sanitize( $context['error']['file'] );
|
||||
}
|
||||
|
||||
$extra_attrs = $context['extra'] ?? array();
|
||||
unset( $context['extra'] );
|
||||
// Merge the extra attributes with the remaining context since we can't send arbitrary fields to Logstash.
|
||||
$log_data['extra'] = array_merge( $extra_attrs, $context );
|
||||
|
||||
/**
|
||||
* Filters the formatted log data before sending it to the remote logging service.
|
||||
* Returning a non-array value will prevent the log from being sent.
|
||||
*
|
||||
* @since 9.2.0
|
||||
*
|
||||
* @param array $log_data The formatted log data.
|
||||
* @param string $level The log level (e.g., 'error', 'warning').
|
||||
* @param string $message The log message.
|
||||
* @param array $context The original context array.
|
||||
*
|
||||
* @return array The filtered log data.
|
||||
*/
|
||||
return apply_filters( 'woocommerce_remote_logger_formatted_log_data', $log_data, $level, $message, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if remote logging is allowed based on the following conditions:
|
||||
*
|
||||
|
@ -35,7 +209,7 @@ class RemoteLogger {
|
|||
return false;
|
||||
}
|
||||
|
||||
if ( ! $this->is_tracking_opted_in() ) {
|
||||
if ( ! \WC_Site_Tracking::is_tracking_enabled() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -50,15 +224,6 @@ class RemoteLogger {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has opted into tracking/logging.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_tracking_opted_in() {
|
||||
return 'yes' === get_option( 'woocommerce_allow_tracking', 'no' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the store is allowed to log based on the variant assignment percentage.
|
||||
*
|
||||
|
@ -84,6 +249,55 @@ class RemoteLogger {
|
|||
return version_compare( WC()->version, $latest_wc_version, '>=' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the error exclusively contains third-party stack frames for fatal-errors source context.
|
||||
*
|
||||
* @param string $message The error message.
|
||||
* @param array $context The error context.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_third_party_error( string $message, array $context ): bool {
|
||||
// Only check for fatal-errors source context.
|
||||
if ( ! isset( $context['source'] ) || 'fatal-errors' !== $context['source'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If backtrace is not available, we can't determine if the error is third-party. Log it for further investigation.
|
||||
if ( ! isset( $context['backtrace'] ) || ! is_array( $context['backtrace'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$wc_plugin_dir = StringUtil::normalize_local_path_slashes( WC_ABSPATH );
|
||||
|
||||
// Check if the error message contains the WooCommerce plugin directory.
|
||||
if ( str_contains( $message, $wc_plugin_dir ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the backtrace contains the WooCommerce plugin directory.
|
||||
foreach ( $context['backtrace'] as $trace ) {
|
||||
if ( is_string( $trace ) && str_contains( $trace, $wc_plugin_dir ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( is_array( $trace ) && isset( $trace['file'] ) && str_contains( $trace['file'], $wc_plugin_dir ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to allow other plugins to overwrite the result of the third-party error check for remote logging.
|
||||
*
|
||||
* @since 9.2.0
|
||||
*
|
||||
* @param bool $is_third_party_error The result of the third-party error check.
|
||||
* @param string $message The error message.
|
||||
* @param array $context The error context.
|
||||
*/
|
||||
return apply_filters( 'woocommerce_remote_logging_is_third_party_error', true, $message, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the latest WooCommerce version using the WordPress API and cache it.
|
||||
*
|
||||
|
@ -125,4 +339,72 @@ class RemoteLogger {
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize the content to exclude sensitive data.
|
||||
*
|
||||
* The trace is sanitized by:
|
||||
*
|
||||
* 1. Remove the absolute path to the WooCommerce plugin directory.
|
||||
* 2. Remove the absolute path to the WordPress root directory.
|
||||
*
|
||||
* For example, the trace:
|
||||
*
|
||||
* /var/www/html/wp-content/plugins/woocommerce/includes/class-wc-remote-logger.php on line 123
|
||||
* will be sanitized to: **\/woocommerce/includes/class-wc-remote-logger.php on line 123
|
||||
*
|
||||
* @param string $message The message to sanitize.
|
||||
* @return string The sanitized message.
|
||||
*/
|
||||
private function sanitize( $message ) {
|
||||
if ( ! is_string( $message ) ) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
$wc_path = StringUtil::normalize_local_path_slashes( WC_ABSPATH );
|
||||
$wp_path = StringUtil::normalize_local_path_slashes( ABSPATH );
|
||||
|
||||
$sanitized = str_replace(
|
||||
array( $wc_path, $wp_path ),
|
||||
array( '**/' . dirname( WC_PLUGIN_BASENAME ) . '/', '**/' ),
|
||||
$message
|
||||
);
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize the error trace to exclude sensitive data.
|
||||
*
|
||||
* @param array|string $trace The error trace.
|
||||
* @return string The sanitized trace.
|
||||
*/
|
||||
private function sanitize_trace( $trace ): string {
|
||||
if ( is_string( $trace ) ) {
|
||||
return $this->sanitize( $trace );
|
||||
}
|
||||
|
||||
if ( ! is_array( $trace ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$sanitized_trace = array_map(
|
||||
function ( $trace_item ) {
|
||||
if ( is_array( $trace_item ) && isset( $trace_item['file'] ) ) {
|
||||
$trace_item['file'] = $this->sanitize( $trace_item['file'] );
|
||||
return $trace_item;
|
||||
}
|
||||
|
||||
return $this->sanitize( $trace_item );
|
||||
},
|
||||
$trace
|
||||
);
|
||||
|
||||
$is_array_by_file = isset( $sanitized_trace[0]['file'] );
|
||||
if ( $is_array_by_file ) {
|
||||
return wc_print_r( $sanitized_trace, true );
|
||||
}
|
||||
|
||||
return implode( "\n", $sanitized_trace );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ declare( strict_types = 1 );
|
|||
namespace Automattic\WooCommerce\Tests\Internal\Logging;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Logging\RemoteLogger;
|
||||
use WC_Rate_Limiter;
|
||||
use WC_Cache_Helper;
|
||||
|
||||
/**
|
||||
* Class RemoteLoggerTest.
|
||||
|
@ -41,6 +43,13 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case {
|
|||
|
||||
delete_option( 'woocommerce_feature_remote_logging_enabled' );
|
||||
delete_transient( RemoteLogger::WC_LATEST_VERSION_TRANSIENT );
|
||||
|
||||
global $wpdb;
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$wpdb->prefix}wc_rate_limits"
|
||||
);
|
||||
|
||||
WC_Cache_Helper::invalidate_cache_group( WC_Rate_Limiter::CACHE_GROUP );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -54,6 +63,8 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case {
|
|||
remove_all_filters( 'option_woocommerce_version' );
|
||||
remove_all_filters( 'option_woocommerce_remote_variant_assignment' );
|
||||
remove_all_filters( 'plugins_api' );
|
||||
remove_all_filters( 'pre_http_request' );
|
||||
remove_all_filters( 'woocommerce_remote_logger_formatted_log_data' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -233,4 +244,310 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case {
|
|||
$this->sut->is_remote_logging_allowed();
|
||||
$this->assertEquals( 3, get_transient( RemoteLogger::FETCH_LATEST_VERSION_RETRY ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox Test get_formatted_log method with basic log data returns expected array.
|
||||
*/
|
||||
public function test_get_formatted_log_basic() {
|
||||
$level = 'error';
|
||||
$message = 'Fatal error occurred at line 123 in ' . ABSPATH . 'wp-content/file.php';
|
||||
$context = array( 'tags' => array( 'tag1', 'tag2' ) );
|
||||
|
||||
$formatted_log = $this->sut->get_formatted_log( $level, $message, $context );
|
||||
|
||||
$this->assertArrayHasKey( 'feature', $formatted_log );
|
||||
$this->assertArrayHasKey( 'severity', $formatted_log );
|
||||
$this->assertArrayHasKey( 'message', $formatted_log );
|
||||
$this->assertArrayHasKey( 'host', $formatted_log );
|
||||
$this->assertArrayHasKey( 'tags', $formatted_log );
|
||||
$this->assertArrayHasKey( 'properties', $formatted_log );
|
||||
|
||||
$this->assertEquals( 'woocommerce_core', $formatted_log['feature'] );
|
||||
$this->assertEquals( 'error', $formatted_log['severity'] );
|
||||
$this->assertEquals( 'Fatal error occurred at line 123 in **/wp-content/file.php', $formatted_log['message'] );
|
||||
$this->assertEquals( wp_parse_url( home_url(), PHP_URL_HOST ), $formatted_log['host'] );
|
||||
|
||||
// Tags.
|
||||
$this->assertArrayHasKey( 'tags', $formatted_log );
|
||||
$this->assertEquals( array( 'woocommerce', 'php', 'tag1', 'tag2' ), $formatted_log['tags'] );
|
||||
|
||||
// Properties.
|
||||
$this->assertEquals( WC()->version, $formatted_log['properties']['wc_version'] );
|
||||
$this->assertEquals( get_bloginfo( 'version' ), $formatted_log['properties']['wp_version'] );
|
||||
$this->assertEquals( phpversion(), $formatted_log['properties']['php_version'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox Test get_formatted_log method sanitizes backtrace.
|
||||
*/
|
||||
public function test_get_formatted_log_with_backtrace() {
|
||||
$level = 'error';
|
||||
$message = 'Test error message';
|
||||
$context = array( 'backtrace' => ABSPATH . 'wp-content/file.php' );
|
||||
|
||||
$result = $this->sut->get_formatted_log( $level, $message, $context );
|
||||
$this->assertEquals( '**/wp-content/file.php', $result['trace'] );
|
||||
|
||||
$context = array( 'backtrace' => ABSPATH . 'wp-content/plugins/woocommerce/file.php' );
|
||||
$result = $this->sut->get_formatted_log( $level, $message, $context );
|
||||
$this->assertEquals( '**/woocommerce/file.php', $result['trace'] );
|
||||
|
||||
$context = array( 'backtrace' => true );
|
||||
$result = $this->sut->get_formatted_log( $level, $message, $context );
|
||||
|
||||
$this->assertIsString( $result['trace'] );
|
||||
$this->assertStringContainsString( '**/woocommerce/tests/php/src/Internal/Logging/RemoteLoggerTest.php', $result['trace'] );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @testdox Test get_formatted_log method log extra attributes.
|
||||
*/
|
||||
public function test_get_formatted_log_with_extra() {
|
||||
$level = 'error';
|
||||
$message = 'Test error message';
|
||||
$context = array(
|
||||
'extra' => array(
|
||||
'key1' => 'value1',
|
||||
'key2' => 'value2',
|
||||
),
|
||||
);
|
||||
|
||||
$result = $this->sut->get_formatted_log( $level, $message, $context );
|
||||
|
||||
$this->assertArrayHasKey( 'extra', $result );
|
||||
$this->assertEquals( 'value1', $result['extra']['key1'] );
|
||||
$this->assertEquals( 'value2', $result['extra']['key2'] );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @testdox Test handle() method when throttled.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_handle_returns_false_when_throttled() {
|
||||
$mock_local_logger = $this->createMock( \WC_Logger::class );
|
||||
$mock_local_logger->expects( $this->once() )
|
||||
->method( 'info' )
|
||||
->with( 'Remote logging throttled.', array( 'source' => 'wc-remote-logger' ) );
|
||||
|
||||
$this->sut = $this->getMockBuilder( RemoteLogger::class )
|
||||
->setConstructorArgs( array( $mock_local_logger ) )
|
||||
->onlyMethods( array( 'is_remote_logging_allowed' ) )
|
||||
->getMock();
|
||||
|
||||
$this->sut->method( 'is_remote_logging_allowed' )->willReturn( true );
|
||||
|
||||
// Set rate limit to simulate exceeded limit.
|
||||
WC_Rate_Limiter::set_rate_limit( RemoteLogger::RATE_LIMIT_ID, 10 );
|
||||
$result = $this->sut->handle( time(), 'error', 'Test message', array() );
|
||||
$this->assertFalse( $result );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @testdox Test handle() method applies filter.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_handle_filtered_log_null() {
|
||||
$this->sut = $this->getMockBuilder( RemoteLogger::class )
|
||||
->onlyMethods( array( 'is_remote_logging_allowed' ) )
|
||||
->getMock();
|
||||
|
||||
$this->sut->method( 'is_remote_logging_allowed' )->willReturn( true );
|
||||
|
||||
add_filter(
|
||||
'woocommerce_remote_logger_formatted_log_data',
|
||||
function ( $log_data, $level, $message, $context ) {
|
||||
$this->assertEquals( 'Test message', $log_data['message'] );
|
||||
$this->assertEquals( 'error', $level );
|
||||
$this->assertEquals( 'Test message', $message );
|
||||
$this->assertEquals( array(), $context );
|
||||
|
||||
return null;
|
||||
},
|
||||
10,
|
||||
4
|
||||
);
|
||||
|
||||
// Mock wp_safe_remote_post using pre_http_request filter.
|
||||
add_filter(
|
||||
'pre_http_request',
|
||||
function () {
|
||||
// assert not called.
|
||||
$this->assertFalse( true );
|
||||
},
|
||||
10,
|
||||
3
|
||||
);
|
||||
|
||||
$this->assertFalse( $this->sut->handle( time(), 'error', 'Test message', array() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox Test handle() method successfully sends log.
|
||||
*/
|
||||
public function test_handle_successful() {
|
||||
$this->sut = $this->getMockBuilder( RemoteLogger::class )
|
||||
->onlyMethods( array( 'is_remote_logging_allowed' ) )
|
||||
->getMock();
|
||||
|
||||
$this->sut->method( 'is_remote_logging_allowed' )->willReturn( true );
|
||||
|
||||
// Mock wp_safe_remote_post using pre_http_request filter.
|
||||
add_filter(
|
||||
'pre_http_request',
|
||||
function ( $preempt, $args, $url ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
|
||||
$this->assertArrayHasKey( 'body', $args );
|
||||
$this->assertArrayHasKey( 'headers', $args );
|
||||
return array(
|
||||
'response' => array(
|
||||
'code' => 200,
|
||||
'message' => 'OK',
|
||||
),
|
||||
'body' => wp_json_encode( array( 'success' => true ) ),
|
||||
);
|
||||
},
|
||||
10,
|
||||
3
|
||||
);
|
||||
|
||||
$this->assertTrue( $this->sut->handle( time(), 'error', 'Test message', array() ) );
|
||||
// Verify that rate limit was set.
|
||||
$this->assertTrue( WC_Rate_Limiter::retried_too_soon( RemoteLogger::RATE_LIMIT_ID ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox Test handle method when remote logging fails.
|
||||
**/
|
||||
public function test_handle_remote_logging_failure() {
|
||||
$mock_local_logger = $this->createMock( \WC_Logger::class );
|
||||
$mock_local_logger->expects( $this->once() )
|
||||
->method( 'error' );
|
||||
|
||||
$this->sut = $this->getMockBuilder( RemoteLogger::class )
|
||||
->setConstructorArgs( array( $mock_local_logger ) )
|
||||
->onlyMethods( array( 'is_remote_logging_allowed' ) )
|
||||
->getMock();
|
||||
|
||||
$this->sut->method( 'is_remote_logging_allowed' )->willReturn( true );
|
||||
|
||||
// Mock wp_safe_remote_post to throw an exception using pre_http_request filter.
|
||||
add_filter(
|
||||
'pre_http_request',
|
||||
function ( $preempt, $args, $url ) {
|
||||
if ( 'https://public-api.wordpress.com/rest/v1.1/logstash' === $url ) {
|
||||
throw new \Exception( 'Remote logging failed: A valid URL was not provided.' );
|
||||
}
|
||||
return $preempt;
|
||||
},
|
||||
10,
|
||||
3
|
||||
);
|
||||
|
||||
$this->assertFalse( $this->sut->handle( time(), 'error', 'Test message', array() ) );
|
||||
// Verify that rate limit was set.
|
||||
$this->assertTrue( WC_Rate_Limiter::retried_too_soon( RemoteLogger::RATE_LIMIT_ID ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox Test is_third_party_error method.
|
||||
*
|
||||
* @dataProvider data_provider_for_is_third_party_error
|
||||
*
|
||||
* @param string $message Error message.
|
||||
* @param array $context Context.
|
||||
* @param bool $expected_result Expected result.
|
||||
*/
|
||||
public function test_is_third_party_error( $message, $context, $expected_result ) {
|
||||
$result = $this->invoke_private_method( $this->sut, 'is_third_party_error', array( $message, $context ) );
|
||||
|
||||
$this->assertEquals( $expected_result, $result );
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for third party error test.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function data_provider_for_is_third_party_error() {
|
||||
return array(
|
||||
array(
|
||||
'Fatal error occurred at line 123 in' . WC_ABSPATH . 'file.php',
|
||||
array(),
|
||||
false,
|
||||
),
|
||||
array(
|
||||
'Fatal error occurred at line 123 in /home/user/path/wp-content/file.php',
|
||||
array(),
|
||||
false, // source is not.
|
||||
),
|
||||
array(
|
||||
'Fatal error occurred at line 123 in /home/user/path/wp-content/file.php',
|
||||
array( 'source' => 'fatal-errors' ),
|
||||
false, // backtrace is not set.
|
||||
),
|
||||
array(
|
||||
'Fatal error occurred at line 123 in /home/user/path/wp-content/plugins/3rd-plugin/file.php',
|
||||
array(
|
||||
'source' => 'fatal-errors',
|
||||
'backtrace' => array(
|
||||
'/home/user/path/wp-content/plugins/3rd-plugin/file.php',
|
||||
WC_ABSPATH . 'file.php',
|
||||
),
|
||||
),
|
||||
false,
|
||||
),
|
||||
array(
|
||||
'Fatal error occurred at line 123 in /home/user/path/wp-content/plugins/woocommerce-3rd-plugin/file.php',
|
||||
array(
|
||||
'source' => 'fatal-errors',
|
||||
'backtrace' => array(
|
||||
WP_PLUGIN_DIR . 'woocommerce-3rd-plugin/file.php',
|
||||
),
|
||||
),
|
||||
true,
|
||||
),
|
||||
array(
|
||||
'Fatal error occurred at line 123 in /home/user/path/wp-content/plugins/3rd-plugin/file.php',
|
||||
array(
|
||||
'source' => 'fatal-errors',
|
||||
'backtrace' => array(
|
||||
WP_PLUGIN_DIR . '3rd-plugin/file.php',
|
||||
),
|
||||
),
|
||||
true,
|
||||
),
|
||||
array(
|
||||
'Fatal error occurred at line 123 in /home/user/path/wp-content/plugins/3rd-plugin/file.php',
|
||||
array(
|
||||
'source' => 'fatal-errors',
|
||||
'backtrace' => array(
|
||||
array(
|
||||
'file' => WP_PLUGIN_DIR . '3rd-plugin/file.php',
|
||||
),
|
||||
),
|
||||
),
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper method to invoke private methods.
|
||||
*
|
||||
* @param object $obj Object instance.
|
||||
* @param string $method_name Name of the private method.
|
||||
* @param array $parameters Parameters to pass to the method.
|
||||
* @return mixed
|
||||
*/
|
||||
private function invoke_private_method( $obj, $method_name, $parameters = array() ) {
|
||||
$reflection = new \ReflectionClass( get_class( $obj ) );
|
||||
$method = $reflection->getMethod( $method_name );
|
||||
$method->setAccessible( true );
|
||||
return $method->invokeArgs( $obj, $parameters );
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue