Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display "Origin" column in Orders table in Orders Analytics #46424

Merged
merged 34 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a9d90f3
Display channel column in Orders Analytics UI.
ecgan Apr 10, 2024
1a48761
Set $data to false to not use cache.
ecgan Apr 11, 2024
70a490b
Get channel info from order meta and put into extended_info.
ecgan Apr 11, 2024
d54ba36
Set channel in order item level.
ecgan Apr 11, 2024
ffdef81
Replace channel with get_origin_label in extended_info.
ecgan Apr 12, 2024
2e26c5f
Remove unnneeded channel in orders_data.
ecgan Apr 12, 2024
494e684
Display origin instead of channel in table.
ecgan Apr 12, 2024
46c52cd
Fix lint errors.
ecgan Apr 12, 2024
aaca142
Query order meta table based on HPOS.
ecgan Apr 12, 2024
286f3bd
Remove code for development purpose.
ecgan Apr 12, 2024
0ee9a7a
Add changelog.
ecgan Apr 12, 2024
494b36c
Fix code comment.
ecgan Apr 12, 2024
f5a1557
Merge branch 'trunk' into add/orders-analytics-channel-column
ecgan Apr 15, 2024
ddda7cd
Guard against null values.
ecgan Apr 15, 2024
ff5f4c7
Set default origin label to "Unknown".
ecgan Apr 15, 2024
2ab21c6
Support server side report download.
ecgan Apr 15, 2024
930c23b
Fix failed test.
ecgan Apr 15, 2024
b98ddd1
Fix lint error.
ecgan Apr 16, 2024
d0911a0
Simplify code.
ecgan Apr 16, 2024
9851886
Change "channel" to "attribution" object in controller.
ecgan Apr 17, 2024
f3099a7
Change `origin` string to `attribution` array in Orders DataStore.
ecgan Apr 17, 2024
e1589b7
Change origin string to attribution object.
ecgan Apr 17, 2024
e496387
Fix indexing after changing from origin string to attribution object.
ecgan Apr 17, 2024
d03e47f
Change from origin string to attribution object in table.js.
ecgan Apr 17, 2024
9c82c3f
Simplify code.
ecgan Apr 17, 2024
a84b2fe
Fix lint errors.
ecgan Apr 17, 2024
73361f4
Fix failed test.
ecgan Apr 17, 2024
58ad4b2
Fix lint error.
ecgan Apr 17, 2024
713b653
Fix retrieving origin in CSV export.
ecgan Apr 17, 2024
cc3d0e0
Use $wpdb->postmeta; cosmetic change.
ecgan Apr 18, 2024
ad770ca
Cosmetic change.
ecgan Apr 18, 2024
c787b0c
Sanitize order IDs by using absint.
ecgan Apr 18, 2024
63eeaa6
Merge branch 'trunk' into add/orders-analytics-channel-column
ecgan Apr 18, 2024
79eda32
Merge branch 'trunk' into add/orders-analytics-channel-column
ecgan Apr 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions plugins/woocommerce-admin/client/analytics/report/orders/table.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ class OrdersReportTable extends Component {
isSortable: true,
isNumeric: true,
},
{
label: __( 'Origin', 'woocommerce' ),
screenReaderLabel: __( 'Origin', 'woocommerce' ),
key: 'origin',
required: false,
isSortable: false,
},
];
}

Expand Down Expand Up @@ -241,6 +248,10 @@ class OrdersReportTable extends Component {
display: renderCurrency( netTotal, currency ),
value: netTotal,
},
{
display: extendedInfo.origin,
value: extendedInfo.origin,
},
];
} );
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Add origin column in Orders Analytics report.
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@ public function get_item_schema() {
'context' => array( 'view', 'edit' ),
'description' => __( 'Order customer information.', 'woocommerce' ),
),
'channel' => array(
'type' => 'object',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Order channel information.', 'woocommerce' ),
),
ecgan marked this conversation as resolved.
Show resolved Hide resolved
),
),
);
Expand Down Expand Up @@ -526,6 +532,7 @@ public function get_export_columns() {
'num_items_sold' => __( 'Items sold', 'woocommerce' ),
'coupons' => __( 'Coupon(s)', 'woocommerce' ),
'net_total' => __( 'N. Revenue', 'woocommerce' ),
'origin' => __( 'Origin', 'woocommerce' ),
);

