Commit 2c1b9fb7a83 for woocommerce

commit 2c1b9fb7a83d0b005a989fb0ed1cd913afe0b988
Author: louwie17 <lourensschep@gmail.com>
Date:   Fri Oct 23 10:53:31 2026 +0200

    Only store the order attribution cookie fields WooCommerce reads (#70098)

    * Only store the order attribution cookie fields WooCommerce reads

    Bump sourcebuster.js to 1.3.0 and pass the tracking field paths to sbjs.init as fields. Add the wc_order_attribution_cookie_fields filter so plugins can store more fields.

    * Add changelog entry for smaller order attribution cookies

    * Skip empty order attribution values and invalid cookie field lists

    Sourcebuster.js 1.3.0 empties a field when it doesn't fit the cookie size limit, which stored empty order meta rows. A cookie fields filter result with no valid strings now falls back to the default list instead of storing every field.

diff --git a/plugins/woocommerce/changelog/wooplug-8050-sourcebuster-fields b/plugins/woocommerce/changelog/wooplug-8050-sourcebuster-fields
new file mode 100644
index 00000000000..2825941a99f
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooplug-8050-sourcebuster-fields
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Keep order attribution cookies small to prevent 400 errors. Session entry is now a page path, referrers drop their query, and very long UTM values are shortened.
diff --git a/plugins/woocommerce/client/legacy/js/frontend/order-attribution.js b/plugins/woocommerce/client/legacy/js/frontend/order-attribution.js
index d307803d559..c3ac249b410 100644
--- a/plugins/woocommerce/client/legacy/js/frontend/order-attribution.js
+++ b/plugins/woocommerce/client/legacy/js/frontend/order-attribution.js
@@ -112,6 +112,8 @@
 				session_length: Number( params.session ),
 				base64: Boolean( params.base64 ),
 				timezone_offset: '0', // utc
+				// Only store the fields WooCommerce reads. An empty list stores every field.
+				fields: params.cookieFields,
 			} );
 		}
 		const values = wc_order_attribution.getAttributionData();
diff --git a/plugins/woocommerce/client/legacy/js/frontend/test/order-attribution.js b/plugins/woocommerce/client/legacy/js/frontend/test/order-attribution.js
index 3c4c4834b66..ce1fc1f09dc 100644
--- a/plugins/woocommerce/client/legacy/js/frontend/test/order-attribution.js
+++ b/plugins/woocommerce/client/legacy/js/frontend/test/order-attribution.js
@@ -94,3 +94,36 @@ describe( 'Order attribution input deduplication', () => {
 		expect( document.getElementById( 'second' ) ).toBeNull();
 	} );
 } );
