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: