Commit c2d8f890d29 for woocommerce

commit c2d8f890d29a66379f64983ef31fc4b925d60064
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Mon Oct 26 14:50:15 2026 +0200

    Preserve PHP error types in Store API logging callbacks (#70222)

    * fix: Preserve Store API error types for logging callbacks

    Caught Store API failures reach the existing remote-logging filter without
    error.type, although the fatal-error producer always supplied that field.
    Callbacks that read it can warn or change their classification.

    Supply the PHP error category each throwable would produce if uncaught.
    Match ParseError and CompileError exactly so their subclasses retain the
    native E_ERROR fallback, and ignore arbitrary exception codes/severities.

    Document the context and cover type mapping, the legacy callback override,
    and formatted log metadata without changing responses or filter rules.

    Refs WOOAIRR-523
    Refs #69585

    * chore: Add changelog for Store API error type compatibility

    Record the logging compatibility fix in the WooCommerce release notes.

    Refs WOOAIRR-523

    * test: Mark unused remote logging filter arguments

    Keep the required positional parameters so the callback receives context,
    but explicitly discard the two values it does not read. This follows the
    repository guidance for fixed-signature closure parameters.

    Refs WOOAIRR-523

    * docs: Target 11.3.0 in remote logging filter docblock

    This fix is milestoned for 11.3.0 so that release never ships Store
    API log entries without error.type. The 11.3.0 note in the docblock
    already covers Store API entries, so the separate @since 11.4.0 line
    would be wrong once the fix is cherry-picked to release/11.3.

    The earlier rewrite also dropped the sentence documenting the
    backtrace both sources still log. Restore it, noting that
    fatal-errors entries carry true when PHP supplied no stack trace.

    Refs WOOAIRR-523

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/fix-store-api-error-types b/plugins/woocommerce/changelog/fix-store-api-error-types
new file mode 100644
index 00000000000..bd24fc15acb
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-store-api-error-types
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Preserve error types in Store API logs for compatibility with extensions.
diff --git a/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php b/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php
index 109888b3d11..f849f8a00c4 100644
--- a/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php
+++ b/plugins/woocommerce/src/Internal/Logging/RemoteLogger.php
@@ -367,7 +367,7 @@ class RemoteLogger extends \WC_Log_Handler {
 		/**
 		 * Filter to allow other plugins to overwrite the result of the third-party error check for remote logging.
 		 *
-		 * Runs for `fatal-errors` entries (from the shutdown handler, with `error.type`, `error.file` and `error.line`) and, since 11.3.0, for `store-api` entries (caught Store API engine errors, with `error.file` and `error.line` only). Both carry a `backtrace` list of `#N file(line): call` strings.
+		 * Runs for `fatal-errors` entries from the shutdown handler and, since 11.3.0, for caught `store-api` errors. Both provide `error.type`, `error.file` and `error.line`; caught errors use the PHP error type they would produce if uncaught. Both also carry a `backtrace` list of `#N file(line): call` strings; `fatal-errors` entries carry `true` instead when PHP supplied no stack trace.
 		 *
 		 * @since 9.2.0
 		 *
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/UnexpectedErrorResponse.php b/plugins/woocommerce/src/StoreApi/Utilities/UnexpectedErrorResponse.php
index 6b594e38d4a..133e835325d 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/UnexpectedErrorResponse.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/UnexpectedErrorResponse.php
@@ -37,12 +37,19 @@ final class UnexpectedErrorResponse {
 			$error->getLine()
 		);

+		// PHP reports subclasses of ParseError and CompileError as E_ERROR, so match exact classes.
+		$error_types = array(
+			\ParseError::class   => E_PARSE,
+			\CompileError::class => E_COMPILE_ERROR,
+		);
+
 		try {
 			wc_get_logger()->critical(
 				$log_message,
 				array(
 					'source'         => 'store-api',
 					'error'          => array(
+						'type' => $error_types[ get_class( $error ) ] ?? E_ERROR,
 						'file' => $error->getFile(),
 						'line' => $error->getLine(),
 					),
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/UnexpectedErrorResponseTest.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/UnexpectedErrorResponseTest.php
index 6d7d2b5dfa8..8469371c01b 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/UnexpectedErrorResponseTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/UnexpectedErrorResponseTest.php
@@ -72,6 +72,7 @@ class UnexpectedErrorResponseTest extends WC_Unit_Test_Case {
 			array(
 				'source'         => 'store-api',
 				'error'          => array(
+					'type' => E_ERROR,
 					'file' => $error->getFile(),
 					'line' => $error->getLine(),
 				),
@@ -93,6 +94,67 @@ class UnexpectedErrorResponseTest extends WC_Unit_Test_Case {
 		$this->assertStringStartsWith( '#0 ', $backtrace[0] );
 	}

+	/**
+	 * @testdox Should log the PHP error type the failure would have produced if uncaught.
+	 * @dataProvider uncaught_error_type_provider
+	 *
+	 * @param \Throwable $error         Caught failure.
+	 * @param int        $expected_type Native uncaught error type.
+	 */
+	public function test_logs_uncaught_error_type( \Throwable $error, int $expected_type ): void {
+		UnexpectedErrorResponse::create( $error, self::class );
+
+		$this->assertSame( $expected_type, $this->captured_logs[0]['context']['error']['type'] ?? null );
+	}
+
+	/**
+	 * Error types reported by PHP for uncaught throwables.
+	 *
+	 * @return array
+	 */
+	public function uncaught_error_type_provider(): array {
+		return array(
+			'type error with unrelated code' => array( new \TypeError( 'Fixture failure.', E_PARSE ), E_ERROR ),
+			'parse error'                    => array( new \ParseError( 'Fixture failure.' ), E_PARSE ),
+			'compile error'                  => array( new \CompileError( 'Fixture failure.' ), E_COMPILE_ERROR ),
+			'exception with unrelated code'  => array( new \RuntimeException( 'Fixture failure.', E_PARSE ), E_ERROR ),
+			'converted warning'              => array( new \ErrorException( 'Fixture failure.', 0, E_WARNING ), E_ERROR ),
+			'parse error subclass'           => array( new class() extends \ParseError {}, E_ERROR ),
+			'compile error subclass'         => array( new class() extends \CompileError {}, E_ERROR ),
+		);
+	}
+
+	/**
+	 * @testdox Should preserve a legacy remote logging override that reads the PHP error type.
+	 */
+	public function test_logged_context_preserves_legacy_error_type_override(): void {
+		UnexpectedErrorResponse::create( new \TypeError( 'Fixture extension failure.' ), self::class );
+		$context              = $this->captured_logs[0]['context'];
+		$context['backtrace'] = array( '#0 ' . WP_PLUGIN_DIR . '/fixture-extension/error.php(1): fail()' );
+		$filter_calls         = 0;
+		add_filter(
+			'woocommerce_remote_logging_is_third_party_error',
+			static function ( $is_third_party, $message, $context ) use ( &$filter_calls ) {
+				unset( $is_third_party, $message );
+				++$filter_calls;
+				return E_ERROR !== $context['error']['type'];
+			},
+			10,
+			3
+		);
+
+		$classify = new \ReflectionMethod( RemoteLogger::class, 'is_third_party_error' );
+		$classify->setAccessible( true );
+		$result = $classify->invoke(
+			wc_get_container()->get( RemoteLogger::class ),
+			'Fixture extension failure.',
+			$context
+		);
+
+		$this->assertSame( 1, $filter_calls );
+		$this->assertFalse( $result, 'The existing callback must still allow E_ERROR records.' );
+	}
+
 	/**
 	 * @testdox Should produce a remote log record that keeps the file and leaks nothing from the error object.
 	 */
@@ -113,6 +175,7 @@ class UnexpectedErrorResponseTest extends WC_Unit_Test_Case {
 		$record        = $remote_logger->get_formatted_log( 'critical', $logged['message'], $logged['context'] );

 		$this->assertSame( 'store-api', $record['extra']['source'] );
+		$this->assertSame( E_ERROR, $record['extra']['error']['type'] ?? null );
 		$this->assertStringContainsString( basename( __FILE__ ), $record['file'], 'The remote record must carry the file the error was raised in.' );
 		$this->assertStringStartsWith( '#0 ', $record['trace'] );
 		$this->assertStringNotContainsString( 'fixture-secret-token', wp_json_encode( $record ), 'Error object state must not reach the remote record.' );