Make OSA fields extendable, simplify naming (#41690)

- Use `referrer` & `source_type` field names consistently
   Remove the need to translate it back and forth.
- Make fields actually extendable using `wc_order_attribution_tracking_fields`.
   Propagate the field configuration to the client side as well.
   Disambiguate fields in variables and functions.

Co-authored-by: Justin Palmer <228780+layoutd@users.noreply.github.com>
This commit is contained in:
Tomek Wytrębowicz 2024-01-08 16:17:04 +01:00 committed by GitHub
parent 245fea22ac
commit fa62e63037
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 96 additions and 117 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Make OSA fields extendable

View File

@ -8,30 +8,6 @@
const propertyAccessor = ( obj, path ) => path.split( '.' ).reduce( ( acc, part ) => acc && acc[ part ], obj );
const returnNull = () => null;
/**
* Map of order attribution field names to sbjs.get property accessors.
*/
wc_order_attribution.fields = {
// main fields.
type: 'current.typ',
url: 'current_add.rf',
// utm fields.
utm_campaign: 'current.cmp',
utm_source: 'current.src',
utm_medium: 'current.mdm',
utm_content: 'current.cnt',
utm_id: 'current.id',
utm_term: 'current.trm',
// additional fields.
session_entry: 'current_add.ep',
session_start_time: 'current_add.fd',
session_pages: 'session.pgs',
session_count: 'udata.vst',
user_agent: 'udata.uag',
};
/**
* Get the order attribution data.
*

View File

@ -120,8 +120,8 @@ class OrderAttributionBlocksController implements RegisterHooksInterface {
*/
private function get_schema_callback() {
return function() {
$schema = array();
$fields = $this->order_attribution_controller->get_fields();
$schema = array();
$field_names = $this->order_attribution_controller->get_field_names();
$validate_callback = function( $value ) {
if ( ! is_string( $value ) && null !== $value ) {
@ -142,12 +142,12 @@ class OrderAttributionBlocksController implements RegisterHooksInterface {
return sanitize_text_field( $value );
};
foreach ( $fields as $field ) {
$schema[ $field ] = array(
foreach ( $field_names as $field_name ) {
$schema[ $field_name ] = array(
'description' => sprintf(
/* translators: %s is the field name */
__( 'Order attribution field: %s', 'woocommerce' ),
esc_html( $field )
esc_html( $field_name )
),
'type' => array( 'string', 'null' ),
'context' => array(),

View File

@ -27,7 +27,7 @@ class OrderAttributionController implements RegisterHooksInterface {
use ScriptDebug;
use OrderAttributionMeta {
get_prefixed_field as public;
get_prefixed_field_name as public;
}
/**
@ -111,13 +111,13 @@ class OrderAttributionController implements RegisterHooksInterface {
}
);
// Include our hidden fields on order notes and registration form.
$source_form_fields = function() {
$this->source_form_fields();
// Include our hidden `<input>` elements on order notes and registration form.
$source_form_elements = function() {
$this->source_form_elements();
};
add_action( 'woocommerce_after_order_notes', $source_form_fields );
add_action( 'woocommerce_register_form', $source_form_fields );
add_action( 'woocommerce_after_order_notes', $source_form_elements );
add_action( 'woocommerce_register_form', $source_form_elements );
// Update order based on submitted fields.
add_action(
@ -125,7 +125,7 @@ class OrderAttributionController implements RegisterHooksInterface {
function( $order ) {
// Nonce check is handled by WooCommerce before woocommerce_checkout_order_created hook.
// phpcs:ignore WordPress.Security.NonceVerification
$params = $this->get_unprefixed_fields( $_POST );
$params = $this->get_unprefixed_field_values( $_POST );
/**
* Run an action to save order attribution data.
*
@ -188,18 +188,18 @@ class OrderAttributionController implements RegisterHooksInterface {
*/
private function maybe_set_admin_source( WC_Order $order ) {
if ( function_exists( 'is_admin' ) && is_admin() ) {
$order->add_meta_data( $this->get_meta_prefixed_field( 'type' ), 'admin' );
$order->add_meta_data( $this->get_meta_prefixed_field_name( 'source_type' ), 'admin' );
$order->save();
}
}
/**
* Get all of the fields.
* Get all of the field names.
*
* @return array
*/
public function get_fields(): array {
return $this->fields;
public function get_field_names(): array {
return $this->field_names;
}
/**
@ -271,6 +271,7 @@ class OrderAttributionController implements RegisterHooksInterface {
'prefix' => $this->field_prefix,
'allowTracking' => 'yes' === $allow_tracking,
),
'fields' => $this->fields,
);
wp_localize_script( 'wc-order-attribution', 'wc_order_attribution', $namespace );
@ -323,8 +324,8 @@ class OrderAttributionController implements RegisterHooksInterface {
* @return void
*/
private function output_origin_column( WC_Order $order ) {
$source_type = $order->get_meta( $this->get_meta_prefixed_field( 'type' ) );
$source = $order->get_meta( $this->get_meta_prefixed_field( 'utm_source' ) );
$source_type = $order->get_meta( $this->get_meta_prefixed_field_name( 'source_type' ) );
$source = $order->get_meta( $this->get_meta_prefixed_field_name( 'utm_source' ) );
$origin = $this->get_origin_label( $source_type, $source );
if ( empty( $origin ) ) {
$origin = __( 'Unknown', 'woocommerce' );
@ -333,11 +334,12 @@ class OrderAttributionController implements RegisterHooksInterface {
}
/**
* Add attribution hidden input fields for checkout & customer register froms.
* Add `<input type="hidden">` elements for source fields.
* Used for checkout & customer register froms.
*/
private function source_form_fields() {
foreach ( $this->fields as $field ) {
printf( '<input type="hidden" name="%s" value="" />', esc_attr( $this->get_prefixed_field( $field ) ) );
private function source_form_elements() {
foreach ( $this->field_names as $field_name ) {
printf( '<input type="hidden" name="%s" value="" />', esc_attr( $this->get_prefixed_field_name( $field_name ) ) );
}
}
@ -351,8 +353,8 @@ class OrderAttributionController implements RegisterHooksInterface {
private function set_customer_source_data( WC_Customer $customer ) {
// Nonce check is handled before user_register hook.
// phpcs:ignore WordPress.Security.NonceVerification
foreach ( $this->get_source_values( $this->get_unprefixed_fields( $_POST ) ) as $key => $value ) {
$customer->add_meta_data( $this->get_meta_prefixed_field( $key ), $value );
foreach ( $this->get_source_values( $this->get_unprefixed_field_values( $_POST ) ) as $key => $value ) {
$customer->add_meta_data( $this->get_meta_prefixed_field_name( $key ), $value );
}
$customer->save_meta_data();
@ -372,7 +374,7 @@ class OrderAttributionController implements RegisterHooksInterface {
return;
}
foreach ( $source_data as $key => $value ) {
$order->add_meta_data( $this->get_meta_prefixed_field( $key ), $value );
$order->add_meta_data( $this->get_meta_prefixed_field_name( $key ), $value );
}
$order->save_meta_data();
@ -414,7 +416,7 @@ class OrderAttributionController implements RegisterHooksInterface {
*/
private function send_order_tracks( array $source_data, WC_Order $order ) {
$origin_label = $this->get_origin_label(
$source_data['type'] ?? '',
$source_data['source_type'] ?? '',
$source_data['utm_source'] ?? '',
false
);
@ -422,7 +424,7 @@ class OrderAttributionController implements RegisterHooksInterface {
$customer_info = $this->get_customer_history( $customer_identifier );
$tracks_data = array(
'order_id' => $order->get_id(),
'type' => $source_data['type'] ?? '',
'source_type' => $source_data['source_type'] ?? '',
'medium' => $source_data['utm_medium'] ?? '',
'source' => $source_data['utm_source'] ?? '',
'device_type' => strtolower( $source_data['device_type'] ?? 'unknown' ),

View File

@ -19,31 +19,43 @@ use WP_Post;
*/
trait OrderAttributionMeta {
/** @var string[] */
/**
* The default fields and their sourcebuster accesors,
* to show in the source data metabox.
*
* @var string[]
* */
private $default_fields = array(
// main fields.
'type',
'url',
'source_type' => 'current.typ',
'referrer' => 'current_add.rf',
// utm fields.
'utm_campaign',
'utm_source',
'utm_medium',
'utm_content',
'utm_id',
'utm_term',
'utm_campaign' => 'current.cmp',
'utm_source' => 'current.src',
'utm_medium' => 'current.mdm',
'utm_content' => 'current.cnt',
'utm_id' => 'current.id',
'utm_term' => 'current.trm',
// additional fields.
'session_entry',
'session_start_time',
'session_pages',
'session_count',
'user_agent',
'session_entry' => 'current_add.ep',
'session_start_time' => 'current_add.fd',
'session_pages' => 'session.pgs',
'session_count' => 'udata.vst',
'user_agent' => 'udata.uag',
);
/** @var array */
private $fields = array();
/**
* Cached `array_keys( $fields )`.
*
* @var array
* */
private $field_names = array();
/** @var string */
private $field_prefix = '';
@ -67,7 +79,7 @@ trait OrderAttributionMeta {
}
/**
* Set the meta fields and the field prefix.
* Set the fields and the field prefix.
*
* @return void
*/
@ -79,7 +91,8 @@ trait OrderAttributionMeta {
*
* @param string[] $fields The fields to show.
*/
$this->fields = (array) apply_filters( 'wc_order_attribution_tracking_fields', $this->default_fields );
$this->fields = (array) apply_filters( 'wc_order_attribution_tracking_fields', $this->default_fields );
$this->field_names = array_keys( $this->fields );
$this->set_field_prefix();
}
@ -119,11 +132,11 @@ trait OrderAttributionMeta {
*/
private function filter_meta_data( array $meta ): array {
$return = array();
$prefix = $this->get_meta_prefixed_field( '' );
$prefix = $this->get_meta_prefixed_field_name( '' );
foreach ( $meta as $item ) {
if ( str_starts_with( $item->key, $prefix ) ) {
$return[ $this->unprefix_meta_field( $item->key ) ] = $item->value;
$return[ $this->unprefix_meta_field_name( $item->key ) ] = $item->value;
}
}
@ -133,7 +146,7 @@ trait OrderAttributionMeta {
}
// Determine the origin based on source type and referrer.
$source_type = $return['type'] ?? '';
$source_type = $return['source_type'] ?? '';
$source = $return['utm_source'] ?? '';
$return['origin'] = $this->get_origin_label( $source_type, $source, true );
@ -143,50 +156,34 @@ trait OrderAttributionMeta {
/**
* Get the field name with the appropriate prefix.
*
* @param string $field Field name.
* @param string $name Field name.
*
* @return string The prefixed field name.
*/
private function get_prefixed_field( $field ): string {
return "{$this->field_prefix}{$field}";
private function get_prefixed_field_name( $name ): string {
return "{$this->field_prefix}{$name}";
}
/**
* Get the field name with the meta prefix.
*
* @param string $field The field name.
* @param string $name The field name.
*
* @return string The prefixed field name.
*/
private function get_meta_prefixed_field( string $field ): string {
// Map some of the fields to the correct meta name.
if ( 'type' === $field ) {
$field = 'source_type';
} elseif ( 'url' === $field ) {
$field = 'referrer';
}
return "_{$this->get_prefixed_field( $field )}";
private function get_meta_prefixed_field_name( string $name ): string {
return "_{$this->get_prefixed_field_name( $name )}";
}
/**
* Remove the meta prefix from the field name.
*
* @param string $field The prefixed field.
* @param string $name The prefixed fieldname .
*
* @return string
*/
private function unprefix_meta_field( string $field ): string {
$return = str_replace( "_{$this->field_prefix}", '', $field );
// Map some of the fields to the correct meta name.
if ( 'source_type' === $return ) {
$return = 'type';
} elseif ( 'referrer' === $return ) {
$return = 'url';
}
return $return;
private function unprefix_meta_field_name( string $name ): string {
return str_replace( "_{$this->field_prefix}", '', $name );
}
/**
@ -218,19 +215,19 @@ trait OrderAttributionMeta {
/**
* Map posted, prefixed values to fields.
* Map posted, prefixed values to field values.
* Used for the classic forms.
*
* @param array $raw_values The raw values from the POST form.
*
* @return array
*/
private function get_unprefixed_fields( array $raw_values = array() ): array {
private function get_unprefixed_field_values( array $raw_values = array() ): array {
$values = array();
// Look through each field in POST data.
foreach ( $this->fields as $field ) {
$values[ $field ] = $raw_values[ $this->get_prefixed_field( $field ) ] ?? '(none)';
foreach ( $this->field_names as $field_name ) {
$values[ $field_name ] = $raw_values[ $this->get_prefixed_field_name( $field_name ) ] ?? '(none)';
}
return $values;
@ -247,13 +244,13 @@ trait OrderAttributionMeta {
$values = array();
// Look through each field in given data.
foreach ( $this->fields as $field ) {
$value = sanitize_text_field( wp_unslash( $raw_values[ $field ] ) );
foreach ( $this->field_names as $field_name ) {
$value = sanitize_text_field( wp_unslash( $raw_values[ $field_name ] ) );
if ( '(none)' === $value ) {
continue;
}
$values[ $field ] = $value;
$values[ $field_name ] = $value;
}
// Set the device type if possible using the user agent.
@ -361,13 +358,13 @@ trait OrderAttributionMeta {
/**
* Get the description for the order attribution field.
*
* @param string $field The field name.
* @param string $field_name The field name.
*
* @return string
*/
private function get_field_description( string $field ): string {
private function get_field_description( string $field_name ): string {
/* translators: %s is the field name */
$description = sprintf( __( 'Order attribution field: %s', 'woocommerce' ), $field );
$description = sprintf( __( 'Order attribution field: %s', 'woocommerce' ), $field_name );
/**
* Filter the description for the order attribution field.
@ -375,9 +372,9 @@ trait OrderAttributionMeta {
* @since 8.5.0
*
* @param string $description The description for the order attribution field.
* @param string $field The field name.
* @param string $field_name The field name.
*/
return (string) apply_filters( 'wc_order_attribution_field_description', $description, $field );
return (string) apply_filters( 'wc_order_attribution_field_description', $description, $field_name );
}
/**

View File

@ -6,7 +6,7 @@
*
* @see Automattic\WooCommerce\Internal\Orders\OrderAttributionController
* @package WooCommerce\Templates
* @version 8.4.0
* @version 8.6.0
*/
declare( strict_types=1 );
@ -47,10 +47,10 @@ defined( 'ABSPATH' ) || exit;
</div>
<div class="woocommerce-order-attribution-details-container closed">
<?php if ( array_key_exists( 'type', $meta ) ) : ?>
<?php if ( array_key_exists( 'source_type', $meta ) ) : ?>
<h4><?php esc_html_e( 'Source type', 'woocommerce' ); ?></h4>
<span class="order-attribution-source_type">
<?php echo esc_html( $meta['type'] ); ?>
<?php echo esc_html( $meta['source_type'] ); ?>
</span>
<?php endif; ?>

View File

@ -125,8 +125,8 @@ class OrderAttributionTest extends WP_UnitTestCase {
$this->assertEquals( '/', $args['meta']['utm_content'] ?? '' );
$this->assertEquals( 'referral', $args['meta']['utm_medium'] ?? '' );
$this->assertEquals( 'woocommerce.com', $args['meta']['utm_source'] ?? '' );
$this->assertEquals( 'https://woocommerce.com/', $args['meta']['url'] ?? '' );
$this->assertEquals( 'referral', $args['meta']['type'] ?? '' );
$this->assertEquals( 'https://woocommerce.com/', $args['meta']['referrer'] ?? '' );
$this->assertEquals( 'referral', $args['meta']['source_type'] ?? '' );
$this->assertTrue( $args['has_more_details'] );
},

View File

@ -24,7 +24,7 @@ class OrderAttributionControllerTest extends WP_UnitTestCase {
*
* @var OrderAttributionController
*/
protected OrderAttributionController $attribution_fields_class;
protected OrderAttributionController $attribution_class;
/**
* Sets up the fixture, for example, open a network connection.
@ -35,7 +35,7 @@ class OrderAttributionControllerTest extends WP_UnitTestCase {
*/
protected function setUp(): void {
parent::setUp();
$this->attribution_fields_class = new OrderAttributionController();
$this->attribution_class = new OrderAttributionController();
/** @var MockableLegacyProxy $legacy_proxy */
$legacy_proxy = wc_get_container()->get( LegacyProxy::class );
@ -55,7 +55,7 @@ class OrderAttributionControllerTest extends WP_UnitTestCase {
->onlyMethods( array( 'log' ) )
->getMock();
$this->attribution_fields_class->init( $legacy_proxy, $feature_mock, $wp_consent_mock, $logger_mock );
$this->attribution_class->init( $legacy_proxy, $feature_mock, $wp_consent_mock, $logger_mock );
}
/**
@ -98,8 +98,8 @@ class OrderAttributionControllerTest extends WP_UnitTestCase {
function( $order ) {
$this->output_origin_column( $order );
},
$this->attribution_fields_class,
$this->attribution_fields_class
$this->attribution_class,
$this->attribution_class
);
foreach ( $test_cases as $test_case ) {