/**
Expand Down Expand Up @@ -558,6 +565,7 @@ public function prepare_item_for_export( $item ) {
'num_items_sold' => $item['num_items_sold'],
'coupons' => isset( $item['extended_info']['coupons'] ) ? $this->get_coupons( $item['extended_info']['coupons'] ) : null,
'net_total' => $item['net_total'],
'origin' => $item['extended_info']['origin'],
);

/**
Expand Down
68 changes: 61 additions & 7 deletions plugins/woocommerce/src/Admin/API/Reports/Orders/DataStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Internal\Traits\OrderAttributionMeta;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Utilities\OrderUtil;
use Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore;
use Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface;
use Automattic\WooCommerce\Admin\API\Reports\SqlQuery;
Expand All @@ -18,6 +21,7 @@
* API\Reports\Orders\DataStore.
*/
class DataStore extends ReportsDataStore implements DataStoreInterface {
use OrderAttributionMeta;

/**
* Dynamically sets the date column name based on configuration
Expand Down Expand Up @@ -338,13 +342,14 @@ protected function normalize_order_by( $order_by ) {
* @param array $query_args Query parameters.
*/
protected function include_extended_info( &$orders_data, $query_args ) {
$mapped_orders = $this->map_array_by_key( $orders_data, 'order_id' );
$related_orders = $this->get_orders_with_parent_id( $mapped_orders );
$order_ids = array_merge( array_keys( $mapped_orders ), array_keys( $related_orders ) );
$products = $this->get_products_by_order_ids( $order_ids );
$coupons = $this->get_coupons_by_order_ids( array_keys( $mapped_orders ) );
$customers = $this->get_customers_by_orders( $orders_data );
$mapped_customers = $this->map_array_by_key( $customers, 'customer_id' );
$mapped_orders = $this->map_array_by_key( $orders_data, 'order_id' );
$related_orders = $this->get_orders_with_parent_id( $mapped_orders );
$order_ids = array_merge( array_keys( $mapped_orders ), array_keys( $related_orders ) );
$products = $this->get_products_by_order_ids( $order_ids );
$coupons = $this->get_coupons_by_order_ids( array_keys( $mapped_orders ) );
$order_attributions = $this->get_order_attributions_by_order_ids( array_keys( $mapped_orders ) );
$customers = $this->get_customers_by_orders( $orders_data );
$mapped_customers = $this->map_array_by_key( $customers, 'customer_id' );

$mapped_data = array();
foreach ( $products as $product ) {
Expand Down Expand Up @@ -398,11 +403,14 @@ protected function include_extended_info( &$orders_data, $query_args ) {
'products' => array(),
'coupons' => array(),
'customer' => array(),
'origin' => '',
ecgan marked this conversation as resolved.
Show resolved Hide resolved
);
$orders_data[ $key ]['extended_info'] = isset( $mapped_data[ $order_data['order_id'] ] ) ? array_merge( $defaults, $mapped_data[ $order_data['order_id'] ] ) : $defaults;
if ( $order_data['customer_id'] && isset( $mapped_customers[ $order_data['customer_id'] ] ) ) {
$orders_data[ $key ]['extended_info']['customer'] = $mapped_customers[ $order_data['customer_id'] ];
}

$orders_data[ $key ]['extended_info']['origin'] = $this->get_origin_label( $order_attributions[ $order_data['order_id'] ]['_wc_order_attribution_source_type'] ?? '', $order_attributions[ $order_data['order_id'] ]['_wc_order_attribution_utm_source'] ?? '' );
ecgan marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -534,6 +542,52 @@ protected function get_coupons_by_order_ids( $order_ids ) {
return $coupons;
}

/**
* Get order attributions data from order IDs.
*
* @param array $order_ids Array of order IDs.
* @return array
*/
protected function get_order_attributions_by_order_ids( $order_ids ) {
global $wpdb;
$order_meta_table = OrdersTableDataStore::get_meta_table_name();
$included_order_ids = implode( ',', $order_ids );
ecgan marked this conversation as resolved.
Show resolved Hide resolved

if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
$order_attributions_meta = $wpdb->get_results(
"SELECT order_id, meta_key, meta_value
FROM $order_meta_table
WHERE order_id IN ({$included_order_ids})
AND meta_key IN ('_wc_order_attribution_source_type', '_wc_order_attribution_utm_source')
",
ARRAY_A
);
ecgan marked this conversation as resolved.
Show resolved Hide resolved
/* phpcs:enable */
} else {
/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
$order_attributions_meta = $wpdb->get_results(
"SELECT post_id as order_id, meta_key, meta_value
FROM {$wpdb->prefix}postmeta
WHERE post_id IN ({$included_order_ids})
AND meta_key IN ('_wc_order_attribution_source_type', '_wc_order_attribution_utm_source')
",
ARRAY_A
);
ecgan marked this conversation as resolved.
Show resolved Hide resolved
/* phpcs:enable */
}

$order_attributions = array();
foreach ( $order_attributions_meta as $meta ) {
if ( ! isset( $order_attributions[ $meta['order_id'] ] ) ) {
$order_attributions[ $meta['order_id'] ] = array();
}
$order_attributions[ $meta['order_id'] ][ $meta['meta_key'] ] = $meta['meta_value'];
}

return $order_attributions;
}
Copy link
Contributor

@ibndawood ibndawood Apr 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📜

I took quite a bit of time to analyse this approach. I was inclined to use wc_get_orders instead of direct SQL. The advantages of using wc_get_orders are that we don't have to worry about HPOS compatibility, we have the WC_Order object giving us more information to work with, and our code is a bit cleaner.

I also reached out to @vedanshujain and asked his opinion. He said both approaches are acceptable.

I did a simple benchmark test using microtime, and I can see using direct SQL is slightly faster than wc_get_orders. In the context of analytics, I am inclined towards faster results than cleaner code. Let's move forward with this approach and we can tweak it if required at a later stage.

What are your thoughts?

cc: @vedanshujain

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was inclined to use wc_get_orders instead of direct SQL. [...]

Yes, I thought about using wc_get_orders too (reference: https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query), and I was also thinking about HPOS compatibility.

However, there were two things that I thought about, that I ended up using direct SQL.

One, I'm not sure if wc_get_orders might give us too much data ("fat" orders) and cause us issues in production. In our development environment, it's usually pretty simple small order objects; but I'm concerned that in the real world, merchants might be using some plugins or extensions that stores a lot of metadata into an order object. If we use wc_get_orders, it would be the first time we retrieve orders object in this Orders Analytics page. Even if the users choose page size 25 (the smallest page size), I'm concerned that it might cause performance issue or "out of memory" issue etc. In other words, I would like to be safe, and use direct SQL to retrieve just the data we need quickly.

Two, I see that we already have existing prior art of using direct SQL in populating other extended_info properties (product, customer, and coupon), so I just follow the existing way.

I did a simple benchmark test using microtime [...]

Just sharing an info that I found in https://www.php.net/manual/en/function.microtime.php: "For performance measurements, using hrtime() is recommended."

But no worry, I guess using microtime is fine as a rough gauge.

[...] and I can see using direct SQL is slightly faster than wc_get_orders.

Related to my comments above about "fat" orders. Using direct SQL is slightly faster in our development environment. I'm thinking that in the real world merchant's WooCommerce store production environment, it may be significantly faster compared to using wc_get_orders.


/**
* Get all statuses that have been synced.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public function test_extended_info() {
),
'coupons' => array(),
'customer' => $data->data[0]['extended_info']['customer'], // Not under test.
'origin' => 'Unknown',
ecgan marked this conversation as resolved.
Show resolved Hide resolved
),
),
),
Expand Down