[Product Collection] Track block instances and feature usage (#46466)
* add block parser and filters wip * replace strings with template slugs and do some polish * Refactor some parts to reused in wc_tracker * Clean up and introduce snapshot data in the WC_Tracker * Minor improvements * Track only fse themes * Consolidate collection names per context in the WC_Tracker snapshot * Add changelog * Separate taxonomy filter usage into specific category and tag usage * Polish event params values * Lint * Remove wc-tracker to simplify the pr * More cleanup * Unify location names * Handle stock status default value * Renamed order confirm context * Track price filter fix * consider 3pd stock statuses in tracking * Lint * Consider the woocommerce_hide_out_of_stock_items option when tracking stock filter * Cleanup empty lines * Rename single product area flag * Add docblock * Avoid drafts and autosaves * introduce a new class under tracking events * Cleanup main controller * Cleanup * Add docblocks * Stop recursive process up to 2 levels and cleanup * Cleanup * Ensure blocks exist * Lint * Normalize keys and values * Track inherit and order-by usage * Fix lint * Add strict types * add strict again * Strict types * Move strict types * Remove codeblock * Clean up * Move directive on separate line * Cleanup spaces
This commit is contained in:
parent
7d71e2235a
commit
a4ff1d01b3
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Product Collection: add tracking for block usage
|
|
@ -209,6 +209,7 @@ class WC_Site_Tracking {
|
|||
include_once WC_ABSPATH . 'includes/tracks/events/class-wc-order-tracking.php';
|
||||
include_once WC_ABSPATH . 'includes/tracks/events/class-wc-coupon-tracking.php';
|
||||
include_once WC_ABSPATH . 'includes/tracks/events/class-wc-theme-tracking.php';
|
||||
include_once WC_ABSPATH . 'includes/tracks/events/class-wc-product-collection-block-tracking.php';
|
||||
|
||||
$tracking_classes = array(
|
||||
'WC_Extensions_Tracking',
|
||||
|
@ -221,6 +222,7 @@ class WC_Site_Tracking {
|
|||
'WC_Order_Tracking',
|
||||
'WC_Coupon_Tracking',
|
||||
'WC_Theme_Tracking',
|
||||
'WC_Product_Collection_Block_Tracking',
|
||||
);
|
||||
|
||||
foreach ( $tracking_classes as $tracking_class ) {
|
||||
|
|
|
@ -0,0 +1,320 @@
|
|||
<?php
|
||||
declare( strict_types = 1);
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplate;
|
||||
use Automattic\WooCommerce\Blocks\Templates\CartTemplate;
|
||||
use Automattic\WooCommerce\Blocks\Templates\MiniCartTemplate;
|
||||
use Automattic\WooCommerce\Blocks\Templates\CheckoutTemplate;
|
||||
use Automattic\WooCommerce\Blocks\Templates\ProductCatalogTemplate;
|
||||
use Automattic\WooCommerce\Blocks\Templates\ProductAttributeTemplate;
|
||||
use Automattic\WooCommerce\Blocks\Templates\OrderConfirmationTemplate;
|
||||
|
||||
/**
|
||||
* This class adds actions to track usage of the Product Collection Block.
|
||||
*/
|
||||
class WC_Product_Collection_Block_Tracking {
|
||||
|
||||
/**
|
||||
* Init Tracking.
|
||||
*/
|
||||
public function init() {
|
||||
add_action( 'save_post', array( $this, 'track_collection_instances' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Track feature usage of the Product Collection block within the site editor.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param \WP_Post $post The post object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function track_collection_instances( $post_id, $post ) {
|
||||
|
||||
if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST || ! wc_current_theme_is_fse_theme() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $post instanceof \WP_Post ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't track autosaves and drafts.
|
||||
$post_status = $post->post_status;
|
||||
if ( 'publish' !== $post_status ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Important: Only track instances within specific types.
|
||||
$post_type = $post->post_type;
|
||||
if ( ! in_array( $post_type, array( 'post', 'page', 'wp_template', 'wp_template_part', 'wp_block' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! has_block( 'woocommerce/product-collection', $post ) && ! has_block( 'core/template-part', $post ) && ! has_block( 'core/block', $post ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$blocks = parse_blocks( $post->post_content );
|
||||
if ( empty( $blocks ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$instances = $this->parse_blocks_track_data( $blocks );
|
||||
if ( empty( $instances ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Count orders.
|
||||
// Hint: Product count included in Track event. See WC_Tracks::get_blog_details().
|
||||
$order_count = 0;
|
||||
foreach ( wc_get_order_statuses() as $status_slug => $status_name ) {
|
||||
$order_count += wc_orders_count( $status_slug );
|
||||
}
|
||||
$additional_data = array(
|
||||
'editor_context' => $this->parse_editor_location_context( $post ),
|
||||
'order_count' => $order_count,
|
||||
);
|
||||
|
||||
foreach ( $instances as $instance ) {
|
||||
|
||||
$event_properties = array_merge(
|
||||
$additional_data,
|
||||
$instance
|
||||
);
|
||||
|
||||
\WC_Tracks::record_event(
|
||||
'product_collection_instance',
|
||||
$event_properties
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track usage of the Product Collection block within the given blocks.
|
||||
*
|
||||
* @param array $blocks The parsed blocks to check.
|
||||
* @param bool $is_in_single_product Whether we are in a single product container (used for keeping state in the recurring process).
|
||||
* @param bool $is_in_template_part Whether we are in a template part (used for keeping state in the recurring process).
|
||||
* @param bool $is_in_synced_pattern Whether we are in a synced block (used for keeping state in the recurring process).
|
||||
*
|
||||
* @return array Parsed instances of the Product Collection block.
|
||||
*/
|
||||
private function parse_blocks_track_data( $blocks, $is_in_single_product = false, $is_in_template_part = false, $is_in_synced_pattern = false ) {
|
||||
|
||||
$instances = array();
|
||||
|
||||
if ( ! is_array( $blocks ) || empty( $blocks ) ) {
|
||||
return $instances;
|
||||
}
|
||||
|
||||
foreach ( $blocks as $block ) {
|
||||
|
||||
if ( empty( $block['blockName'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( 'woocommerce/product-collection' === $block['blockName'] ) {
|
||||
$instances[] = array(
|
||||
'collection' => $block['attrs']['collection'] ?? 'product-catalog',
|
||||
'in_single_product' => $is_in_single_product ? 'yes' : 'no',
|
||||
'in_template_part' => $is_in_template_part ? 'yes' : 'no',
|
||||
'in_synced_pattern' => $is_in_synced_pattern ? 'yes' : 'no',
|
||||
'filters' => $this->get_query_filters_usage_data( $block ),
|
||||
);
|
||||
}
|
||||
|
||||
// Track instances within single product container.
|
||||
$local_is_in_single_product = $is_in_single_product;
|
||||
if ( 'woocommerce/single-product' === $block['blockName'] ) {
|
||||
$local_is_in_single_product = true;
|
||||
}
|
||||
|
||||
// Track instances within template part.
|
||||
// Hint: Supports up to two levels of depth.
|
||||
if ( ! $is_in_synced_pattern && ! $is_in_template_part && 'core/template-part' === $block['blockName'] ) {
|
||||
|
||||
$template_part_theme = $block['attrs']['theme'] ?? '';
|
||||
$template_part_slug = $block['attrs']['slug'] ?? '';
|
||||
$template_part = get_block_template( $template_part_theme . '//' . $template_part_slug, 'wp_template_part' );
|
||||
if ( $template_part instanceof WP_Block_Template && ! empty( $template_part->content ) ) {
|
||||
// Recursive.
|
||||
$instances = array_merge( $instances, $this->parse_blocks_track_data( parse_blocks( $template_part->content ), $local_is_in_single_product, true, $is_in_synced_pattern ) );
|
||||
}
|
||||
}
|
||||
|
||||
// Track instances within synced block.
|
||||
// Hint: Supports up to two levels of depth.
|
||||
if ( ! $is_in_synced_pattern && ! $is_in_template_part && 'core/block' === $block['blockName'] ) {
|
||||
|
||||
$block_id = $block['attrs']['ref'] ?? 0;
|
||||
$synced_pattern = get_post( $block_id );
|
||||
if ( $synced_pattern instanceof WP_Post && ! empty( $synced_pattern->post_content ) ) {
|
||||
// Recursive.
|
||||
$instances = array_merge( $instances, $this->parse_blocks_track_data( parse_blocks( $synced_pattern->post_content ), $local_is_in_single_product, $is_in_template_part, true ) );
|
||||
}
|
||||
}
|
||||
|
||||
// Recursive.
|
||||
if ( ! empty( $block['innerBlocks'] ) ) {
|
||||
$instances = array_merge( $instances, $this->parse_blocks_track_data( $block['innerBlocks'], $local_is_in_single_product, $is_in_template_part, $is_in_synced_pattern ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse editor's location context from WP Post.
|
||||
*
|
||||
* Possible contexts:
|
||||
* - post
|
||||
* - page
|
||||
* - single-product
|
||||
* - product-archive
|
||||
* - cart
|
||||
* - checkout
|
||||
* - product-catalog
|
||||
* - order-confirmation
|
||||
*
|
||||
* @param WP_Post $post The Post instance.
|
||||
*
|
||||
* @return string Returns the context.
|
||||
*/
|
||||
private function parse_editor_location_context( $post ) {
|
||||
$context = 'other';
|
||||
|
||||
if ( ! $post instanceof \WP_Post ) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
$post_type = $post->post_type;
|
||||
if ( ! in_array( $post_type, array( 'post', 'page', 'wp_template', 'wp_template_part', 'wp_block' ), true ) ) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
if ( 'wp_template' === $post_type ) {
|
||||
$name = $post->post_name;
|
||||
if ( false !== strpos( $name, SingleProductTemplate::SLUG ) ) {
|
||||
$context = 'single-product';
|
||||
} elseif ( ProductAttributeTemplate::SLUG === $name ) {
|
||||
$context = 'product-archive';
|
||||
} elseif ( false !== strpos( $name, 'taxonomy-' ) ) { // Including the '-' in the check to avoid false positives.
|
||||
$taxonomy = str_replace( 'taxonomy-', '', $name );
|
||||
$product_taxonomies = get_object_taxonomies( 'product', 'names' );
|
||||
if ( in_array( $taxonomy, $product_taxonomies, true ) ) {
|
||||
$context = 'product-archive';
|
||||
}
|
||||
} elseif ( in_array( $name, array( CartTemplate::SLUG, MiniCartTemplate::SLUG ), true ) ) {
|
||||
$context = 'cart';
|
||||
} elseif ( CheckoutTemplate::SLUG === $name ) {
|
||||
$context = 'checkout';
|
||||
} elseif ( ProductCatalogTemplate::SLUG === $name ) {
|
||||
$context = 'product-catalog';
|
||||
} elseif ( OrderConfirmationTemplate::SLUG === $name ) {
|
||||
$context = 'order-confirmation';
|
||||
}
|
||||
}
|
||||
|
||||
if ( in_array( $post_type, array( 'wp_block', 'wp_template_part' ), true ) ) {
|
||||
$context = 'isolated';
|
||||
}
|
||||
|
||||
if ( 'page' === $post_type ) {
|
||||
$context = 'page';
|
||||
}
|
||||
if ( 'post' === $post_type ) {
|
||||
$context = 'post';
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the collection query filters from the query attributes.
|
||||
*
|
||||
* @param array $block The parsed block.
|
||||
* @return array The filters data for tracking.
|
||||
*/
|
||||
private function get_query_filters_usage_data( $block ) {
|
||||
|
||||
if ( ! isset( $block['attrs'] ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$query_attrs = $block['attrs']['query'] ?? array();
|
||||
$filters = array(
|
||||
'inherit' => 'no',
|
||||
'order-by' => 'no',
|
||||
'on-sale' => 'no',
|
||||
'stock-status' => 'no',
|
||||
'handpicked' => 'no',
|
||||
'keyword' => 'no',
|
||||
'attributes' => 'no',
|
||||
'category' => 'no',
|
||||
'tag' => 'no',
|
||||
'featured' => 'no',
|
||||
'created' => 'no',
|
||||
'price' => 'no',
|
||||
);
|
||||
|
||||
if ( ! empty( $query_attrs['inherit'] ) && true === $query_attrs['inherit'] ) {
|
||||
$filters['inherit'] = 'yes';
|
||||
}
|
||||
|
||||
if ( ( ! empty( $query_attrs['order'] ) && 'asc' !== $query_attrs['order'] ) || ( ! empty( $query_attrs['orderBy'] ) && 'title' !== $query_attrs['orderBy'] ) ) {
|
||||
$filters['order-by'] = 'yes';
|
||||
}
|
||||
|
||||
if ( ! empty( $query_attrs['woocommerceOnSale'] ) ) {
|
||||
$filters['on-sale'] = 'yes';
|
||||
}
|
||||
|
||||
if ( ! empty( $query_attrs['woocommerceStockStatus'] ) ) {
|
||||
$stock_statuses = wc_get_product_stock_status_options();
|
||||
$default_values = 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ? array_diff_key( $stock_statuses, array( 'outofstock' => '' ) ) : $stock_statuses;
|
||||
$default_diff = array_diff( array_keys( $default_values ), $query_attrs['woocommerceStockStatus'] );
|
||||
if ( ! empty( $default_diff ) ) {
|
||||
$filters['stock-status'] = 'yes';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $query_attrs['woocommerceAttributes'] ) ) {
|
||||
$filters['attributes'] = 'yes';
|
||||
}
|
||||
|
||||
if ( ! empty( $query_attrs['timeFrame'] ) ) {
|
||||
$filters['created'] = 'yes';
|
||||
}
|
||||
|
||||
if ( ! empty( $query_attrs['taxQuery'] ) ) {
|
||||
|
||||
if ( ! empty( $query_attrs['taxQuery']['product_cat'] ) ) {
|
||||
$filters['category'] = 'yes';
|
||||
}
|
||||
|
||||
if ( ! empty( $query_attrs['taxQuery']['product_tag'] ) ) {
|
||||
$filters['tag'] = 'yes';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $query_attrs['woocommerceHandPickedProducts'] ) ) {
|
||||
$filters['handpicked'] = 'yes';
|
||||
}
|
||||
|
||||
if ( ! empty( $query_attrs['search'] ) ) {
|
||||
$filters['keyword'] = 'yes';
|
||||
}
|
||||
|
||||
if ( ! empty( $query_attrs['featured'] ) ) {
|
||||
$filters['featured'] = 'yes';
|
||||
}
|
||||
|
||||
if ( ! empty( $query_attrs['priceRange'] ) ) {
|
||||
$filters['price'] = 'yes';
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue