Commit 89a773af2da for woocommerce
commit 89a773af2dada71ce37fb3aa3028a1a29efc4a13
Author: Ján Mikláš <neosinner@gmail.com>
Date: Fri Sep 4 16:03:14 2026 +0200
Avoid Inbox Note cleanup during admin AJAX (#68088)
* Move Inbox Note cleanup off admin initialization
* Add changelog entry for Inbox Note cleanup
* Run survey note cleanup before the daily fetch
Hook possibly_delete_survey_notes at priority 9 so the cleanup runs ahead of
Events::do_wc_admin_daily, which adds notes and refreshes the remote data
source pollers at the default priority. Both sat at 10 before, leaving the
order dependent on registration.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQuqFGRsLMyceRFW1KY3qF
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/performance-admin-ajax-inbox-note-cleanup b/plugins/woocommerce/changelog/performance-admin-ajax-inbox-note-cleanup
new file mode 100644
index 00000000000..243042f3275
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-admin-ajax-inbox-note-cleanup
@@ -0,0 +1,4 @@
+Significance: patch
+Type: performance
+
+Avoid running Inbox Note cleanup during admin initialization.
diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index 29f0be80edb..dc7eef11f6d 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -353,6 +353,7 @@ class WC_Install {
'11.2.0' => array(
'wc_update_1120_remove_abandoned_cart_recovery',
'wc_update_1120_migrate_stock_notifications_alpha_constant',
+ 'wc_update_1120_delete_surface_cart_checkout_note',
),
'11.2.0-1' => array(
'wc_update_11201_migrate_tax_lookup_order_items',
diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php
index a77e5d63d48..d7a6ff8862e 100644
--- a/plugins/woocommerce/includes/wc-update-functions.php
+++ b/plugins/woocommerce/includes/wc-update-functions.php
@@ -44,6 +44,7 @@ use Automattic\WooCommerce\Internal\Utilities\FilesystemUtil;
use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
use Automattic\WooCommerce\Internal\VariationGallery\Package as VariationGalleryPackage;
use Automattic\WooCommerce\Utilities\StringUtil;
+use Automattic\WooCommerce\Blocks\InboxNotifications;
use Automattic\WooCommerce\Blocks\Options as BlockOptions;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
@@ -3704,6 +3705,17 @@ function wc_update_1120_migrate_stock_notifications_alpha_constant() {
update_option( StockNotifications::ENABLE_OPTION_NAME, 'yes', true );
}
+/**
+ * Delete the retired Surface Cart and Checkout inbox note.
+ *
+ * @since 11.2.0
+ *
+ * @return void
+ */
+function wc_update_1120_delete_surface_cart_checkout_note(): void {
+ InboxNotifications::delete_surface_cart_checkout_blocks_notification();
+}
+
/**
* Invalidate the Analytics report cache.
*
diff --git a/plugins/woocommerce/src/Admin/Notes/Notes.php b/plugins/woocommerce/src/Admin/Notes/Notes.php
index c790ba62083..59d3cab5f7f 100644
--- a/plugins/woocommerce/src/Admin/Notes/Notes.php
+++ b/plugins/woocommerce/src/Admin/Notes/Notes.php
@@ -24,7 +24,7 @@ class Notes {
*/
public static function init() {
add_action( 'admin_init', array( __CLASS__, 'schedule_unsnooze_notes' ) );
- add_action( 'admin_init', array( __CLASS__, 'possibly_delete_survey_notes' ) );
+ add_action( 'wc_admin_daily', array( __CLASS__, 'possibly_delete_survey_notes' ), 9 );
add_action( 'update_option_woocommerce_show_marketplace_suggestions', array( __CLASS__, 'possibly_delete_marketing_notes' ), 10, 2 );
add_action( self::UNSNOOZE_HOOK, array( __CLASS__, 'unsnooze_notes' ) );
}
diff --git a/plugins/woocommerce/src/Blocks/Domain/Bootstrap.php b/plugins/woocommerce/src/Blocks/Domain/Bootstrap.php
index 2bb55635556..1499c6733b1 100644
--- a/plugins/woocommerce/src/Blocks/Domain/Bootstrap.php
+++ b/plugins/woocommerce/src/Blocks/Domain/Bootstrap.php
@@ -22,7 +22,6 @@ use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsAdmin;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsFrontend;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutLink;
-use Automattic\WooCommerce\Blocks\InboxNotifications;
use Automattic\WooCommerce\Blocks\Installer;
use Automattic\WooCommerce\Blocks\Payments\Api as PaymentsApi;
use Automattic\WooCommerce\Blocks\Payments\Integrations\BankTransfer;
@@ -93,17 +92,6 @@ class Bootstrap {
$this->register_dependencies();
$this->register_payment_methods();
- add_action(
- 'admin_init',
- function () {
- // Delete this notification because the blocks are included in WC Core now. This will handle any sites
- // with lingering notices.
- InboxNotifications::delete_surface_cart_checkout_blocks_notification();
- },
- 10,
- 0
- );
-
// We need to initialize BlockTemplatesController and BlockTemplatesRegistry at the end of `after_setup_theme`
// so themes had the opportunity to declare support for template parts.
add_action(
diff --git a/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
index 2744f289539..227ae24ce0c 100644
--- a/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
@@ -7,6 +7,9 @@
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
+use Automattic\WooCommerce\Admin\Notes\Note;
+use Automattic\WooCommerce\Admin\Notes\Notes;
+use Automattic\WooCommerce\Blocks\InboxNotifications;
use Automattic\WooCommerce\Blocks\Options as BlockOptions;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
use Automattic\WooCommerce\Internal\Admin\OrderTaxLookupMigrator;
@@ -498,6 +501,34 @@ class WC_Update_Functions_Test extends \WC_Unit_Test_Case {
$batch_processor->remove_processor( OrderTaxLookupMigrator::class );
}
+ /**
+ * @testdox Migration registers for WooCommerce 11.2.0 and deletes the retired Surface Cart and Checkout note.
+ */
+ public function test_wc_update_1120_delete_surface_cart_checkout_note(): void {
+ include_once WC_ABSPATH . 'includes/wc-update-functions.php';
+
+ $db_updates = WC_Install::get_db_update_callbacks();
+ $this->assertArrayHasKey( '11.2.0', $db_updates );
+ $this->assertContains( 'wc_update_1120_delete_surface_cart_checkout_note', $db_updates['11.2.0'] );
+
+ $note = new Note();
+ $note->set_name( InboxNotifications::SURFACE_CART_CHECKOUT_NOTE_NAME );
+ $note->set_title( 'Surface Cart and Checkout' );
+ $note->set_content( 'Test content' );
+ $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
+ $note->set_source( 'PHPUNIT_TEST' );
+ $note->add_action( 'learn-more', 'Learn more', 'https://woocommerce.com/' );
+ $note->save();
+ $this->assertNotFalse( Notes::get_note_by_name( InboxNotifications::SURFACE_CART_CHECKOUT_NOTE_NAME ), 'The retired note fixture should exist before the update.' );
+
+ wc_update_1120_delete_surface_cart_checkout_note();
+
+ $this->assertFalse( Notes::get_note_by_name( InboxNotifications::SURFACE_CART_CHECKOUT_NOTE_NAME ), 'The retired note should be deleted during the update.' );
+
+ wc_update_1120_delete_surface_cart_checkout_note();
+ $this->assertFalse( Notes::get_note_by_name( InboxNotifications::SURFACE_CART_CHECKOUT_NOTE_NAME ), 'The update should remain safe when the note is already absent.' );
+ }
+
/**
* @testdox Migration invalidates the Analytics report cache, so a response cached before the update stops being served.
*/
diff --git a/plugins/woocommerce/tests/php/src/Admin/Notes/NotesTest.php b/plugins/woocommerce/tests/php/src/Admin/Notes/NotesTest.php
new file mode 100644
index 00000000000..5d831213315
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Admin/Notes/NotesTest.php
@@ -0,0 +1,63 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Admin\Notes;
+
+use Automattic\WooCommerce\Admin\Notes\Note;
+use Automattic\WooCommerce\Admin\Notes\Notes;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for admin note cleanup.
+ */
+class NotesTest extends WC_Unit_Test_Case {
+
+ /**
+ * @testdox Survey cleanup runs daily instead of during admin initialization.
+ */
+ public function test_survey_cleanup_runs_daily(): void {
+ $callback = array( Notes::class, 'possibly_delete_survey_notes' );
+
+ $this->assertFalse( has_action( 'admin_init', $callback ), 'Survey cleanup should not run during admin initialization.' );
+
+ // Priority 9 so the cleanup runs before Events::do_wc_admin_daily, which adds notes and
+ // refreshes the remote data source pollers at the default priority.
+ $this->assertSame( 9, has_action( 'wc_admin_daily', $callback ), 'Survey cleanup should run from the daily admin event, ahead of the fetch.' );
+ }
+
+ /**
+ * @testdox Survey cleanup deletes only actioned survey notes.
+ */
+ public function test_survey_cleanup_deletes_only_actioned_survey_notes(): void {
+ $actioned_note_id = $this->create_note( 'phpunit-actioned-survey-note', Note::E_WC_ADMIN_NOTE_SURVEY, Note::E_WC_ADMIN_NOTE_ACTIONED );
+ $unactioned_note_id = $this->create_note( 'phpunit-unactioned-survey-note', Note::E_WC_ADMIN_NOTE_SURVEY, Note::E_WC_ADMIN_NOTE_UNACTIONED );
+ $other_note_id = $this->create_note( 'phpunit-actioned-info-note', Note::E_WC_ADMIN_NOTE_INFORMATIONAL, Note::E_WC_ADMIN_NOTE_ACTIONED );
+
+ Notes::possibly_delete_survey_notes();
+
+ $this->assertTrue( ( new Note( $actioned_note_id ) )->get_is_deleted(), 'The actioned survey note should be deleted.' );
+ $this->assertFalse( ( new Note( $unactioned_note_id ) )->get_is_deleted(), 'The unactioned survey note should remain visible.' );
+ $this->assertFalse( ( new Note( $other_note_id ) )->get_is_deleted(), 'Actioned notes of other types should remain visible.' );
+ }
+
+ /**
+ * Create an admin note.
+ *
+ * @param string $name Note name.
+ * @param string $type Note type.
+ * @param string $status Note status.
+ * @return int Note ID.
+ */
+ private function create_note( string $name, string $type, string $status ): int {
+ $note = new Note();
+ $note->set_name( $name );
+ $note->set_title( 'Test note' );
+ $note->set_content( 'Test content' );
+ $note->set_type( $type );
+ $note->set_source( 'PHPUNIT_TEST' );
+ $note->set_status( $status );
+ $note->save();
+
+ return $note->get_id();
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Domain/BootstrapTest.php b/plugins/woocommerce/tests/php/src/Blocks/Domain/BootstrapTest.php
index 09a218625cf..ae532bda43b 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Domain/BootstrapTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Domain/BootstrapTest.php
@@ -88,6 +88,32 @@ class BootstrapTest extends WC_Unit_Test_Case {
$property->setValue( null, $has_run );
}
+ /**
+ * @testdox Bootstrap does not register admin-init work that also runs during admin AJAX.
+ */
+ public function test_bootstrap_registers_no_admin_init_callbacks(): void {
+ global $wp_filter;
+
+ $bootstrap_file = wp_normalize_path( WC_ABSPATH . 'src/Blocks/Domain/Bootstrap.php' );
+ $callbacks = $wp_filter['admin_init']->callbacks ?? array();
+
+ foreach ( $callbacks as $callbacks_at_priority ) {
+ foreach ( $callbacks_at_priority as $callback ) {
+ $callback_function = $callback['function'] ?? null;
+ if ( ! is_callable( $callback_function ) ) {
+ continue;
+ }
+ $reflection = new \ReflectionFunction( \Closure::fromCallable( $callback_function ) );
+
+ $this->assertNotSame(
+ $bootstrap_file,
+ wp_normalize_path( (string) $reflection->getFileName() ),
+ 'Bootstrap callbacks on admin_init also run during unrelated admin AJAX requests.'
+ );
+ }
+ }
+ }
+
/**
* @testdox Bootstrap hooks on-demand block-type registration to woocommerce_short_description before do_blocks.
*/