+
+describe( 'Order attribution sourcebuster.js init', () => {
+	afterEach( () => {
+		delete window.wc_order_attribution;
+		delete window.sbjs;
+		jest.restoreAllMocks();
+	} );
+
+	test( 'passes the cookie fields to sbjs.init', () => {
+		window.sbjs = { init: jest.fn(), get: {} };
+		window.wc_order_attribution = {
+			fields: {
+				source_type: 'current.typ',
+			},
+			params: {
+				allowTracking: false,
+				prefix: 'wc_order_attribution_',
+				cookieFields: [ 'current.typ', 'first.src' ],
+			},
+		};
+		// The first suite already defined the custom element.
+		jest.spyOn( window.customElements, 'define' ).mockImplementation( () => {} );
+		jest.isolateModules( () => require( '../order-attribution' ) );
+
+		window.wc_order_attribution.setOrderTracking( true );
+
+		expect( window.sbjs.init ).toHaveBeenCalledWith(
+			expect.objectContaining( {
+				fields: [ 'current.typ', 'first.src' ],
+			} )
+		);
+	} );
+} );
diff --git a/plugins/woocommerce/client/legacy/package.json b/plugins/woocommerce/client/legacy/package.json
index cfea8827ceb..338b2d4a8f0 100644
--- a/plugins/woocommerce/client/legacy/package.json
+++ b/plugins/woocommerce/client/legacy/package.json
@@ -45,7 +45,7 @@
 		"stylelint": "catalog:tooling"
 	},
 	"dependencies": {
-		"sourcebuster": "github:woocommerce/sourcebuster-js#d7f4616d5a17e17db925ca1842457f309379d861"
+		"sourcebuster": "github:woocommerce/sourcebuster-js#b28980f98d8ed3f35ac84974e603a6c3bd103752"
 	},
 	"config": {
 		"ci": {
diff --git a/plugins/woocommerce/src/Internal/Orders/OrderAttributionController.php b/plugins/woocommerce/src/Internal/Orders/OrderAttributionController.php
index 1e7338a5d3e..c6328bf940b 100644
--- a/plugins/woocommerce/src/Internal/Orders/OrderAttributionController.php
+++ b/plugins/woocommerce/src/Internal/Orders/OrderAttributionController.php
@@ -326,6 +326,26 @@ class OrderAttributionController implements RegisterHooksInterface {
 		 */
 		$allow_tracking = wc_bool_to_string( apply_filters( 'wc_order_attribution_allow_tracking', true ) );

+		$default_cookie_fields = array_values( $this->fields );
+
+		/**
+		 * Filter the sourcebuster.js fields that are stored in the visitor's cookies.
+		 *
+		 * Defaults to the paths in `wc_order_attribution_tracking_fields`. Add a path such as `first.src`
+		 * to keep a cookie WooCommerce doesn't read, or return an empty array to store every field.
+		 *
+		 * @since 11.4.0
+		 *
+		 * @param string[] $cookie_fields Sourcebuster.js field paths, such as `current.typ`.
+		 */
+		$filtered_cookie_fields = apply_filters( 'wc_order_attribution_cookie_fields', $default_cookie_fields );
+		$cookie_fields          = is_array( $filtered_cookie_fields ) ? array_values( array_unique( array_filter( $filtered_cookie_fields, 'is_string' ) ) ) : array();
+
+		// An empty list stores every field, so only pass one on when the filter returned it.
+		if ( ! $cookie_fields && array() !== $filtered_cookie_fields ) {
+			$cookie_fields = $default_cookie_fields;
+		}
+
 		// Create Order Attribution JS namespace with parameters.
 		$namespace = array(
 			'params' => array(
@@ -335,6 +355,7 @@ class OrderAttributionController implements RegisterHooksInterface {
 				'ajaxurl'       => admin_url( 'admin-ajax.php' ),
 				'prefix'        => $this->field_prefix,
 				'allowTracking' => 'yes' === $allow_tracking,
+				'cookieFields'  => $cookie_fields,
 			),
 			'fields' => $this->fields,
 		);
diff --git a/plugins/woocommerce/src/Internal/Traits/OrderAttributionMeta.php b/plugins/woocommerce/src/Internal/Traits/OrderAttributionMeta.php
index a381afa3cf2..810fa4c6307 100644
--- a/plugins/woocommerce/src/Internal/Traits/OrderAttributionMeta.php
+++ b/plugins/woocommerce/src/Internal/Traits/OrderAttributionMeta.php
@@ -248,7 +248,8 @@ trait OrderAttributionMeta {
 		// Look through each field in given data.
 		foreach ( $this->field_names as $field_name ) {
 			$value = sanitize_text_field( wp_unslash( $raw_values[ $field_name ] ) );
-			if ( '(none)' === $value ) {
+			// Sourcebuster.js empties a field when it doesn't fit the cookie size limit.
+			if ( '' === $value || '(none)' === $value ) {
 				continue;
 			}

diff --git a/plugins/woocommerce/tests/php/src/Internal/Orders/OrderAttributionControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Orders/OrderAttributionControllerTest.php
index bef88403d82..df9a63864c9 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Orders/OrderAttributionControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Orders/OrderAttributionControllerTest.php
@@ -340,4 +340,157 @@ class OrderAttributionControllerTest extends WP_UnitTestCase {

 		remove_filter( 'wc_order_attribution_allow_multiple_elements', '__return_false' );
 	}
+
+	/**
+	 * @testdox Should pass the tracking field paths to sourcebuster.js as cookie fields by default.
+	 */
+	public function test_cookie_fields_default_to_tracking_field_paths(): void {
+		$cookie_fields = $this->get_localized_script_data()['params']['cookieFields'];
+
+		$this->assertSame(
+			array(
+				'current.typ',
+				'current_add.rf',
+				'current.cmp',
+				'current.src',
+				'current.mdm',
+				'current.cnt',
+				'current.id',
+				'current.trm',
+				'current.plt',
+				'current.fmt',
+				'current.tct',
+				'current_add.ep',
+				'current_add.fd',
+				'session.pgs',
+				'udata.vst',
+				'udata.uag',
+			),
+			$cookie_fields,
+			'Cookie fields should match the paths of the default tracking fields.'
+		);
+	}
+
+	/**
+	 * @testdox Should derive the cookie fields from the filtered tracking fields.
+	 */
+	public function test_cookie_fields_follow_filtered_tracking_fields(): void {
+		$filter = function ( $fields ) {
+			unset( $fields['utm_content'] );
+			$fields['first_source'] = 'first.src';
+			return $fields;
+		};
+		add_filter( 'wc_order_attribution_tracking_fields', $filter );
+		$set_fields = new \ReflectionMethod( OrderAttributionController::class, 'set_fields_and_prefix' );
+		$set_fields->setAccessible( true );
+		$set_fields->invoke( $this->attribution_class );
+
+		$data = $this->get_localized_script_data();
+
+		$this->assertNotContains( 'current.cnt', $data['params']['cookieFields'], 'A removed tracking field should not be a cookie field.' );
+		$this->assertContains( 'first.src', $data['params']['cookieFields'], 'An added tracking field should be a cookie field.' );
+		$this->assertSame( 'first.src', $data['fields']['first_source'], 'The tracking fields should still be passed to the script.' );
+	}
+
+	/**
+	 * Data provider for test_cookie_fields_filter.
+	 *
+	 * @return array[]
+	 */
+	public function provider_cookie_fields_filter(): array {
+		return array(
+			'added first.* paths'        => array( array( 'current.typ', 'first.typ', 'first.src', 'first.mdm' ), array( 'current.typ', 'first.typ', 'first.src', 'first.mdm' ) ),
+			'empty array'                => array( array(), array() ),
+			'duplicates and non-strings' => array( array( 'current.typ', 42, 'current.typ', null, 'session.pgs' ), array( 'current.typ', 'session.pgs' ) ),
+		);
+	}
+
+	/**
+	 * @testdox Should fall back to the default cookie fields when the filter returns only non-strings.
+	 */
+	public function test_cookie_fields_filter_ignores_non_string_list(): void {
+		$default = $this->get_localized_script_data()['params']['cookieFields'];
+		add_filter(
+			'wc_order_attribution_cookie_fields',
+			function () {
+				return array( 42, null );
+			}
+		);
+
+		$this->assertSame( $default, $this->get_localized_script_data()['params']['cookieFields'] );
+	}
+
+	/**
+	 * @testdox Should not store empty or (none) values submitted from the cookies.
+	 */
+	public function test_source_values_skip_empty_and_none_values(): void {
+		$raw_values = array_fill_keys( $this->attribution_class->get_field_names(), '' );
+
+		$raw_values['source_type']  = 'utm';
+		$raw_values['utm_source']   = 'newsletter';
+		$raw_values['utm_campaign'] = '(none)';
+
+		$method = new \ReflectionMethod( OrderAttributionController::class, 'get_source_values' );
+		$method->setAccessible( true );
+
+		$this->assertSame(
+			array(
+				'source_type' => 'utm',
+				'utm_source'  => 'newsletter',
+			),
+			$method->invoke( $this->attribution_class, $raw_values )
+		);
+	}
+
+	/**
+	 * @testdox Should pass the filtered cookie fields to sourcebuster.js.
+	 *
+	 * @dataProvider provider_cookie_fields_filter
+	 *
+	 * @param array $filtered The value returned by the filter.
+	 * @param array $expected The expected cookie fields.
+	 */
+	public function test_cookie_fields_filter( array $filtered, array $expected ): void {
+		add_filter(
+			'wc_order_attribution_cookie_fields',
+			function () use ( $filtered ) {
+				return $filtered;
+			}
+		);
+
+		$this->assertSame( $expected, $this->get_localized_script_data()['params']['cookieFields'] );
+	}
+
+	/**
+	 * @testdox Should fall back to the default cookie fields when the filter does not return an array.
+	 */
+	public function test_cookie_fields_filter_ignores_non_array(): void {
+		$default = $this->get_localized_script_data()['params']['cookieFields'];
+		add_filter( 'wc_order_attribution_cookie_fields', '__return_false' );
+
+		$this->assertSame( $default, $this->get_localized_script_data()['params']['cookieFields'] );
+	}
+
+	/**
+	 * Enqueue the frontend scripts and return the data localized for `wc-order-attribution`.
+	 *
+	 * @return array
+	 */
+	private function get_localized_script_data(): array {
+		wp_deregister_script( 'wc-order-attribution' );
+		wp_deregister_script( 'sourcebuster-js' );
+
+		$method = new \ReflectionMethod( OrderAttributionController::class, 'enqueue_scripts_and_styles' );
+		$method->setAccessible( true );
+		$method->invoke( $this->attribution_class );
+
+		$data = wp_scripts()->get_data( 'wc-order-attribution', 'data' );
+		wp_deregister_script( 'wc-order-attribution' );
+		wp_deregister_script( 'sourcebuster-js' );
+
+		$this->assertIsString( $data, 'The script should have localized data.' );
+		$this->assertSame( 1, preg_match( '/var wc_order_attribution = (.*);/', $data, $matches ), 'The script should localize wc_order_attribution.' );
+
+		return json_decode( $matches[1], true );
+	}
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d00773760d7..47882fd4707 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3840,8 +3840,8 @@ importers:
   plugins/woocommerce/client/legacy:
     dependencies:
       sourcebuster:
-        specifier: github:woocommerce/sourcebuster-js#d7f4616d5a17e17db925ca1842457f309379d861
-        version: https://codeload.github.com/woocommerce/sourcebuster-js/tar.gz/d7f4616d5a17e17db925ca1842457f309379d861
+        specifier: github:woocommerce/sourcebuster-js#b28980f98d8ed3f35ac84974e603a6c3bd103752
+        version: https://codeload.github.com/woocommerce/sourcebuster-js/tar.gz/b28980f98d8ed3f35ac84974e603a6c3bd103752
     devDependencies:
       '@types/node':
         specifier: ^24.1.0
@@ -18309,9 +18309,9 @@ packages:
     resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
     engines: {node: '>= 12'}

-  sourcebuster@https://codeload.github.com/woocommerce/sourcebuster-js/tar.gz/d7f4616d5a17e17db925ca1842457f309379d861:
-    resolution: {tarball: https://codeload.github.com/woocommerce/sourcebuster-js/tar.gz/d7f4616d5a17e17db925ca1842457f309379d861}
-    version: 1.1.2
+  sourcebuster@https://codeload.github.com/woocommerce/sourcebuster-js/tar.gz/b28980f98d8ed3f35ac84974e603a6c3bd103752:
+    resolution: {tarball: https://codeload.github.com/woocommerce/sourcebuster-js/tar.gz/b28980f98d8ed3f35ac84974e603a6c3bd103752}
+    version: 1.3.0

   spawnd@10.1.4:
     resolution: {integrity: sha512-drqHc0mKJmtMsiGMOCwzlc5eZ0RPtRvT7tQAluW2A0qUc0G7TQ8KLcn3E6K5qzkLkH2UkS3nYQiVGULvvsD9dw==}
@@ -40225,7 +40225,7 @@ snapshots:

   source-map@0.7.6: {}

-  sourcebuster@https://codeload.github.com/woocommerce/sourcebuster-js/tar.gz/d7f4616d5a17e17db925ca1842457f309379d861: {}
+  sourcebuster@https://codeload.github.com/woocommerce/sourcebuster-js/tar.gz/b28980f98d8ed3f35ac84974e603a6c3bd103752: {}

   spawnd@10.1.4:
     dependencies: