Commit a1bfa6f0aae for woocommerce

commit a1bfa6f0aae5e0a8533ae4a6c5aec7772a31ae30
Author: Luigi Teschio <gigitux@gmail.com>
Date:   Wed Jul 15 14:09:53 2026 +0200

    Add legacy Select2 usage telemetry (#66245)

    * Add legacy Select2 usage telemetry

    * Add changelog entry for Select2 usage telemetry

    * improve logic

    * improve logic

    * fix name test

    * fix unit test

    * improve logic

    * improve logic

    * remove page_url

    * fix unit test

    * fix unit test

    * remove load track function

    * store the relative path

    * fix logic

    * fix unit test

    * store handles source path

diff --git a/plugins/woocommerce/changelog/add-select2-usage-metrics b/plugins/woocommerce/changelog/add-select2-usage-metrics
new file mode 100644
index 00000000000..13ada427246
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-select2-usage-metrics
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Add telemetry for legacy Select2 script usage.
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index 33a18d52624..c808504d4d5 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -18,6 +18,7 @@ use Automattic\WooCommerce\Internal\ComingSoon\ComingSoonRequestHandler;
 use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
 use Automattic\WooCommerce\Internal\DownloadPermissionsAdjuster;
 use Automattic\WooCommerce\Internal\Features\FeaturesController;
+use Automattic\WooCommerce\Internal\LegacyAssets\LegacySelect2UsageTracker;
 use Automattic\WooCommerce\Internal\MCP\MCPAdapterProvider;
 use Automattic\WooCommerce\Internal\Abilities\AbilitiesRegistry;
 use Automattic\WooCommerce\Internal\ProductAttributes\VisualAttributeTermAdmin;
@@ -405,6 +406,7 @@ final class WooCommerce {
 		$container->get( Automattic\WooCommerce\Internal\Admin\Settings\PaymentsController::class )->register();
 		$container->get( Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\WooPayments\WooPaymentsController::class )->register();
 		$container->get( Automattic\WooCommerce\Internal\Utilities\LegacyRestApiStub::class )->register();
+		$container->get( LegacySelect2UsageTracker::class )->register();
 		$container->get( Automattic\WooCommerce\Internal\VariationGallery\Telemetry::class )->register();
 		$container->get( Automattic\WooCommerce\Internal\Email\EmailStyleSync::class )->register();
 		$container->get( EmailLogger::class )->register();
diff --git a/plugins/woocommerce/src/Internal/LegacyAssets/LegacySelect2UsageTracker.php b/plugins/woocommerce/src/Internal/LegacyAssets/LegacySelect2UsageTracker.php
new file mode 100644
index 00000000000..f2b8d8de0fd
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/LegacyAssets/LegacySelect2UsageTracker.php
@@ -0,0 +1,390 @@
+<?php
+/**
+ * Tracks usage of WooCommerce's bundled legacy Select2 handles.
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\LegacyAssets;
+
+use Automattic\WooCommerce\Internal\RegisterHooksInterface;
+use WP_Scripts;
+use WC_Site_Tracking;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Tracks extensions that still depend on the legacy Select2 handles bundled by WooCommerce.
+ */
+class LegacySelect2UsageTracker implements RegisterHooksInterface {
+
+	public const EVENT_NAME = 'legacy_select2_usage_detected';
+
+	private const CONTEXT_ADMIN    = 'admin';
+	private const CONTEXT_FRONTEND = 'frontend';
+
+	private const LEGACY_HANDLES = array(
+		'select2',
+		'wc-select2',
+	);
+
+	private const TRANSIENT_KEY_PREFIX = 'wc_legacy_select2_check_';
+
+	/**
+	 * Register hook callbacks.
+	 *
+	 * @return void
+	 *
+	 * @since 11.0.0
+	 */
+	public function register() {
+		if ( WC_Site_Tracking::is_tracking_enabled() ) {
+			add_action( 'admin_print_footer_scripts', array( $this, 'handle_admin_print_footer_scripts' ), PHP_INT_MAX );
+			add_action( 'wp_print_footer_scripts', array( $this, 'handle_wp_print_footer_scripts' ), PHP_INT_MAX );
+		}
+	}
+
+	/**
+	 * Handle the admin_print_footer_scripts hook.
+	 *
+	 * @internal
+	 *
+	 * @return void
+	 */
+	public function handle_admin_print_footer_scripts(): void {
+		$this->track_usage( self::CONTEXT_ADMIN );
+	}
+
+	/**
+	 * Handle the wp_print_footer_scripts hook.
+	 *
+	 * @internal
+	 *
+	 * @return void
+	 */
+	public function handle_wp_print_footer_scripts(): void {
+		$this->track_usage( self::CONTEXT_FRONTEND );
+	}
+
+	/**
+	 * Build and record legacy Select2 usage events for the current request.
+	 *
+	 * @param string $context The request context.
+	 * @return void
+	 */
+	private function track_usage( string $context ): void {
+		if ( ! $this->is_legacy_select2_printed() ) {
+			return;
+		}
+
+		$event = $this->get_usage_event( $context );
+		if ( empty( $event ) ) {
+			return;
+		}
+
+		if ( $this->was_recently_checked( $event ) ) {
+			return;
+		}
+
+		$this->mark_recently_checked( $event );
+		$this->record_event( self::EVENT_NAME, $event );
+	}
+
+	/**
+	 * Get a legacy Select2 usage event for the current script registry.
+	 *
+	 * @internal
+	 *
+	 * @param string $context The request context.
+	 * @return array<string, string>
+	 */
+	public function get_usage_event( string $context ): array {
+		$wp_scripts = wp_scripts();
+		if ( ! $wp_scripts instanceof WP_Scripts ) {
+			return array();
+		}
+
+		// `done` includes dependencies that WordPress printed while resolving the queue.
+		// Keep only queued handles so dependents identify scripts explicitly enqueued for the page.
+		$printed_queued_scripts = array_intersect( $wp_scripts->queue, $wp_scripts->done );
+
+		$handles            = array();
+		$dependents         = array();
+		$dependents_sources = array();
+		$plugins_path       = wp_parse_url( plugins_url( '/' ), PHP_URL_PATH );
+
+		foreach ( $printed_queued_scripts as $handle ) {
+			$legacy_handles = $this->get_legacy_handles( $wp_scripts, $handle );
+
+			if ( empty( $legacy_handles ) ) {
+				continue;
+			}
+
+			$handles              += array_fill_keys( $legacy_handles, true );
+			$dependents[ $handle ] = true;
+			$source                = isset( $wp_scripts->registered[ $handle ] ) ? $wp_scripts->registered[ $handle ]->src : '';
+			$source                = self::get_plugin_relative_source( $source, $plugins_path );
+
+			if ( '' !== $source ) {
+				$dependents_sources[] = $source;
+			}
+		}
+
+		if ( empty( $handles ) ) {
+			return array();
+		}
+
+		$handles            = array_keys( $handles );
+		$dependents         = array_keys( $dependents );
+		$dependents_sources = array_unique( $dependents_sources );
+		$handles_sources    = array();
+
+		foreach ( $handles as $handle ) {
+			$source = self::get_legacy_handle_source( $wp_scripts, $handle );
+			$source = self::get_plugin_relative_source( $source, $plugins_path );
+
+			if ( '' !== $source ) {
+				$handles_sources[] = $source;
+			}
+		}
+
+		$handles_sources = array_unique( $handles_sources );
+
+		sort( $handles );
+		sort( $dependents );
+		sort( $dependents_sources );
+		sort( $handles_sources );
+
+		return array(
+			'context'            => $context,
+			'page_type'          => $this->get_current_page_type( $context ),
+			'handles'            => implode( ',', $handles ),
+			'dependents'         => implode( ',', $dependents ),
+			'dependents_sources' => implode( ',', $dependents_sources ),
+			'handles_sources'    => implode( ',', $handles_sources ),
+		);
+	}
+
+	/**
+	 * Record a Tracks event.
+	 *
+	 * @param string                $event_name Event name.
+	 * @param array<string, string> $properties Event properties.
+	 * @return void
+	 *
+	 * @since 11.0.0
+	 */
+	protected function record_event( string $event_name, array $properties ): void {
+		if ( ! class_exists( 'WC_Tracks' ) ) {
+			return;
+		}
+
+		\WC_Tracks::record_event( $event_name, $properties );
+	}
+
+	/**
+	 * Whether this usage event was already checked recently.
+	 *
+	 * @param array<string, string> $event Usage event.
+	 * @return bool
+	 */
+	private function was_recently_checked( array $event ): bool {
+		return false !== get_transient( $this->get_transient_key( $event ) );
+	}
+
+	/**
+	 * Mark this usage event as recently checked.
+	 *
+	 * @param array<string, string> $event Usage event.
+	 * @return void
+	 */
+	private function mark_recently_checked( array $event ): void {
+		set_transient( $this->get_transient_key( $event ), 'yes', WEEK_IN_SECONDS );
+	}
+
+	/**
+	 * Check whether a legacy Select2 handle has been printed.
+	 *
+	 * @return bool
+	 */
+	private function is_legacy_select2_printed(): bool {
+		foreach ( self::LEGACY_HANDLES as $handle ) {
+			if ( wp_script_is( $handle, 'done' ) ) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Get the transient key for a usage event.
+	 *
+	 * @param array<string, string> $event Usage event.
+	 * @return string
+	 */
+	private function get_transient_key( array $event ): string {
+		ksort( $event );
+
+		$event_json = wp_json_encode( $event );
+
+		return self::TRANSIENT_KEY_PREFIX . md5( is_string( $event_json ) ? $event_json : '' );
+	}
+
+	/**
+	 * Get legacy Select2 handles for a printed top-level handle.
+	 *
+	 * @param WP_Scripts $wp_scripts WordPress scripts registry.
+	 * @param string     $handle     Script handle.
+	 * @return array<int, string>
+	 */
+	private function get_legacy_handles( WP_Scripts $wp_scripts, string $handle ): array {
+		$legacy_handles = array();
+
+		if ( in_array( $handle, self::LEGACY_HANDLES, true ) ) {
+			$legacy_handles[ $handle ] = true;
+			return array_keys( $legacy_handles );
+		}
+
+		if ( isset( $wp_scripts->registered[ $handle ] ) ) {
+			foreach ( $wp_scripts->registered[ $handle ]->deps as $dependency_handle ) {
+				if ( in_array( $dependency_handle, self::LEGACY_HANDLES, true ) ) {
+					$legacy_handles[ $dependency_handle ] = true;
+				}
+			}
+		}
+
+		return array_keys( $legacy_handles );
+	}
+
+	/**
+	 * Get a script source relative to the plugins directory.
+	 *
+	 * @param string|false      $source       Script source.
+	 * @param string|false|null $plugins_path Plugins directory URL path.
+	 * @return string
+	 */
+	private static function get_plugin_relative_source( $source, $plugins_path ): string {
+		if ( ! is_string( $source ) || '' === $source || ! is_string( $plugins_path ) || '' === $plugins_path ) {
+			return '';
+		}
+
+		$source_path = wp_parse_url( $source, PHP_URL_PATH );
+		if ( ! is_string( $source_path ) || ! str_starts_with( $source_path, $plugins_path ) ) {
+			return '';
+		}
+
+		return ltrim( substr( $source_path, strlen( $plugins_path ) ), '/' );
+	}
+
+	/**
+	 * Get the source for a legacy Select2 handle.
+	 *
+	 * @param WP_Scripts $wp_scripts WordPress scripts registry.
+	 * @param string     $handle     Script handle.
+	 * @return string
+	 */
+	private static function get_legacy_handle_source( WP_Scripts $wp_scripts, string $handle ): string {
+		$source_handle     = 'select2' === $handle ? 'wc-select2' : $handle;
+		$registered_script = $wp_scripts->registered[ $source_handle ] ?? null;
+
+		if ( null === $registered_script ) {
+			return '';
+		}
+
+		return is_string( $registered_script->src ) ? $registered_script->src : '';
+	}
+
+	/**
+	 * Get the current admin screen ID.
+	 *
+	 * @return string
+	 */
+	private function get_current_screen_id(): string {
+		if ( ! function_exists( 'get_current_screen' ) ) {
+			return '';
+		}
+
+		$screen = get_current_screen();
+		return $screen ? (string) $screen->id : '';
+	}
+
+	/**
+	 * Get the current page type.
+	 *
+	 * @param string $context The request context.
+	 * @return string
+	 */
+	private function get_current_page_type( string $context ): string {
+		if ( self::CONTEXT_ADMIN === $context ) {
+			return $this->get_current_screen_id();
+		}
+
+		if ( self::CONTEXT_FRONTEND === $context ) {
+			return $this->get_current_frontend_page_type();
+		}
+
+		return '';
+	}
+
+	/**
+	 * Get the current frontend page type.
+	 *
+	 * @return string
+	 */
+	private function get_current_frontend_page_type(): string {
+		if ( is_cart() ) {
+			return 'cart';
+		}
+
+		if ( is_checkout() ) {
+			return 'checkout';
+		}
+
+		if ( is_account_page() ) {
+			return 'my_account';
+		}
+
+		if ( is_shop() ) {
+			return 'shop';
+		}
+
+		if ( is_product() ) {
+			return 'product';
+		}
+
+		if ( is_product_category() ) {
+			return 'product_category';
+		}
+
+		if ( is_product_tag() ) {
+			return 'product_tag';
+		}
+
+		if ( is_product_taxonomy() ) {
+			return 'product_taxonomy';
+		}
+
+		if ( is_front_page() ) {
+			return 'front_page';
+		}
+
+		if ( is_home() ) {
+			return 'home';
+		}
+
+		if ( is_search() ) {
+			return 'search';
+		}
+
+		if ( is_archive() ) {
+			return 'archive';
+		}
+
+		if ( is_singular() ) {
+			return 'singular';
+		}
+
+		return 'other';
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/LegacyAssets/LegacySelect2UsageTrackerTest.php b/plugins/woocommerce/tests/php/src/Internal/LegacyAssets/LegacySelect2UsageTrackerTest.php
new file mode 100644
index 00000000000..44621681346
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/LegacyAssets/LegacySelect2UsageTrackerTest.php
@@ -0,0 +1,455 @@
+<?php
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\LegacyAssets;
+
+use Automattic\Jetpack\Constants;
+use Automattic\WooCommerce\Internal\LegacyAssets\LegacySelect2UsageTracker;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the LegacySelect2UsageTracker class.
+ */
+class LegacySelect2UsageTrackerTest extends WC_Unit_Test_Case {
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var LegacySelect2UsageTracker
+	 */
+	private LegacySelect2UsageTracker $sut;
+
+	/**
+	 * Script handles registered by the tests.
+	 *
+	 * @var array<int, string>
+	 */
+	private array $test_script_handles = array(
+		'my-extension-admin',
+		'my-extension-footer',
+		'intermediate-select2-wrapper',
+		'my-extension-transitive',
+	);
+
+	/**
+	 * Original request URI.
+	 *
+	 * @var string|null
+	 */
+	private ?string $original_request_uri = null;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Test fixture preserves the raw request URI for restoration.
+		$this->original_request_uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) $_SERVER['REQUEST_URI'] : null;
+		$_SERVER['REQUEST_URI']     = '/';
+		$this->reset_scripts();
+		$this->register_legacy_select2_scripts();
+		$this->sut = new LegacySelect2UsageTracker();
+	}
+
+	/**
+	 * Tear down test fixtures.
+	 */
+	public function tearDown(): void {
+		$this->reset_scripts();
+		set_current_screen( 'front' );
+
+		if ( null === $this->original_request_uri ) {
+			unset( $_SERVER['REQUEST_URI'] );
+		} else {
+			$_SERVER['REQUEST_URI'] = $this->original_request_uri;
+		}
+
+		parent::tearDown();
+	}
+
+	/**
+	 * @testdox Should track a plugin-owned header dependency on select2.
+	 */
+	public function test_tracks_plugin_owned_header_dependency_on_select2(): void {
+		set_current_screen( 'woocommerce_page_wc-settings' );
+		$_SERVER['REQUEST_URI'] = '/wp-admin/admin.php?page=wc-settings';
+		wp_register_script(
+			'my-extension-admin',
+			$this->get_my_extension_asset_url( 'admin.js' ),
+			array( 'select2' ),
+			'1.0.0',
+			false
+		);
+		wp_enqueue_script( 'my-extension-admin' );
+
+		$this->print_script_group( 0 );
+
+		$this->assertSame(
+			array(
+				'context'            => 'admin',
+				'page_type'          => 'woocommerce_page_wc-settings',
+				'handles'            => 'select2',
+				'dependents'         => 'my-extension-admin',
+				'dependents_sources' => 'my-extension/assets/admin.js',
+				'handles_sources'    => $this->get_expected_wc_select2_source(),
+			),
+			$this->sut->get_usage_event( 'admin' ),
+			'Admin usage should include only the expected event properties.'
+		);
+	}
+
+	/**
+	 * @testdox Should track a plugin-owned footer dependency on wc-select2.
+	 */
+	public function test_tracks_plugin_owned_footer_dependency_on_wc_select2(): void {
+		wp_register_script(
+			'my-extension-footer',
+			$this->get_my_extension_asset_url( 'footer.js' ),
+			array( 'wc-select2' ),
+			'1.0.0',
+			true
+		);
+		wp_enqueue_script( 'my-extension-footer' );
+
+		$this->print_script_group( 1 );
+
+		$this->assertSame(
+			array(
+				'context'            => 'frontend',
+				'page_type'          => $this->get_expected_frontend_page_type(),
+				'handles'            => 'wc-select2',
+				'dependents'         => 'my-extension-footer',
+				'dependents_sources' => 'my-extension/assets/footer.js',
+				'handles_sources'    => $this->get_expected_wc_select2_source(),
+			),
+			$this->sut->get_usage_event( 'frontend' ),
+			'Frontend usage should report the direct wc-select2 handle.'
+		);
+	}
+
+	/**
+	 * @testdox Should not track transitive dependencies reaching select2.
+	 */
+	public function test_does_not_track_transitive_dependency_reaching_select2(): void {
+		wp_register_script(
+			'intermediate-select2-wrapper',
+			$this->get_my_extension_asset_url( 'wrapper.js' ),
+			array( 'select2' ),
+			'1.0.0',
+			true
+		);
+		wp_register_script(
+			'my-extension-transitive',
+			$this->get_my_extension_asset_url( 'transitive.js' ),
+			array( 'intermediate-select2-wrapper' ),
+			'1.0.0',
+			true
+		);
+		wp_enqueue_script( 'my-extension-transitive' );
+
+		$this->print_script_group( 1 );
+
+		$this->assertSame( array(), $this->sut->get_usage_event( 'frontend' ), 'Transitive dependencies are intentionally ignored to keep tracking bounded.' );
+	}
+
+	/**
+	 * @testdox Should track direct legacy handle enqueue without an attributable plugin.
+	 */
+	public function test_tracks_direct_legacy_handle_enqueue_without_attributable_plugin(): void {
+		wp_enqueue_script( 'select2' );
+
+		$this->print_script_group( 1 );
+
+		$this->assertSame(
+			array(
+				'context'            => 'frontend',
+				'page_type'          => $this->get_expected_frontend_page_type(),
+				'handles'            => 'select2',
+				'dependents'         => 'select2',
+				'dependents_sources' => '',
+				'handles_sources'    => $this->get_expected_wc_select2_source(),
+			),
+			$this->sut->get_usage_event( 'frontend' ),
+			'Direct Select2 enqueue should report the requested handle.'
+		);
+	}
+
+	/**
+	 * @testdox Should not track selectWoo usage alone.
+	 */
+	public function test_does_not_track_selectwoo_alone(): void {
+		wp_register_script(
+			'selectWoo',
+			plugins_url( 'woocommerce/assets/js/selectWoo/selectWoo.full.js' ),
+			array( 'jquery' ),
+			'1.0.0',
+			true
+		);
+		wp_enqueue_script( 'selectWoo' );
+
+		$this->print_script_group( 1 );
+
+		$this->assertSame( array(), $this->sut->get_usage_event( 'frontend' ), 'selectWoo alone should not trigger legacy Select2 usage tracking.' );
+	}
+
+	/**
+	 * @testdox Should not track queued dependencies before they are printed.
+	 */
+	public function test_does_not_track_queued_dependencies_before_they_are_printed(): void {
+		set_current_screen( 'woocommerce_page_wc-settings' );
+		wp_register_script(
+			'my-extension-admin',
+			$this->get_my_extension_asset_url( 'admin.js' ),
+			array( 'select2' ),
+			'1.0.0',
+			false
+		);
+		wp_enqueue_script( 'my-extension-admin' );
+
+		$this->assertSame( array(), $this->sut->get_usage_event( 'admin' ), 'Queued scripts should not be reported before WordPress prints them.' );
+	}
+
+	/**
+	 * @testdox Should report footer dependencies after footer scripts are printed.
+	 */
+	public function test_reports_footer_dependencies_after_footer_scripts_are_printed(): void {
+		wp_register_script(
+			'my-extension-footer',
+			$this->get_my_extension_asset_url( 'footer.js' ),
+			array( 'wc-select2' ),
+			'1.0.0',
+			true
+		);
+		wp_enqueue_script( 'my-extension-footer' );
+
+		$this->print_script_group( 0 );
+
+		$this->assertSame( array(), $this->sut->get_usage_event( 'frontend' ), 'Footer usage should not be reported before footer scripts are printed.' );
+
+		$this->print_script_group( 1 );
+
+		$this->assertSame(
+			array(
+				'context'            => 'frontend',
+				'page_type'          => $this->get_expected_frontend_page_type(),
+				'handles'            => 'wc-select2',
+				'dependents'         => 'my-extension-footer',
+				'dependents_sources' => 'my-extension/assets/footer.js',
+				'handles_sources'    => $this->get_expected_wc_select2_source(),
+			),
+			$this->sut->get_usage_event( 'frontend' ),
+			'Footer dependencies should be reported after footer scripts are printed.'
+		);
+	}
+
+	/**
+	 * @testdox Should not track registered plugin dependencies that are not enqueued.
+	 */
+	public function test_does_not_track_registered_plugin_dependencies_that_are_not_enqueued(): void {
+		wp_register_script(
+			'my-extension-footer',
+			$this->get_my_extension_asset_url( 'footer.js' ),
+			array( 'wc-select2' ),
+			'1.0.0',
+			true
+		);
+
+		$this->assertSame( array(), $this->sut->get_usage_event( 'frontend' ), 'Registered scripts should not be reported until they are enqueued.' );
+	}
+
+	/**
+	 * @testdox Should record each detected usage event only once per week.
+	 */
+	public function test_records_each_detected_usage_event_only_once_per_week(): void {
+		$original_request_uri   = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : null;
+		$_SERVER['REQUEST_URI'] = '/shop/?filter=featured';
+		$event                  = array(
+			'context'            => 'frontend',
+			'page_type'          => $this->get_expected_frontend_page_type(),
+			'handles'            => 'wc-select2',
+			'dependents'         => 'my-extension-footer',
+			'dependents_sources' => 'my-extension/assets/footer.js',
+			'handles_sources'    => $this->get_expected_wc_select2_source(),
+		);
+
+		$this->delete_usage_event_transient( $event );
+
+		$sut = new class() extends LegacySelect2UsageTracker {
+			/**
+			 * The number of times the usage event was built.
+			 *
+			 * @var int
+			 */
+			public int $usage_event_calls = 0;
+
+			/**
+			 * Recorded Tracks events.
+			 *
+			 * @var array<int, array{name: string, properties: array<string, string>}>
+			 */
+			public array $recorded_events = array();
+
+			/**
+			 * Get a legacy Select2 usage event for the current script registry.
+			 *
+			 * @internal
+			 *
+			 * @param string $context The request context.
+			 * @return array<string, string>
+			 */
+			public function get_usage_event( string $context ): array {
+				++$this->usage_event_calls;
+
+				return parent::get_usage_event( $context );
+			}
+
+			/**
+			 * Record a Tracks event.
+			 *
+			 * @param string                $event_name Event name.
+			 * @param array<string, string> $properties Event properties.
+			 * @return void
+			 */
+			protected function record_event( string $event_name, array $properties ): void {
+				$this->recorded_events[] = array(
+					'name'       => $event_name,
+					'properties' => $properties,
+				);
+			}
+		};
+
+		wp_register_script(
+			'my-extension-footer',
+			$this->get_my_extension_asset_url( 'footer.js' ),
+			array( 'wc-select2' ),
+			'1.0.0',
+			true
+		);
+		wp_enqueue_script( 'my-extension-footer' );
+
+		$this->print_script_group( 1 );
+		$sut->handle_wp_print_footer_scripts();
+
+		$_SERVER['REQUEST_URI'] = '/product/hoodie/';
+
+		$sut->handle_wp_print_footer_scripts();
+
+		$this->assertSame(
+			array(
+				array(
+					'name'       => LegacySelect2UsageTracker::EVENT_NAME,
+					'properties' => $event,
+				),
+			),
+			$sut->recorded_events,
+			'Repeated detections of the same legacy Select2 usage event should be rate limited.'
+		);
+		$this->assertSame( 2, $sut->usage_event_calls, 'Each detection should scan the script registry before rate limiting the exact usage event.' );
+
+		$this->delete_usage_event_transient( $event );
+
+		if ( null === $original_request_uri ) {
+			unset( $_SERVER['REQUEST_URI'] );
+		} else {
+			$_SERVER['REQUEST_URI'] = $original_request_uri;
+		}
+	}
+
+	/**
+	 * Register WooCommerce's legacy Select2 handles.
+	 */
+	private function register_legacy_select2_scripts(): void {
+		if ( ! class_exists( 'WC_Admin_Assets' ) && defined( 'WC_ABSPATH' ) ) {
+			include_once WC_ABSPATH . 'includes/admin/class-wc-admin-assets.php';
+		}
+
+		$admin_assets_reflection = new \ReflectionClass( \WC_Admin_Assets::class );
+		$admin_assets            = $admin_assets_reflection->newInstanceWithoutConstructor();
+		$admin_assets->register_scripts();
+	}
+
+	/**
+	 * Reset the global scripts registry runtime state.
+	 */
+	private function reset_scripts(): void {
+		$wp_scripts = wp_scripts();
+
+		$wp_scripts->queue     = array();
+		$wp_scripts->to_do     = array();
+		$wp_scripts->done      = array();
+		$wp_scripts->groups    = array();
+		$wp_scripts->in_footer = array();
+		$wp_scripts->args      = array();
+
+		foreach ( $this->test_script_handles as $handle ) {
+			wp_deregister_script( $handle );
+		}
+	}
+
+	/**
+	 * Print queued scripts for a script group while discarding script output.
+	 *
+	 * @param int $group Script group. 0 for header, 1 for footer.
+	 */
+	private function print_script_group( int $group ): void {
+		ob_start();
+
+		if ( 0 === $group ) {
+			wp_scripts()->do_head_items();
+		} else {
+			wp_scripts()->do_footer_items();
+		}
+
+		ob_end_clean();
+	}
+
+	/**
+	 * Get a plugin fixture asset URL.
+	 *
+	 * @param string $asset Asset filename.
+	 * @return string
+	 */
+	private function get_my_extension_asset_url( string $asset ): string {
+		return plugins_url( 'my-extension/assets/' . $asset );
+	}
+
+	/**
+	 * Get the frontend page type expected for the current PHPUnit process.
+	 *
+	 * Some tests render cart flows that define WOOCOMMERCE_CART. Since PHP
+	 * constants cannot be undefined, this test must accept the inherited cart
+	 * page type when it runs later in the same process.
+	 *
+	 * @return string
+	 */
+	private function get_expected_frontend_page_type(): string {
+		return Constants::is_defined( 'WOOCOMMERCE_CART' ) ? 'cart' : 'other';
+	}
+
+	/**
+	 * Get the expected WooCommerce Select2 source path.
+	 *
+	 * @return string
+	 */
+	private function get_expected_wc_select2_source(): string {
+		$suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min';
+
+		return 'woocommerce/assets/js/select2/select2.full' . $suffix . '.js';
+	}
+
+	/**
+	 * Delete the transient used to rate limit a usage event.
+	 *
+	 * @param array<string, string> $event Usage event.
+	 */
+	private function delete_usage_event_transient( array $event ): void {
+		ksort( $event );
+
+		$event_json = wp_json_encode( $event );
+
+		delete_transient(
+			'wc_legacy_select2_check_' . md5( is_string( $event_json ) ? $event_json : '' )
+		);
+	}
+}