Commit 2762e009350 for woocommerce
commit 2762e0093509f92e501f0aec31460a06fbb33bfb
Author: Chi-Hsuan Huang <chihsuan.tw@gmail.com>
Date: Wed Sep 23 10:45:19 2026 +0800
Stop URL-encoding array event properties twice in the analytics pixel (#68907)
* Fix array event properties being URL-encoded twice on the pixel URL
* Add changelog entry for array property double encoding fix
diff --git a/packages/php/woocommerce-analytics/changelog/fix-wooa7s-2110-array-prop-encoding b/packages/php/woocommerce-analytics/changelog/fix-wooa7s-2110-array-prop-encoding
new file mode 100644
index 00000000000..d8f741cdddc
--- /dev/null
+++ b/packages/php/woocommerce-analytics/changelog/fix-wooa7s-2110-array-prop-encoding
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Stop URL-encoding array event properties twice so Tracks stores the plain comma-joined value.
diff --git a/packages/php/woocommerce-analytics/src/class-wc-analytics-tracking.php b/packages/php/woocommerce-analytics/src/class-wc-analytics-tracking.php
index 390dc29bb6c..48a303b0253 100644
--- a/packages/php/woocommerce-analytics/src/class-wc-analytics-tracking.php
+++ b/packages/php/woocommerce-analytics/src/class-wc-analytics-tracking.php
@@ -700,8 +700,9 @@ class WC_Analytics_Tracking {
return '';
}
+ // Not URL-encoded here: http_build_query() in Pixel_Builder encodes the whole URL, so encoding twice stores `%2F` in Tracks.
if ( array_keys( $value ) === range( 0, count( $value ) - 1 ) ) {
- return rawurlencode( implode( ',', $value ) );
+ return implode( ',', $value );
}
return wp_json_encode( $value, JSON_UNESCAPED_SLASHES );
diff --git a/packages/php/woocommerce-analytics/tests/php/WC_Analytics_Tracking_Array_Props_Test.php b/packages/php/woocommerce-analytics/tests/php/WC_Analytics_Tracking_Array_Props_Test.php
new file mode 100644
index 00000000000..1ec3fbbdc3e
--- /dev/null
+++ b/packages/php/woocommerce-analytics/tests/php/WC_Analytics_Tracking_Array_Props_Test.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * Tests for how array-valued event properties reach the pixel URL.
+ *
+ * @package automattic/woocommerce-analytics
+ */
+
+namespace Automattic\Woocommerce_Analytics;
+
+use WorDBless\BaseTestCase;
+
+/**
+ * Array props must arrive at Tracks as one comma-joined value under their own key.
+ *
+ * Two regressions are guarded here: the bracketed keys (`prop[0]`) that
+ * `http_build_query()` produces for a raw array, which Tracks rejects, and the
+ * double URL-encoding that left `%2F` in the stored value once decoded.
+ */
+class WC_Analytics_Tracking_Array_Props_Test extends BaseTestCase {
+
+ /**
+ * Build the pixel URL's raw query string for the given event properties.
+ *
+ * @param array $event_properties Properties handed to `get_properties()`.
+ * @return string The query string exactly as it leaves `http_build_query()`.
+ */
+ private function raw_pixel_query( array $event_properties ): string {
+ $props = WC_Analytics_Tracking::get_properties( 'woocommerceanalytics_add_to_cart', $event_properties );
+ $url = Pixel_Builder::build_tracks_url( $props );
+
+ $this->assertIsString( $url, 'The pixel URL must build for array props.' );
+
+ return (string) wp_parse_url( $url, PHP_URL_QUERY );
+ }
+
+ /**
+ * Decode the pixel URL once, the way the Tracks endpoint does.
+ *
+ * @param array $event_properties Properties handed to `get_properties()`.
+ * @return array Query parameters as Tracks would read them.
+ */
+ private function decoded_pixel_query( array $event_properties ): array {
+ parse_str( $this->raw_pixel_query( $event_properties ), $query );
+
+ return $query;
+ }
+
+ /**
+ * Indexed arrays reach Tracks as a plain comma-joined string.
+ */
+ public function test_indexed_array_prop_decodes_once_to_the_joined_value(): void {
+ $query = $this->decoded_pixel_query(
+ array(
+ 'additional_blocks_on_cart_page' => array( 'woocommerce/cart-cross-sells-block', 'core/paragraph' ),
+ )
+ );
+
+ $this->assertSame(
+ 'woocommerce/cart-cross-sells-block,core/paragraph',
+ $query['additional_blocks_on_cart_page'],
+ 'A single decode must yield the block names, not a second layer of %2F and %2C.'
+ );
+ }
+
+ /**
+ * No key in the pixel URL carries the brackets that make Tracks reject the event.
+ */
+ public function test_array_props_never_produce_bracketed_keys(): void {
+ $event_properties = array(
+ 'additional_blocks_on_cart_page' => array( 'core/paragraph', 'core/group' ),
+ 'additional_blocks_on_checkout_page' => array(),
+ );
+
+ // Checked on the raw query string: parse_str() would fold `prop[0]` back into a nested array and hide the brackets.
+ $raw = $this->raw_pixel_query( $event_properties );
+ $this->assertDoesNotMatchRegularExpression( '/(^|&)[^=&]*(%5B|%5D|\[|\])[^=&]*=/i', $raw );
+
+ $query = $this->decoded_pixel_query( $event_properties );
+ $this->assertSame( '', $query['additional_blocks_on_checkout_page'], 'An empty array is an empty value under the original key.' );
+ }
+
+ /**
+ * Associative arrays keep their JSON form, still decoding once to valid JSON.
+ */
+ public function test_associative_array_prop_decodes_once_to_json(): void {
+ $query = $this->decoded_pixel_query( array( 'meta' => array( 'a' => 1, 'b' => 'x/y' ) ) );
+
+ $this->assertSame( array( 'a' => 1, 'b' => 'x/y' ), json_decode( $query['meta'], true ) );
+ }
+}
diff --git a/packages/php/woocommerce-analytics/tests/php/WC_Analytics_Tracking_Reserved_Props_Test.php b/packages/php/woocommerce-analytics/tests/php/WC_Analytics_Tracking_Reserved_Props_Test.php
index 7a816c90166..09762b2d232 100644
--- a/packages/php/woocommerce-analytics/tests/php/WC_Analytics_Tracking_Reserved_Props_Test.php
+++ b/packages/php/woocommerce-analytics/tests/php/WC_Analytics_Tracking_Reserved_Props_Test.php
@@ -517,7 +517,7 @@ class WC_Analytics_Tracking_Reserved_Props_Test extends BaseTestCase {
$props = WC_Analytics_Tracking::get_properties( 'woocommerceanalytics_product_view', $sanitized, true );
$this->assertSame(
- rawurlencode( implode( ',', array_fill( 0, WC_Analytics_Tracking::MAX_CLIENT_ARRAY_MEMBERS, 'a' ) ) ),
+ implode( ',', array_fill( 0, WC_Analytics_Tracking::MAX_CLIENT_ARRAY_MEMBERS, 'a' ) ),
$props['pc']
);
}