Commit e11ae8b63f for woocommerce
commit e11ae8b63f1d0aa2ee51fc6c40d61d842e013ffb
Author: Jasper Kang <kangzjnet@gmail.com>
Date: Fri Dec 19 14:37:08 2025 +1300
Add batched pixel requests to WC_Tracks_Client for improved performance (#62461)
* Add batched pixel requests to WC_Tracks_Client for improved performance
This commit implements request batching for tracking pixels to improve performance by sending multiple tracking events together rather than individually.
Changes:
* Queue tracking pixels during request execution instead of sending immediately
* Send all queued pixels together on the shutdown hook using WordPress Requests library
* Use Requests::request_multiple() with blocking => false for parallel non-blocking execution
* Fallback to sequential wp_safe_remote_get() when batch API is unavailable
* Add wc_tracks_use_batch_requests filter to allow developers to disable batching
* Add comprehensive PHPUnit tests for batching functionality
Benefits:
* Performance improvement: Parallel execution reduces total request time
* Non-blocking: Uses blocking => false to ensure zero impact on page load time
* Efficient batching: Reduces overhead by grouping related tracking requests
* WordPress-native: Uses WordPress Requests library instead of direct curl usage
* Graceful degradation: Automatically falls back when batching is unavailable
* Backward compatible: Maintains full compatibility with existing WC_Tracks_Client usage
* Intercept HTTP requests in WC_Tracks_Client tests
Add HTTP request interception to prevent actual network calls during testing.
Changes:
* Add pre_http_request filter to intercept pixel requests
* Track intercepted requests in $intercepted_requests property
* Update tests to verify intercepted requests instead of actual sends
* Add test_http_requests_are_intercepted to verify interception works
* Add test_fallback_requests_are_intercepted for non-batched requests
* Verify non-blocking parameter is set correctly in intercepted requests
This ensures tests run without making actual HTTP calls to pixel.wp.com
* Address CodeRabbit AI feedback on tests
- Add else clause to test_can_use_batch_requests_when_supported to verify false when library unavailable
- Rename test_send_with_requests_multiple_handles_exceptions_gracefully to test_send_with_requests_multiple_handles_empty_array to match actual behavior
- Use addToAssertionCount() instead of assertTrue(true) for explicit assertion tracking
* Add changelog entry for batched pixel requests
* Refactor batched pixel tracking to preserve backward compatibility
Changed the architecture to add new `record_pixel_batched()` and `record_event_batched()` methods instead of modifying the existing `record_pixel()` method. This ensures that any code relying on immediate pixel sending continues to work unchanged.
Key changes:
- Restored `record_pixel()` to its original immediate sending behavior
- Added `record_pixel_batched()` for batched pixel requests
- Added `record_event_batched()` for batched event recording
- Updated all batching tests to use the new methods
- Added new test for immediate sending behavior
* Remove strict_types declaration from WC_Tracks_Client
The declare(strict_types=1) declaration was removed to maintain consistency with the existing codebase style in the includes/ directory, which doesn't use strict type declarations.
* Update PHPStan baseline for batched pixel methods
Updated baseline to account for the new record_event_batched() method which duplicates some patterns from record_event():
- Updated count for is_wp_error check from 1 to 2
- Updated count for build_pixel_url parameter check from 1 to 2
- Added ignores for method_exists checks in can_use_batch_requests()
- Added ignores for missing return types in new private methods
* Add blank line after docblock in WC_Tracks_Client
Minor formatting improvement to add spacing between docblock and use statements.
* Fix PHPStan baseline pattern escaping for WpOrg namespace
Corrected the regex pattern to use proper backslash escaping (2 backslashes instead of 4) to match the actual error message format.
* Apply suggestion from @kangzj
* Revert PHPStan baseline escaping to use 4 backslashes
The regex pattern needs 4 backslashes to properly match 2 backslashes in the error message. Each backslash in the error requires 2 backslashes in the regex pattern to escape it.
* Fix changelog type from 'add' to 'performance'
Changed Type field to 'performance' to match the actual nature of this change - it's a performance improvement rather than a new feature.
* Add filter for intercepting batched requests in tests
- Added wc_tracks_batch_requests_before_send filter to allow tests to intercept batched requests
- Added intercept_batch_requests method in tests to capture batched requests without making actual HTTP calls
- Re-added strict_types declaration as required by linter
- Fixed test failures where Requests::request_multiple() bypassed pre_http_request filter
* Remove unmatched method_exists patterns from PHPStan baseline
These baseline entries were not matching the actual reported errors, likely due to PHPStan's ellipsis formatting of long class names in error messages.
* Add return type hints to fix PHPStan errors
Added void return type hints to:
- queue_pixel_for_batch()
- send_batched_pixels()
- send_with_requests_multiple()
Removed these entries from PHPStan baseline since they're now properly fixed.
* Add changefile(s) from automation for the following project(s): woocommerce
* Delete plugins/woocommerce/changelog/62461-add-batched-pixel-requests-to-wc-tracks-client
* Remove filter added solely for testing purposes
Removed wc_tracks_batch_requests_before_send filter that was only added to intercept requests in tests. Refactored tests to focus on verifying queue behavior rather than intercepting HTTP calls:
- Removed intercept_batch_requests() method from tests
- Updated tests to verify queue management instead of HTTP interception
- Fixed tests to use record_event_batched() instead of record_event()
- Tests now properly validate batching behavior without relying on production filters
* Add parameter type hints per CodeRabbit feedback
Added string and array type hints to private method parameters:
- queue_pixel_for_batch(): Added string type hint for $pixel parameter
- send_with_requests_multiple(): Added array type hint for $pixels parameter
This improves type safety and aligns with the strict_types declaration.
* Add PHPStan baseline entries for method_exists checks
Suppressed PHPStan warnings for method_exists() checks on Requests::request_multiple(). These checks are intentionally defensive programming to verify both class existence and method availability across different WordPress/Requests library versions.
* Fix PHPStan baseline pattern to match ellipsis in error message
Changed the baseline pattern to use '.+' wildcard to match PHPStan's ellipsis formatting of long class names (e.g., 'WpOrg\\Requests…' instead of the full 'WpOrg\\Requests\\Requests').
* Fix build_pixel_url() calls to use correct signature
Removed unnecessary parameter from build_pixel_url() calls - the method requires 0 parameters but was being invoked with 1.
- Fixed record_event() to call build_pixel_url() without parameter
- Fixed record_event_batched() to call build_pixel_url() without parameter
- Removed PHPStan baseline entry since issue is now resolved
* Apply suggestion from @kangzj
* Fix error handling in record_event_batched()
Corrected the error check to properly access the error property on WC_Tracks_Event object:
- Changed from checking is_wp_error($event) which always returns false
- Now checks isset($event->error) && is_wp_error($event->error)
- Returns $event->error instead of $event when an error occurs
- Updated PHPStan baseline count from 2 to 1 since one instance is now fixed
* Fix PHPDoc type for WC_Tracks_Event::error property
The error property is only set when validation fails, so it should be
typed as WP_Error|null to accurately reflect its nullable nature.
* Update PHPStan baseline and fix method call
* Move PHPStan suppressions from baseline to inline comments
Replace PHPStan baseline entries for method_exists checks with inline
@phpstan-ignore comments in can_use_batch_requests() method.
* Update PHPStan baseline with count and path fields
* Add periods to PHPStan ignore comments
* Move PHPStan ignore comments to end of lines
* add testdox block
---------
Co-authored-by: github-actions <github-actions@github.com>
diff --git a/plugins/woocommerce/changelog/62461-add-tracks-client-batch-requests b/plugins/woocommerce/changelog/62461-add-tracks-client-batch-requests
new file mode 100644
index 0000000000..c45191bd29
--- /dev/null
+++ b/plugins/woocommerce/changelog/62461-add-tracks-client-batch-requests
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Implement batched pixel requests for improved performance when sending tracking events
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/tracks/class-wc-tracks-client.php b/plugins/woocommerce/includes/tracks/class-wc-tracks-client.php
index 7426fe4c37..52a21ab65f 100644
--- a/plugins/woocommerce/includes/tracks/class-wc-tracks-client.php
+++ b/plugins/woocommerce/includes/tracks/class-wc-tracks-client.php
@@ -30,6 +30,20 @@ class WC_Tracks_Client {
*/
const USER_AGENT_SLUG = 'tracks-client';
+ /**
+ * Batch pixel queue for batched requests.
+ *
+ * @var array
+ */
+ private static $pixel_batch_queue = array();
+
+ /**
+ * Whether the shutdown hook has been registered.
+ *
+ * @var bool
+ */
+ private static $shutdown_hook_registered = false;
+
/**
* Initialize tracks client class
*
@@ -101,6 +115,33 @@ class WC_Tracks_Client {
return self::record_pixel( $pixel );
}
+ /**
+ * Record a Tracks event using batched requests for improved performance.
+ * Events are queued and sent together on the shutdown hook.
+ *
+ * @since 10.5.0
+ *
+ * @param array $event Array of event properties.
+ * @return bool|WP_Error True on success, WP_Error on failure.
+ */
+ public static function record_event_batched( $event ) {
+ if ( ! $event instanceof WC_Tracks_Event ) {
+ $event = new WC_Tracks_Event( $event );
+ }
+
+ if ( isset( $event->error ) && is_wp_error( $event->error ) ) {
+ return $event->error;
+ }
+
+ $pixel = $event->build_pixel_url();
+
+ if ( ! $pixel ) {
+ return new WP_Error( 'invalid_pixel', 'cannot generate tracks pixel for given input', 400 );
+ }
+
+ return self::record_pixel_batched( $pixel );
+ }
+
/**
* Synchronously request the pixel.
*
@@ -124,6 +165,38 @@ class WC_Tracks_Client {
return true;
}
+ /**
+ * Record a pixel using batched requests for improved performance.
+ * Pixels are queued and sent together on the shutdown hook.
+ *
+ * @since 10.5.0
+ *
+ * @param string $pixel pixel url and query string.
+ * @return bool Always returns true.
+ */
+ public static function record_pixel_batched( $pixel ) {
+ // Check if batching is enabled and supported.
+ $use_batching = self::can_use_batch_requests();
+
+ /**
+ * Filters whether to use batch requests for tracking pixels.
+ *
+ * @since 10.5.0
+ *
+ * @param bool $use_batching Whether to use batch requests. Default true if supported.
+ */
+ $use_batching = apply_filters( 'wc_tracks_use_batch_requests', $use_batching );
+
+ if ( $use_batching ) {
+ // Queue the pixel and send on shutdown.
+ self::queue_pixel_for_batch( $pixel );
+ return true;
+ }
+
+ // Fallback to immediate sending if batching is not supported or disabled.
+ return self::record_pixel( $pixel );
+ }
+
/**
* Create a timestamp representing milliseconds since 1970-01-01
*
@@ -222,6 +295,101 @@ class WC_Tracks_Client {
return $anon_id;
}
+
+ /**
+ * Check if batch requests are supported.
+ *
+ * @since 10.5.0
+ *
+ * @return bool Whether batch requests are supported.
+ */
+ private static function can_use_batch_requests() {
+ // Check if the Requests library supports request_multiple().
+ return ( class_exists( 'WpOrg\Requests\Requests' ) && method_exists( 'WpOrg\Requests\Requests', 'request_multiple' ) ) // @phpstan-ignore function.alreadyNarrowedType
+ || ( class_exists( 'Requests' ) && method_exists( 'Requests', 'request_multiple' ) ); // @phpstan-ignore function.alreadyNarrowedType
+ }
+
+ /**
+ * Queue a pixel URL for batch sending.
+ *
+ * @since 10.5.0
+ *
+ * @param string $pixel The pixel URL to queue.
+ */
+ private static function queue_pixel_for_batch( string $pixel ): void {
+ self::$pixel_batch_queue[] = $pixel;
+
+ // Register shutdown hook once.
+ if ( ! self::$shutdown_hook_registered ) {
+ add_action( 'shutdown', array( __CLASS__, 'send_batched_pixels' ), 20 );
+ self::$shutdown_hook_registered = true;
+ }
+ }
+
+ /**
+ * Send all queued pixels using batched non-blocking requests.
+ * This runs on the shutdown hook to batch all requests together.
+ *
+ * Uses Requests library's request_multiple() for true parallel batching via curl_multi.
+ *
+ * @since 10.5.0
+ */
+ public static function send_batched_pixels(): void {
+ if ( empty( self::$pixel_batch_queue ) ) {
+ return;
+ }
+
+ // Add request timestamp and nocache to all pixels.
+ $pixels_to_send = array();
+ foreach ( self::$pixel_batch_queue as $pixel ) {
+ $pixels_to_send[] = self::add_request_timestamp_and_nocache( $pixel );
+ }
+
+ // Send with Requests library for true parallel batching.
+ self::send_with_requests_multiple( $pixels_to_send );
+
+ // Clear the queue.
+ self::$pixel_batch_queue = array();
+ }
+
+ /**
+ * Send pixels using Requests::request_multiple() for parallel non-blocking execution.
+ * Uses blocking => false for true non-blocking behavior via curl_multi.
+ *
+ * @since 10.5.0
+ *
+ * @param array $pixels Array of pixel URLs to send.
+ */
+ private static function send_with_requests_multiple( array $pixels ): void {
+ $requests = array();
+ $options = array(
+ 'blocking' => false, // Non-blocking mode - returns immediately.
+ 'timeout' => 1,
+ );
+
+ foreach ( $pixels as $pixel ) {
+ $requests[] = array(
+ 'url' => $pixel,
+ 'headers' => array(),
+ 'data' => array(),
+ 'type' => 'GET',
+ );
+ }
+
+ try {
+ // Try modern namespaced version first.
+ if ( class_exists( 'WpOrg\Requests\Requests' ) ) {
+ \WpOrg\Requests\Requests::request_multiple( $requests, $options );
+ } elseif ( class_exists( 'Requests' ) ) {
+ \Requests::request_multiple( $requests, $options ); // phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.requestsDeprecated
+ }
+ } catch ( \Exception $e ) {
+ // Log error but don't break the site - tracking pixels should fail gracefully.
+ if ( function_exists( 'wc_get_logger' ) ) {
+ wc_get_logger()->error( 'WC_Tracks_Client: Batch pixel request failed - ' . $e->getMessage(), array( 'source' => 'wc-tracks' ) );
+ }
+ }
+ }
}
WC_Tracks_Client::init();
diff --git a/plugins/woocommerce/includes/tracks/class-wc-tracks-event.php b/plugins/woocommerce/includes/tracks/class-wc-tracks-event.php
index 3e3d9a656f..cd84fe3386 100644
--- a/plugins/woocommerce/includes/tracks/class-wc-tracks-event.php
+++ b/plugins/woocommerce/includes/tracks/class-wc-tracks-event.php
@@ -28,7 +28,7 @@ class WC_Tracks_Event {
/**
* Error message as WP_Error.
*
- * @var WP_Error
+ * @var WP_Error|null
*/
public $error;
diff --git a/plugins/woocommerce/tests/php/includes/tracks/class-wc-tracks-client-test.php b/plugins/woocommerce/tests/php/includes/tracks/class-wc-tracks-client-test.php
new file mode 100644
index 0000000000..ad0691162b
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/tracks/class-wc-tracks-client-test.php
@@ -0,0 +1,521 @@
+<?php
+declare(strict_types=1);
+
+/**
+ * Class WC_Tracks_Client_Test.
+ */
+class WC_Tracks_Client_Test extends \WC_Unit_Test_Case {
+
+ /**
+ * Array to track intercepted HTTP requests.
+ *
+ * @var array
+ */
+ private $intercepted_requests = array();
+
+ /**
+ * Set up test.
+ *
+ * @return void
+ */
+ public function setUp(): void {
+ parent::setUp();
+ include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks.php';
+ include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-client.php';
+ include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-event.php';
+
+ // Clear any existing batch queue and hooks.
+ $this->reset_batch_state();
+
+ // Clear intercepted requests.
+ $this->intercepted_requests = array();
+
+ // Intercept HTTP requests to prevent actual network calls.
+ add_filter( 'pre_http_request', array( $this, 'intercept_http_requests' ), 10, 3 );
+ }
+
+ /**
+ * Tear down test.
+ *
+ * @return void
+ */
+ public function tearDown(): void {
+ $this->reset_batch_state();
+ remove_filter( 'pre_http_request', array( $this, 'intercept_http_requests' ), 10 );
+ parent::tearDown();
+ }
+
+ /**
+ * Intercept HTTP requests to prevent actual network calls during testing.
+ *
+ * @param false|array|WP_Error $response A preemptive return value of an HTTP request.
+ * @param array $parsed_args HTTP request arguments.
+ * @param string $url The request URL.
+ * @return array Mocked HTTP response.
+ */
+ public function intercept_http_requests( $response, $parsed_args, $url ) {
+ // Track the intercepted request.
+ if ( strpos( $url, 'pixel.wp.com/t.gif' ) !== false ) {
+ $this->intercepted_requests[] = array(
+ 'url' => $url,
+ 'args' => $parsed_args,
+ );
+
+ // Return a successful mock response.
+ return array(
+ 'response' => array(
+ 'code' => 200,
+ 'message' => 'OK',
+ ),
+ 'body' => '',
+ );
+ }
+
+ return $response;
+ }
+
+ /**
+ * Reset the batch state using reflection.
+ *
+ * @return void
+ */
+ private function reset_batch_state() {
+ $reflection = new ReflectionClass( 'WC_Tracks_Client' );
+
+ $queue_property = $reflection->getProperty( 'pixel_batch_queue' );
+ $queue_property->setAccessible( true );
+ $queue_property->setValue( array() );
+
+ $hook_property = $reflection->getProperty( 'shutdown_hook_registered' );
+ $hook_property->setAccessible( true );
+ $hook_property->setValue( false );
+
+ // Remove shutdown hook if it exists.
+ remove_action( 'shutdown', array( 'WC_Tracks_Client', 'send_batched_pixels' ), 20 );
+ }
+
+ /**
+ * Get the pixel batch queue using reflection.
+ *
+ * @return array
+ */
+ private function get_batch_queue() {
+ $reflection = new ReflectionClass( 'WC_Tracks_Client' );
+ $property = $reflection->getProperty( 'pixel_batch_queue' );
+ $property->setAccessible( true );
+ return $property->getValue();
+ }
+
+ /**
+ * @testdox Should detect batch request support when Requests library is available.
+ */
+ public function test_can_use_batch_requests_when_supported() {
+ $reflection = new ReflectionClass( 'WC_Tracks_Client' );
+ $method = $reflection->getMethod( 'can_use_batch_requests' );
+ $method->setAccessible( true );
+
+ $can_batch = $method->invoke( null );
+
+ // Should return true if either WpOrg\Requests\Requests or Requests class exists with request_multiple method.
+ if ( class_exists( 'WpOrg\Requests\Requests' ) || class_exists( 'Requests' ) ) {
+ $this->assertTrue( $can_batch );
+ } else {
+ $this->assertFalse( $can_batch );
+ }
+ }
+
+ /**
+ * @testdox Should queue pixels when batching is enabled.
+ */
+ public function test_record_pixel_batched_queues_pixel_when_batching_enabled() {
+ // Ensure batching is supported.
+ if ( ! class_exists( 'WpOrg\Requests\Requests' ) && ! class_exists( 'Requests' ) ) {
+ $this->markTestSkipped( 'Requests library not available for batching.' );
+ }
+
+ $pixel = 'https://pixel.wp.com/t.gif?_en=test_event&test=value';
+
+ // Record the pixel using batched method.
+ WC_Tracks_Client::record_pixel_batched( $pixel );
+
+ // Get the batch queue.
+ $queue = $this->get_batch_queue();
+
+ // Pixel should be in the queue (without timestamp/nocache since that's added on send).
+ $this->assertCount( 1, $queue );
+ $this->assertEquals( $pixel, $queue[0] );
+ }
+
+ /**
+ * @testdox Should send pixels immediately when using record_pixel without batching.
+ */
+ public function test_record_pixel_sends_immediately() {
+ $pixel = 'https://pixel.wp.com/t.gif?_en=test_event&test=value';
+
+ // Clear intercepted requests.
+ $this->intercepted_requests = array();
+
+ // Record the pixel using immediate method.
+ WC_Tracks_Client::record_pixel( $pixel );
+
+ // Should be sent immediately, not queued.
+ $queue = $this->get_batch_queue();
+ $this->assertEmpty( $queue, 'Pixel should not be queued when using record_pixel().' );
+
+ // Should have been sent immediately via HTTP.
+ $this->assertCount( 1, $this->intercepted_requests, 'Pixel should be sent immediately.' );
+ $this->assertStringContainsString( '_en=test_event', $this->intercepted_requests[0]['url'] );
+ }
+
+ /**
+ * @testdox Should queue all pixels when recording multiple batched pixels.
+ */
+ public function test_record_multiple_pixels_batched_queues_all() {
+ // Ensure batching is supported.
+ if ( ! class_exists( 'WpOrg\Requests\Requests' ) && ! class_exists( 'Requests' ) ) {
+ $this->markTestSkipped( 'Requests library not available for batching.' );
+ }
+
+ $pixel1 = 'https://pixel.wp.com/t.gif?_en=event1';
+ $pixel2 = 'https://pixel.wp.com/t.gif?_en=event2';
+ $pixel3 = 'https://pixel.wp.com/t.gif?_en=event3';
+
+ WC_Tracks_Client::record_pixel_batched( $pixel1 );
+ WC_Tracks_Client::record_pixel_batched( $pixel2 );
+ WC_Tracks_Client::record_pixel_batched( $pixel3 );
+
+ $queue = $this->get_batch_queue();
+
+ $this->assertCount( 3, $queue );
+ $this->assertEquals( $pixel1, $queue[0] );
+ $this->assertEquals( $pixel2, $queue[1] );
+ $this->assertEquals( $pixel3, $queue[2] );
+ }
+
+ /**
+ * @testdox Should register shutdown hook when pixels are batched.
+ */
+ public function test_shutdown_hook_registered_when_pixel_batched() {
+ // Ensure batching is supported.
+ if ( ! class_exists( 'WpOrg\Requests\Requests' ) && ! class_exists( 'Requests' ) ) {
+ $this->markTestSkipped( 'Requests library not available for batching.' );
+ }
+
+ $pixel = 'https://pixel.wp.com/t.gif?_en=test_event';
+
+ // Record the pixel using batched method.
+ WC_Tracks_Client::record_pixel_batched( $pixel );
+
+ // Check if the shutdown hook is registered.
+ $this->assertNotFalse(
+ has_action( 'shutdown', array( 'WC_Tracks_Client', 'send_batched_pixels' ) ),
+ 'Shutdown hook should be registered after queuing a pixel.'
+ );
+ }
+
+ /**
+ * @testdox Should register shutdown hook only once when multiple pixels are batched.
+ */
+ public function test_shutdown_hook_registered_only_once() {
+ // Ensure batching is supported.
+ if ( ! class_exists( 'WpOrg\Requests\Requests' ) && ! class_exists( 'Requests' ) ) {
+ $this->markTestSkipped( 'Requests library not available for batching.' );
+ }
+
+ WC_Tracks_Client::record_pixel_batched( 'https://pixel.wp.com/t.gif?_en=event1' );
+ WC_Tracks_Client::record_pixel_batched( 'https://pixel.wp.com/t.gif?_en=event2' );
+
+ // Get the priority value (should be 20).
+ $priority = has_action( 'shutdown', array( 'WC_Tracks_Client', 'send_batched_pixels' ) );
+
+ $this->assertEquals( 20, $priority, 'Shutdown hook should be registered with priority 20.' );
+
+ // Count how many times the hook is registered.
+ global $wp_filter;
+ $hook_count = 0;
+ if ( isset( $wp_filter['shutdown'] ) && isset( $wp_filter['shutdown'][20] ) ) {
+ foreach ( $wp_filter['shutdown'][20] as $hook ) {
+ if ( is_array( $hook['function'] ) && 'WC_Tracks_Client' === $hook['function'][0] && 'send_batched_pixels' === $hook['function'][1] ) {
+ ++$hook_count;
+ }
+ }
+ }
+
+ $this->assertEquals( 1, $hook_count, 'Shutdown hook should only be registered once.' );
+ }
+
+ /**
+ * @testdox Should clear queue after sending batched pixels.
+ */
+ public function test_send_batched_pixels_clears_queue() {
+ // Ensure batching is supported.
+ if ( ! class_exists( 'WpOrg\Requests\Requests' ) && ! class_exists( 'Requests' ) ) {
+ $this->markTestSkipped( 'Requests library not available for batching.' );
+ }
+
+ $pixel = 'https://pixel.wp.com/t.gif?_en=test_event';
+
+ // Queue a pixel.
+ WC_Tracks_Client::record_pixel_batched( $pixel );
+
+ // Verify pixel was queued.
+ $queue = $this->get_batch_queue();
+ $this->assertCount( 1, $queue );
+
+ // Send the batched pixels.
+ WC_Tracks_Client::send_batched_pixels();
+
+ // Queue should be empty after sending.
+ $queue = $this->get_batch_queue();
+ $this->assertEmpty( $queue, 'Queue should be empty after sending batched pixels.' );
+ }
+
+ /**
+ * @testdox Should do nothing when sending batched pixels with empty queue.
+ */
+ public function test_send_batched_pixels_does_nothing_when_queue_empty() {
+ $this->reset_batch_state();
+
+ // Call send_batched_pixels with empty queue.
+ WC_Tracks_Client::send_batched_pixels();
+
+ // Should not cause any errors and queue should remain empty.
+ $queue = $this->get_batch_queue();
+ $this->assertEmpty( $queue );
+ }
+
+ /**
+ * @testdox Should disable batching when wc_tracks_use_batch_requests filter returns false.
+ */
+ public function test_filter_can_disable_batching() {
+ // Ensure batching is supported.
+ if ( ! class_exists( 'WpOrg\Requests\Requests' ) && ! class_exists( 'Requests' ) ) {
+ $this->markTestSkipped( 'Requests library not available for batching.' );
+ }
+
+ // Add filter to disable batching.
+ add_filter( 'wc_tracks_use_batch_requests', '__return_false' );
+
+ $pixel = 'https://pixel.wp.com/t.gif?_en=test_event';
+
+ // Record the pixel using batched method (but filter will disable batching).
+ WC_Tracks_Client::record_pixel_batched( $pixel );
+
+ // Queue should be empty since batching is disabled.
+ $queue = $this->get_batch_queue();
+ $this->assertEmpty( $queue, 'Queue should be empty when batching is disabled via filter.' );
+
+ // Clean up filter.
+ remove_filter( 'wc_tracks_use_batch_requests', '__return_false' );
+ }
+
+ /**
+ * @testdox Should enable batching when wc_tracks_use_batch_requests filter returns true.
+ */
+ public function test_filter_can_enable_batching() {
+ // Ensure batching is supported.
+ if ( ! class_exists( 'WpOrg\Requests\Requests' ) && ! class_exists( 'Requests' ) ) {
+ $this->markTestSkipped( 'Requests library not available for batching.' );
+ }
+
+ // Add filter to explicitly enable batching.
+ add_filter( 'wc_tracks_use_batch_requests', '__return_true' );
+
+ $pixel = 'https://pixel.wp.com/t.gif?_en=test_event';
+
+ // Record the pixel using batched method.
+ WC_Tracks_Client::record_pixel_batched( $pixel );
+
+ // Queue should have the pixel.
+ $queue = $this->get_batch_queue();
+ $this->assertCount( 1, $queue );
+
+ // Clean up filter.
+ remove_filter( 'wc_tracks_use_batch_requests', '__return_true' );
+ }
+
+ /**
+ * @testdox Should generate timestamp in milliseconds.
+ */
+ public function test_build_timestamp() {
+ $timestamp = WC_Tracks_Client::build_timestamp();
+
+ // Should be a numeric string.
+ $this->assertIsString( $timestamp );
+ $this->assertMatchesRegularExpression( '/^\d+$/', $timestamp );
+
+ // Should be approximately current time in milliseconds (13 digits).
+ $this->assertGreaterThanOrEqual( 13, strlen( $timestamp ) );
+ }
+
+ /**
+ * @testdox Should add request timestamp and nocache parameters to pixel URL.
+ */
+ public function test_add_request_timestamp_and_nocache() {
+ $pixel = 'https://pixel.wp.com/t.gif?_en=test_event';
+
+ $pixel_with_params = WC_Tracks_Client::add_request_timestamp_and_nocache( $pixel );
+
+ // Should contain _rt parameter.
+ $this->assertStringContainsString( '&_rt=', $pixel_with_params );
+
+ // Should contain _=_ terminator.
+ $this->assertStringContainsString( '&_=_', $pixel_with_params );
+
+ // Parse URL to verify parameters.
+ $parsed_url = wp_parse_url( $pixel_with_params );
+ parse_str( $parsed_url['query'], $query_params );
+
+ $this->assertArrayHasKey( '_en', $query_params );
+ $this->assertEquals( 'test_event', $query_params['_en'] );
+ $this->assertArrayHasKey( '_rt', $query_params );
+ $this->assertArrayHasKey( '_', $query_params );
+ $this->assertEquals( '_', $query_params['_'] );
+ }
+
+ /**
+ * @testdox Should queue event when using record_event_batched.
+ */
+ public function test_record_event_batched_queues_event() {
+ // Ensure batching is supported.
+ if ( ! class_exists( 'WpOrg\Requests\Requests' ) && ! class_exists( 'Requests' ) ) {
+ $this->markTestSkipped( 'Requests library not available for batching.' );
+ }
+
+ $event_props = array(
+ '_en' => 'test_event',
+ '_ts' => WC_Tracks_Client::build_timestamp(),
+ 'test_prop' => 'test_value',
+ );
+
+ $event = new \WC_Tracks_Event( $event_props );
+
+ // Record the event using batched method.
+ WC_Tracks_Client::record_event_batched( $event );
+
+ // Queue should have one pixel.
+ $queue = $this->get_batch_queue();
+ $this->assertCount( 1, $queue );
+
+ // The queued pixel should contain the event name.
+ $this->assertStringContainsString( '_en=test_event', $queue[0] );
+ $this->assertStringContainsString( 'test_prop=test_value', $queue[0] );
+ }
+
+ /**
+ * @testdox Should queue multiple events when batching.
+ */
+ public function test_batch_multiple_events() {
+ // Ensure batching is supported.
+ if ( ! class_exists( 'WpOrg\Requests\Requests' ) && ! class_exists( 'Requests' ) ) {
+ $this->markTestSkipped( 'Requests library not available for batching.' );
+ }
+
+ // Create multiple events.
+ for ( $i = 1; $i <= 5; $i++ ) {
+ $event_props = array(
+ '_en' => "test_event_$i",
+ '_ts' => WC_Tracks_Client::build_timestamp(),
+ 'number' => $i,
+ );
+
+ $event = new \WC_Tracks_Event( $event_props );
+ WC_Tracks_Client::record_event_batched( $event );
+ }
+
+ // Queue should have 5 pixels.
+ $queue = $this->get_batch_queue();
+ $this->assertCount( 5, $queue );
+
+ // Verify each event is in the queue.
+ for ( $i = 1; $i <= 5; $i++ ) {
+ $this->assertStringContainsString( "_en=test_event_$i", $queue[ $i - 1 ] );
+ }
+ }
+
+ /**
+ * @testdox Should handle empty array when calling send_with_requests_multiple.
+ */
+ public function test_send_with_requests_multiple_handles_empty_array() {
+ $reflection = new ReflectionClass( 'WC_Tracks_Client' );
+ $method = $reflection->getMethod( 'send_with_requests_multiple' );
+ $method->setAccessible( true );
+
+ // Call with empty array - should not cause any issues.
+ $method->invoke( null, array() );
+
+ // Explicit assertion that no exception was thrown.
+ $this->addToAssertionCount( 1 );
+ }
+
+ /**
+ * @testdox Should clear queue after sending pixels.
+ */
+ public function test_queue_cleared_after_sending() {
+ // Ensure batching is supported.
+ if ( ! class_exists( 'WpOrg\Requests\Requests' ) && ! class_exists( 'Requests' ) ) {
+ $this->markTestSkipped( 'Requests library not available for batching.' );
+ }
+
+ // Queue multiple pixels.
+ WC_Tracks_Client::record_pixel_batched( 'https://pixel.wp.com/t.gif?_en=event1' );
+ WC_Tracks_Client::record_pixel_batched( 'https://pixel.wp.com/t.gif?_en=event2' );
+ WC_Tracks_Client::record_pixel_batched( 'https://pixel.wp.com/t.gif?_en=event3' );
+
+ $queue = $this->get_batch_queue();
+ $this->assertCount( 3, $queue );
+
+ // Send batched pixels.
+ WC_Tracks_Client::send_batched_pixels();
+
+ // Queue should be empty.
+ $queue = $this->get_batch_queue();
+ $this->assertEmpty( $queue );
+ }
+
+ /**
+ * @testdox Should batch multiple pixels together and clear queue after sending.
+ */
+ public function test_multiple_pixels_batched_together() {
+ // Ensure batching is supported.
+ if ( ! class_exists( 'WpOrg\Requests\Requests' ) && ! class_exists( 'Requests' ) ) {
+ $this->markTestSkipped( 'Requests library not available for batching.' );
+ }
+
+ // Record multiple pixels.
+ WC_Tracks_Client::record_pixel_batched( 'https://pixel.wp.com/t.gif?_en=event1&prop=value1' );
+ WC_Tracks_Client::record_pixel_batched( 'https://pixel.wp.com/t.gif?_en=event2&prop=value2' );
+
+ // Verify both pixels are in the queue before sending.
+ $queue = $this->get_batch_queue();
+ $this->assertCount( 2, $queue, 'Should have 2 pixels queued.' );
+
+ // Send batched pixels.
+ WC_Tracks_Client::send_batched_pixels();
+
+ // Verify queue is cleared after sending.
+ $queue = $this->get_batch_queue();
+ $this->assertEmpty( $queue, 'Queue should be empty after sending.' );
+ }
+
+ /**
+ * @testdox Should intercept fallback requests when batching is disabled.
+ */
+ public function test_fallback_requests_are_intercepted() {
+ // Disable batching via filter.
+ add_filter( 'wc_tracks_use_batch_requests', '__return_false' );
+
+ // Clear intercepted requests.
+ $this->intercepted_requests = array();
+
+ // Record a pixel (should use fallback method).
+ WC_Tracks_Client::record_pixel_batched( 'https://pixel.wp.com/t.gif?_en=fallback_event' );
+
+ // Verify that HTTP request was intercepted.
+ $this->assertCount( 1, $this->intercepted_requests, 'Should have intercepted 1 HTTP request.' );
+ $this->assertStringContainsString( '_en=fallback_event', $this->intercepted_requests[0]['url'] );
+
+ // Clean up filter.
+ remove_filter( 'wc_tracks_use_batch_requests', '__return_false' );
+ }
+}