From 1fe62b7bdf7370e38f73c8cb433a3e1babfc9caf Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:42:05 +0100 Subject: [PATCH 01/15] Added: funnels and goals provisioning. --- src/Admin/Provisioning.php | 16 ++- src/Admin/Provisioning/Integrations.php | 102 ++++++++++++++++++ src/Admin/Provisioning/Integrations/EDD.php | 78 ++++++++++++++ .../Provisioning/Integrations/WooCommerce.php | 81 ++++++++++++++ src/Integrations.php | 12 +-- src/Integrations/EDD.php | 37 +++++++ src/Plugin.php | 2 +- 7 files changed, 311 insertions(+), 17 deletions(-) create mode 100644 src/Admin/Provisioning/Integrations.php create mode 100644 src/Admin/Provisioning/Integrations/EDD.php create mode 100644 src/Admin/Provisioning/Integrations/WooCommerce.php create mode 100644 src/Integrations/EDD.php diff --git a/src/Admin/Provisioning.php b/src/Admin/Provisioning.php index 95c3709..5798345 100644 --- a/src/Admin/Provisioning.php +++ b/src/Admin/Provisioning.php @@ -20,14 +20,14 @@ class Provisioning { /** - * @var ClientFactory + * @var Client $client */ - private $client_factory; + public $client; /** - * @var Client $client + * @var ClientFactory */ - private $client; + private $client_factory; /** * @var string[] $custom_event_goals @@ -90,9 +90,7 @@ private function init() { add_action( 'update_option_plausible_analytics_settings', [ $this, 'create_shared_link' ], 10, 2 ); add_action( 'update_option_plausible_analytics_settings', [ $this, 'maybe_create_goals' ], 10, 2 ); - add_action( 'update_option_plausible_analytics_settings', [ $this, 'maybe_create_woocommerce_funnel' ], 10, 2 ); add_action( 'update_option_plausible_analytics_settings', [ $this, 'maybe_delete_goals' ], 11, 2 ); - add_action( 'update_option_plausible_analytics_settings', [ $this, 'maybe_delete_woocommerce_goals' ], 11, 2 ); add_action( 'update_option_plausible_analytics_settings', [ $this, 'maybe_create_custom_properties' ], 11, 2 ); } @@ -179,7 +177,7 @@ public function create_goal_request( $name, $type = 'CustomEvent', $currency = ' * * @return void */ - private function create_goals( $goals ) { + public function create_goals( $goals ) { if ( empty( $goals ) ) { return; // @codeCoverageIgnore } @@ -253,7 +251,7 @@ public function maybe_create_woocommerce_funnel( $old_settings, $settings ) { * @return void * @codeCoverageIgnore Because this method should be mocked in tests if needed. */ - private function create_funnel( $name, $steps ) { + public function create_funnel( $name, $steps ) { $create_request = new Client\Model\FunnelCreateRequest( [ 'funnel' => [ @@ -365,7 +363,7 @@ public function maybe_delete_woocommerce_goals( $old_settings, $settings ) { * @return false|mixed * @codeCoverageIgnore Because it can't be unit tested. */ - private function array_search_contains( $string, $haystack ) { + public function array_search_contains( $string, $haystack ) { if ( preg_match( '/\([A-Z]*?\)/', $string ) ) { $string = preg_replace( '/ \([A-Z]*?\)/', '', $string ); } diff --git a/src/Admin/Provisioning/Integrations.php b/src/Admin/Provisioning/Integrations.php new file mode 100644 index 0000000..8868c82 --- /dev/null +++ b/src/Admin/Provisioning/Integrations.php @@ -0,0 +1,102 @@ +init(); + } + + /** + * Action & filter hooks. + * + * @return void + * @codeCoverageIgnore This is merely a wrapper to load classes. No need to test. + */ + private function init() { + new Integrations\WooCommerce( $this ); + new Integrations\EDD( $this ); + } + + /** + * @param array $event_goals + * @param string $funnel_name + * + * @return void + * @codeCoverageIgnore We don't want to test the API. + */ + public function create_integration_funnel( $event_goals, $funnel_name ) { + $goals = []; + + foreach ( $event_goals as $event_key => $event_goal ) { + // Don't add this goal to the funnel. Create it separately instead. + if ( $event_key === 'remove-from-cart' ) { + $this->create_goals( [ $this->create_goal_request( $event_goal ) ] ); + + continue; + } + + if ( $event_key === 'purchase' ) { + if ( \Plausible\Analytics\WP\Integrations::is_edd_active() ) { + $currency = edd_get_currency(); + } else { + $currency = get_woocommerce_currency(); + } + + $goals[] = $this->create_goal_request( $event_goal, 'Revenue', $currency ); + + continue; + } + + if ( $event_key === 'view-product' ) { + $goals[] = $this->create_goal_request( $event_goal, 'Pageview', null, '/product*' ); + + continue; + } + + $goals[] = $this->create_goal_request( $event_goal ); + } + + $this->create_funnel( $funnel_name, $goals ); + } + + /** + * Deletes the integration-specific goals using the stored goal IDs. + * + * @param object $integration The integration object containing event goals to be deleted. + * + * @return void + * @codeCoverageIgnore Because we don't want to test the API. + */ + public function delete_integration_goals( $integration ) { + $goals = get_option( 'plausible_analytics_enhanced_measurements_goal_ids', [] ); + + foreach ( $goals as $id => $name ) { + $key = $this->array_search_contains( $name, $integration->event_goals ); + + if ( $key ) { + $this->client->delete_goal( $id ); + + unset( $goals[ $id ] ); + } + } + + // Refresh the stored IDs in the DB. + update_option( 'plausible_analytics_enhanced_measurements_goal_ids', $goals ); + } +} diff --git a/src/Admin/Provisioning/Integrations/EDD.php b/src/Admin/Provisioning/Integrations/EDD.php new file mode 100644 index 0000000..ccd1dbb --- /dev/null +++ b/src/Admin/Provisioning/Integrations/EDD.php @@ -0,0 +1,78 @@ +integrations = $integrations; + + $this->init(); + } + + /** + * Action and filter hooks. + * + * @return void + */ + private function init() { + add_action( 'update_option_plausible_analytics_settings', [ $this, 'maybe_create_edd_funnel' ], 10, 2 ); + add_action( 'update_option_plausible_analytics_settings', [ $this, 'maybe_delete_edd_goals' ], 11, 2 ); + } + + /** + * Creates an EDD purchase funnel if enhanced measurement is enabled and EDD is active. + * + * @param array $old_settings The previous settings before the update. + * @param array $settings The updated settings array. + * + * @return void + */ + public function maybe_create_edd_funnel( $old_settings, $settings ) { + if ( ! Helpers::is_enhanced_measurement_enabled( 'revenue', $settings[ 'enhanced_measurements' ] ) || ! Integrations::is_edd_active() ) { + return; // @codeCoverageIgnore + } + + $edd = new Integrations\EDD( false ); + + $this->integrations->create_integration_funnel( $edd->event_goals, __( 'EDD Purchase Funnel', 'plausible-analytics' ) ); + } + + /** + * * Delete all custom EDD event goals if Revenue setting is disabled. The funnel is deleted when the minimum + * * required no. of goals is no longer met. + * + * @param array $old_settings The previous settings before the update. + * @param array $settings The current updated settings. + * + * @return void + */ + public function maybe_delete_edd_goals( $old_settings, $settings ) { + $enhanced_measurements = array_filter( $settings[ 'enhanced_measurements' ] ); + + if ( Helpers::is_enhanced_measurement_enabled( 'revenue', $enhanced_measurements ) ) { + return; + } + + $edd_integration = new Integrations\EDD( false ); + + $this->integrations->delete_integration_goals( $edd_integration ); + } +} diff --git a/src/Admin/Provisioning/Integrations/WooCommerce.php b/src/Admin/Provisioning/Integrations/WooCommerce.php new file mode 100644 index 0000000..9b6596c --- /dev/null +++ b/src/Admin/Provisioning/Integrations/WooCommerce.php @@ -0,0 +1,81 @@ +integrations = $integrations; + + $this->init(); + } + + /** + * Action & filters hooks. + * + * @return void + */ + private function init() { + add_action( 'update_option_plausible_analytics_settings', [ $this, 'maybe_create_woocommerce_funnel' ], 10, 2 ); + add_action( 'update_option_plausible_analytics_settings', [ $this, 'maybe_delete_woocommerce_goals' ], 11, 2 ); + } + + /** + * Checks whether the WooCommerce funnel should be created based on the provided settings + * and creates the funnel if the conditions are met. + * + * @param array $old_settings The previous settings before the update. + * @param array $settings The updated settings to check for enhanced measurement and WooCommerce integration. + * + * @return void + */ + public function maybe_create_woocommerce_funnel( $old_settings, $settings ) { + if ( ! Helpers::is_enhanced_measurement_enabled( 'revenue', $settings[ 'enhanced_measurements' ] ) || ! Integrations::is_wc_active() ) { + return; // @codeCoverageIgnore + } + + $woocommerce = new Integrations\WooCommerce( false ); + + $this->integrations->create_integration_funnel( $woocommerce->event_goals, __( 'Woo Purchase Funnel', 'plausible-analytics' ) ); + } + + /** + * Delete all custom WooCommerce event goals if Revenue setting is disabled. The funnel is deleted when the minimum + * required no. of goals is no longer met. + * + * @param $old_settings + * @param $settings + * + * @return void + * @codeCoverageIgnore Because we don't want to test if the API is working. + */ + public function maybe_delete_woocommerce_goals( $old_settings, $settings ) { + $enhanced_measurements = array_filter( $settings[ 'enhanced_measurements' ] ); + + // Setting is enabled, no need to continue. + if ( Helpers::is_enhanced_measurement_enabled( 'revenue', $enhanced_measurements ) ) { + return; + } + + $woo_integration = new Integrations\WooCommerce( false ); + + $this->integrations->delete_integration_goals( $woo_integration ); + } +} diff --git a/src/Integrations.php b/src/Integrations.php index 0e2c035..fafabea 100644 --- a/src/Integrations.php +++ b/src/Integrations.php @@ -31,9 +31,10 @@ private function init() { // Easy Digital Downloads if ( self::is_edd_active() ) { - // new Integrations\EDD(); + new Integrations\EDD(); } + // Form Plugins if ( self::is_form_submit_active() ) { new Integrations\FormSubmit(); } @@ -44,7 +45,7 @@ private function init() { * @return bool */ public static function is_wc_active() { - return apply_filters( 'plausible_analytics_integrations_woocommerce', function_exists( 'WC' ) && Helpers::is_enhanced_measurement_enabled( 'revenue' ) ); + return apply_filters( 'plausible_analytics_integrations_woocommerce', defined( 'WC_PLUGIN_FILE' ) && Helpers::is_enhanced_measurement_enabled( 'revenue' ) ); } /** @@ -52,7 +53,7 @@ public static function is_wc_active() { * @return bool */ public static function is_edd_active() { - return apply_filters( 'plausible_analytics_integrations_edd', function_exists( 'EDD' ) && Helpers::is_enhanced_measurement_enabled( 'revenue' ) ); + return apply_filters( 'plausible_analytics_integrations_edd', defined( 'EDD_PLUGIN_FILE' ) && Helpers::is_enhanced_measurement_enabled( 'revenue' ) ); } /** @@ -60,9 +61,6 @@ public static function is_edd_active() { * @return mixed|null */ public static function is_form_submit_active() { - return apply_filters( - 'plausible_analytics_integrations_form_submit', - Helpers::is_enhanced_measurement_enabled( 'form-completions' ) - ); + return apply_filters( 'plausible_analytics_integrations_form_submit', Helpers::is_enhanced_measurement_enabled( 'form-completions' ) ); } } diff --git a/src/Integrations/EDD.php b/src/Integrations/EDD.php new file mode 100644 index 0000000..262453d --- /dev/null +++ b/src/Integrations/EDD.php @@ -0,0 +1,37 @@ +event_goals = [ + 'view-product' => __( 'Visit /product*', 'plausible-analytics' ), + 'add-to-cart' => __( 'EDD Add to Cart', 'plausible-analytics' ), + 'remove-from-cart' => __( 'EDD Remove from Cart', 'plausible-analytics' ), + 'checkout' => __( 'EDD Start Checkout', 'plausible-analytics' ), + 'purchase' => __( 'EDD Complete Purchase', 'plausible-analytics' ), + ]; + + $this->init( $init ); + } + + private function init( $init ) { + if ( ! $init ) { + return; + } + + // Add to Cart + // Remove from Cart + // Entered Checkout + // Track Purchase + } +} diff --git a/src/Plugin.php b/src/Plugin.php index 84f87d5..c6ac048 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -37,7 +37,7 @@ public function register_services() { new Admin\Filters(); new Admin\Actions(); new Admin\Module(); - new Admin\Provisioning(); + new Admin\Provisioning\Integrations(); } new Integrations(); From 61cb8e11e05e23a5e4787c1b2a2be1e9ad4df663 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:43:02 +0100 Subject: [PATCH 02/15] PHPDoc. --- src/Admin/Provisioning/Integrations.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Admin/Provisioning/Integrations.php b/src/Admin/Provisioning/Integrations.php index 8868c82..d47f8f1 100644 --- a/src/Admin/Provisioning/Integrations.php +++ b/src/Admin/Provisioning/Integrations.php @@ -25,6 +25,8 @@ public function __construct() { /** * Action & filter hooks. * + * We use Dependency Injection to prevent circular dependency. + * * @return void * @codeCoverageIgnore This is merely a wrapper to load classes. No need to test. */ From 650de2e330b2afb32c2b5a222e297bf0bae2036c Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:50:56 +0100 Subject: [PATCH 03/15] DI suits better here. --- src/Admin/Provisioning/Integrations.php | 27 +++++++++++++++---------- src/Plugin.php | 1 + 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Admin/Provisioning/Integrations.php b/src/Admin/Provisioning/Integrations.php index d47f8f1..ead36df 100644 --- a/src/Admin/Provisioning/Integrations.php +++ b/src/Admin/Provisioning/Integrations.php @@ -9,15 +9,20 @@ namespace Plausible\Analytics\WP\Admin\Provisioning; use Plausible\Analytics\WP\Admin\Provisioning; -use Plausible\Analytics\WP\Client\ApiException; -class Integrations extends Provisioning { +class Integrations { + /** + * @var Provisioning + */ + private $provisioning; + /** * Build class. - * @throws ApiException + * + * We use DI to prevent circular dependency. */ public function __construct() { - parent::__construct(); + $this->provisioning = new Provisioning(); $this->init(); } @@ -48,7 +53,7 @@ public function create_integration_funnel( $event_goals, $funnel_name ) { foreach ( $event_goals as $event_key => $event_goal ) { // Don't add this goal to the funnel. Create it separately instead. if ( $event_key === 'remove-from-cart' ) { - $this->create_goals( [ $this->create_goal_request( $event_goal ) ] ); + $this->provisioning->create_goals( [ $this->provisioning->create_goal_request( $event_goal ) ] ); continue; } @@ -60,21 +65,21 @@ public function create_integration_funnel( $event_goals, $funnel_name ) { $currency = get_woocommerce_currency(); } - $goals[] = $this->create_goal_request( $event_goal, 'Revenue', $currency ); + $goals[] = $this->provisioning->create_goal_request( $event_goal, 'Revenue', $currency ); continue; } if ( $event_key === 'view-product' ) { - $goals[] = $this->create_goal_request( $event_goal, 'Pageview', null, '/product*' ); + $goals[] = $this->provisioning->create_goal_request( $event_goal, 'Pageview', null, '/product*' ); continue; } - $goals[] = $this->create_goal_request( $event_goal ); + $goals[] = $this->provisioning->create_goal_request( $event_goal ); } - $this->create_funnel( $funnel_name, $goals ); + $this->provisioning->create_funnel( $funnel_name, $goals ); } /** @@ -89,10 +94,10 @@ public function delete_integration_goals( $integration ) { $goals = get_option( 'plausible_analytics_enhanced_measurements_goal_ids', [] ); foreach ( $goals as $id => $name ) { - $key = $this->array_search_contains( $name, $integration->event_goals ); + $key = $this->provisioning->array_search_contains( $name, $integration->event_goals ); if ( $key ) { - $this->client->delete_goal( $id ); + $this->provisioning->client->delete_goal( $id ); unset( $goals[ $id ] ); } diff --git a/src/Plugin.php b/src/Plugin.php index c6ac048..2bbaa97 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -37,6 +37,7 @@ public function register_services() { new Admin\Filters(); new Admin\Actions(); new Admin\Module(); + new Admin\Provisioning(); new Admin\Provisioning\Integrations(); } From 2af1031179441b75d160c5ef69c80cb042864255 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Thu, 20 Feb 2025 17:21:35 +0100 Subject: [PATCH 04/15] Added: Custom Properties provisioning and Track Add To Cart event for EDD. --- src/Admin/Provisioning.php | 22 ++++++++++-- src/Integrations/EDD.php | 57 ++++++++++++++++++++++++++++++-- src/Integrations/WooCommerce.php | 21 ++---------- 3 files changed, 77 insertions(+), 23 deletions(-) diff --git a/src/Admin/Provisioning.php b/src/Admin/Provisioning.php index 5798345..6d395c1 100644 --- a/src/Admin/Provisioning.php +++ b/src/Admin/Provisioning.php @@ -19,6 +19,24 @@ use Plausible\Analytics\WP\Integrations\WooCommerce; class Provisioning { + const CUSTOM_PROPERTIES = [ + 'cart_total', + 'cart_total_items', + 'id', + 'name', + 'price', + 'product_id', + 'product_name', + 'quantity', + 'shipping', + 'subtotal', + 'subtotal_tax', + 'tax_class', + 'total', + 'total_tax', + 'variation_id', + ]; + /** * @var Client $client */ @@ -408,8 +426,8 @@ public function maybe_create_custom_properties( $old_settings, $settings ) { /** * Create Custom Properties for WooCommerce integration. */ - if ( Helpers::is_enhanced_measurement_enabled( 'revenue', $enhanced_measurements ) && Integrations::is_wc_active() ) { - foreach ( WooCommerce::CUSTOM_PROPERTIES as $property ) { + if ( Helpers::is_enhanced_measurement_enabled( 'revenue', $enhanced_measurements ) && ( Integrations::is_wc_active() || Integrations::is_edd_active() ) ) { + foreach ( self::CUSTOM_PROPERTIES as $property ) { $properties[] = new Client\Model\CustomProp( [ 'custom_prop' => [ 'key' => $property ] ] ); } } diff --git a/src/Integrations/EDD.php b/src/Integrations/EDD.php index 262453d..be918de 100644 --- a/src/Integrations/EDD.php +++ b/src/Integrations/EDD.php @@ -1,6 +1,6 @@ ID === 0 ) { + return; + } + + $quantity = array_filter( + $items, + function ( $item ) use ( $download_id ) { + return $item[ 'id' ] === $download_id; + } + ); + $quantity = reset( $quantity )[ 'quantity' ] ?? 1; + + $props = apply_filters( + 'plausible_analytics_edd_add_to_cart_custom_properties', + [ + 'product_name' => edd_get_download_name( $download_id ), + 'product_id' => $download_id, + 'quantity' => $quantity, + 'price' => edd_get_download_price( $download_id ), + 'tax_class' => edd_get_cart_tax_rate(), + 'cart_total_items' => edd_get_cart_quantity(), + 'cart_total' => edd_get_cart_total(), + ] + ); + + $proxy = new Proxy( false ); + + $proxy->do_request( $this->event_goals[ 'add-to-cart' ], null, null, $props ); + } } diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index b72eecb..a493673 100644 --- a/src/Integrations/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -9,6 +9,7 @@ namespace Plausible\Analytics\WP\Integrations; +use Plausible\Analytics\WP\Admin\Provisioning; use Plausible\Analytics\WP\Integrations; use Plausible\Analytics\WP\Proxy; use WC_Cart; @@ -17,24 +18,6 @@ class WooCommerce { const PURCHASE_TRACKED_META_KEY = '_plausible_analytics_purchase_tracked'; - const CUSTOM_PROPERTIES = [ - 'cart_total', - 'cart_total_items', - 'id', - 'name', - 'price', - 'product_id', - 'product_name', - 'quantity', - 'shipping', - 'subtotal', - 'subtotal_tax', - 'tax_class', - 'total', - 'total_tax', - 'variation_id', - ]; - /** * @var array Custom Event Goals used to track Events in WooCommerce. */ @@ -244,7 +227,7 @@ public function track_add_to_cart( $product, $add_to_cart_data ) { */ private function clean_data( $product ) { foreach ( $product as $key => $value ) { - if ( ! in_array( $key, self::CUSTOM_PROPERTIES ) ) { + if ( ! in_array( $key, Provisioning::CUSTOM_PROPERTIES ) ) { unset( $product[ $key ] ); } } From 4b28424daee6e5125f16a3f287eb732a671b35b5 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:06:41 +0100 Subject: [PATCH 05/15] Added: Remove from Cart, Entered Checkout and Track Purchase events. --- src/Integrations/EDD.php | 141 ++++++++++++++++++++++++++----- src/Integrations/WooCommerce.php | 6 ++ 2 files changed, 126 insertions(+), 21 deletions(-) diff --git a/src/Integrations/EDD.php b/src/Integrations/EDD.php index be918de..a7d52ac 100644 --- a/src/Integrations/EDD.php +++ b/src/Integrations/EDD.php @@ -11,27 +11,17 @@ use Plausible\Analytics\WP\Proxy; +/** + * @codeCoverageIgnore Because all we'd be testing is the (external) API. + */ class EDD { - const CUSTOM_PROPERTIES = [ - 'cart_total', - 'cart_total_items', - 'id', - 'name', - 'price', - 'product_id', - 'product_name', - 'quantity', - 'shipping', - 'subtotal', - 'subtotal_tax', - 'tax_class', - 'total', - 'total_tax', - 'variation_id', - ]; - public $event_goals = []; + /** + * Build class. + * + * @param $init + */ public function __construct( $init = true ) { $this->event_goals = [ 'view-product' => __( 'Visit /product*', 'plausible-analytics' ), @@ -44,17 +34,33 @@ public function __construct( $init = true ) { $this->init( $init ); } + /** + * Action & filter hooks. + * + * @param $init + * + * @return void + */ private function init( $init ) { if ( ! $init ) { return; } add_action( 'edd_post_add_to_cart', [ $this, 'track_add_to_cart' ], 10, 3 ); - // Remove from Cart - // Entered Checkout - // Track Purchase + add_action( 'edd_pre_remove_from_cart', [ $this, 'track_remove_cart_item' ], 10 ); + add_action( 'edd_before_purchase_form', [ $this, 'track_entered_checkout' ] ); + add_action( 'edd_complete_purchase', [ $this, 'track_purchase' ], 10, 2 ); } + /** + * Tracks the "add to cart" event with relevant product and cart data. + * + * @param int $download_id The ID of the product being added to the cart. + * @param array $options Optional data associated with the product being added. + * @param array $items The current items in the cart. + * + * @return void + */ public function track_add_to_cart( $download_id, $options, $items ) { $download = new \EDD_Download( $download_id ); @@ -87,4 +93,97 @@ function ( $item ) use ( $download_id ) { $proxy->do_request( $this->event_goals[ 'add-to-cart' ], null, null, $props ); } + + /** + * Tracks the removal of an item from the cart, updates cart contents and triggers an event to log this action. + * + * @param string|int $key The key of the item in the cart to be removed. + * + * @return void + */ + public function track_remove_cart_item( $key ) { + $cart_contents = edd_get_cart_contents(); + $item_removed_from_cart = $cart_contents[ $key ] ?? []; + $product = null; + + if ( empty( $item_removed_from_cart ) ) { + return; + } + + unset( $cart_contents[ $key ] ); + + if ( isset( $item_removed_from_cart[ 'id' ] ) ) { + $product = new \EDD_Download( $item_removed_from_cart[ 'id' ] ); + } + + if ( ! $product ) { + return; + } + + $total_removed_from_cart = edd_get_cart_total() - ( $product->get_price() * $item_removed_from_cart[ 'quantity' ] ); + + $props = apply_filters( + 'plausible_analytics_edd_remove_cart_item_custom_properties', + [ + 'product_name' => $product->get_name(), + 'product_id' => $item_removed_from_cart[ 'id' ], + 'quantity' => $item_removed_from_cart[ 'quantity' ], + 'cart_total_items' => count( $cart_contents ), + 'cart_total' => $total_removed_from_cart, + ] + ); + + $proxy = new Proxy( false ); + + $proxy->do_request( $this->event_goals[ 'remove-from-cart' ], null, null, $props ); + } + + /** + * Tracks the "entered checkout" event with relevant cart data. + * + * @return void + */ + public function track_entered_checkout() { + // Just to make sure we're where we're supposed to be. + if ( ! edd_is_checkout() ) { + return; + } + + $props = apply_filters( + 'plausible_analytics_edd_entered_checkout_custom_properties', + [ + 'subtotal' => edd_get_cart_subtotal(), + 'tax' => edd_get_cart_tax(), + 'total' => edd_get_cart_total(), + ] + ); + + $proxy = new Proxy( false ); + + $proxy->do_request( $this->event_goals[ 'checkout' ], null, null, $props ); + } + + /** + * Tracks the "purchase" event with relevant order and payment data. + * + * @param int $order_id The unique identifier of the order. + * @param object $payment The payment object containing details like total amount and currency. + * + * @return void + */ + public function track_purchase( $order_id, $payment ) { + $props = apply_filters( + 'plausible_analytics_edd_purchase_custom_properties', + [ + 'revenue' => [ + 'amount' => $payment->total, + 'currency' => $payment->currency, + ], + ] + ); + + $proxy = new Proxy( false ); + + $proxy->do_request( $this->event_goals[ 'purchase' ], null, null, $props ); + } } diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index a493673..decf21c 100644 --- a/src/Integrations/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -275,6 +275,12 @@ public function track_remove_cart_item( $cart_item_key, $cart ) { } /** + * Tracks when a user enters the checkout process and sends event data to Plausible Analytics. + * + * This method checks if the current page is the checkout page. If it is, it collects relevant + * cart information like subtotal, shipping, tax, and total, applies filters to allow custom properties, + * encodes the data as JSON, and generates a JavaScript snippet to send the data to Plausible Analytics. + * * @return void */ public function track_entered_checkout() { From 838fc5d3ccd89c2af8e77f27808c6e5f49747ed0 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:16:54 +0100 Subject: [PATCH 06/15] Keep in mind that base can be changed in EDD using the EDD_SLUG constant. --- src/Integrations/EDD.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Integrations/EDD.php b/src/Integrations/EDD.php index a7d52ac..ee46fad 100644 --- a/src/Integrations/EDD.php +++ b/src/Integrations/EDD.php @@ -24,7 +24,7 @@ class EDD { */ public function __construct( $init = true ) { $this->event_goals = [ - 'view-product' => __( 'Visit /product*', 'plausible-analytics' ), + 'view-product' => sprintf( __( 'Visit /%s*', 'plausible-analytics' ), defined( 'EDD_SLUG' ) ? EDD_SLUG : 'downloads' ), 'add-to-cart' => __( 'EDD Add to Cart', 'plausible-analytics' ), 'remove-from-cart' => __( 'EDD Remove from Cart', 'plausible-analytics' ), 'checkout' => __( 'EDD Start Checkout', 'plausible-analytics' ), @@ -176,7 +176,7 @@ public function track_purchase( $order_id, $payment ) { 'plausible_analytics_edd_purchase_custom_properties', [ 'revenue' => [ - 'amount' => $payment->total, + 'amount' => number_format_i18n( $payment->total, 2 ), 'currency' => $payment->currency, ], ] From a397939bebf0a9f2f0721e67c7576af271f25fda Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:40:20 +0100 Subject: [PATCH 07/15] Fixed: WC integration didn't work properly in multisite environments and environments with non-default permalink structures. --- src/Admin/Provisioning/Integrations.php | 3 ++- src/Integrations/WooCommerce.php | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Admin/Provisioning/Integrations.php b/src/Admin/Provisioning/Integrations.php index ead36df..7ec94e8 100644 --- a/src/Admin/Provisioning/Integrations.php +++ b/src/Admin/Provisioning/Integrations.php @@ -71,7 +71,8 @@ public function create_integration_funnel( $event_goals, $funnel_name ) { } if ( $event_key === 'view-product' ) { - $goals[] = $this->provisioning->create_goal_request( $event_goal, 'Pageview', null, '/product*' ); + $path = preg_replace( '/^.*?\//', '', $event_goal ); + $goals[] = $this->provisioning->create_goal_request( $event_goal, 'Pageview', null, '/' . $path ); continue; } diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index decf21c..5bc7149 100644 --- a/src/Integrations/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -29,8 +29,16 @@ class WooCommerce { * @codeCoverageIgnore */ public function __construct( $init = true ) { + $uri = wc_get_permalink_structure()[ 'product_base' ]; + + if ( is_multisite() ) { + $uri = get_blog_details()->path . $uri; + } else { + $uri = '/' . $uri; + } + $this->event_goals = [ - 'view-product' => __( 'Visit /product*', 'plausible-analytics' ), + 'view-product' => sprintf( __( 'Visit %s*', 'plausible-analytics' ), $uri ), 'add-to-cart' => __( 'Woo Add to Cart', 'plausible-analytics' ), 'remove-from-cart' => __( 'Woo Remove from Cart', 'plausible-analytics' ), 'checkout' => __( 'Woo Start Checkout', 'plausible-analytics' ), From 56064d9524226387dc8ffbe310b8e30a6dc02a82 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:40:42 +0100 Subject: [PATCH 08/15] Fixed: EDD integration didn't work in multisite environments with subdirectories. --- src/Integrations/EDD.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Integrations/EDD.php b/src/Integrations/EDD.php index ee46fad..0e338b2 100644 --- a/src/Integrations/EDD.php +++ b/src/Integrations/EDD.php @@ -23,8 +23,16 @@ class EDD { * @param $init */ public function __construct( $init = true ) { + $uri = defined( 'EDD_SLUG' ) ? EDD_SLUG : 'downloads'; + + if ( is_multisite() ) { + $uri = get_blog_details()->path . $uri; + } else { + $uri = '/' . $uri; + } + $this->event_goals = [ - 'view-product' => sprintf( __( 'Visit /%s*', 'plausible-analytics' ), defined( 'EDD_SLUG' ) ? EDD_SLUG : 'downloads' ), + 'view-product' => sprintf( __( 'Visit %s*', 'plausible-analytics' ), $uri ), 'add-to-cart' => __( 'EDD Add to Cart', 'plausible-analytics' ), 'remove-from-cart' => __( 'EDD Remove from Cart', 'plausible-analytics' ), 'checkout' => __( 'EDD Start Checkout', 'plausible-analytics' ), From 3f5cbf3a5012870fbf1c4b5a9bb0b3e19b81a587 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:56:33 +0100 Subject: [PATCH 09/15] Format amount to two decimals for track_purchase() --- src/Integrations/EDD.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Integrations/EDD.php b/src/Integrations/EDD.php index 0e338b2..c0c9d77 100644 --- a/src/Integrations/EDD.php +++ b/src/Integrations/EDD.php @@ -184,7 +184,7 @@ public function track_purchase( $order_id, $payment ) { 'plausible_analytics_edd_purchase_custom_properties', [ 'revenue' => [ - 'amount' => number_format_i18n( $payment->total, 2 ), + 'amount' => number_format( (float) $payment->total, 2 ), 'currency' => $payment->currency, ], ] From 024aeeaf89adb086f7e67e5389abfc242a779606 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:53:06 +0100 Subject: [PATCH 10/15] Fixed: Revenue tracking didn't work in EDD integration. --- src/Proxy.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Proxy.php b/src/Proxy.php index dd8b8ca..79e6476 100644 --- a/src/Proxy.php +++ b/src/Proxy.php @@ -80,10 +80,7 @@ private function init( $init ) { $settings = []; - if ( array_key_exists( 'option_name', $_POST ) && - $_POST[ 'option_name' ] == 'proxy_enabled' && - array_key_exists( 'option_value', $_POST ) && - $_POST[ 'option_value' ] == 'on' ) { + if ( array_key_exists( 'option_name', $_POST ) && $_POST[ 'option_name' ] == 'proxy_enabled' && array_key_exists( 'option_value', $_POST ) && $_POST[ 'option_value' ] == 'on' ) { $settings[ 'proxy_enabled' ] = 'on'; // @codeCoverageIgnore } @@ -115,7 +112,10 @@ public function do_request( $name = 'pageview', $domain = '', $url = '', $props 'u' => $url ?: wp_get_referer(), ]; - if ( ! empty( $props ) ) { + // Revenue events use a different approach. + if ( isset( $props[ 'revenue' ] ) ) { + $body[ 'revenue' ] = reset( $props ); + } elseif ( ! empty( $props ) ) { $body[ 'p' ] = $props; // @codeCoverageIgnore } From 567ab44958b237e8c4c4bcc1742912aaf1e4af13 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:57:54 +0100 Subject: [PATCH 11/15] Switched back is_edd_ and is_wc_active methods. --- src/Integrations.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Integrations.php b/src/Integrations.php index fafabea..11992a7 100644 --- a/src/Integrations.php +++ b/src/Integrations.php @@ -45,7 +45,7 @@ private function init() { * @return bool */ public static function is_wc_active() { - return apply_filters( 'plausible_analytics_integrations_woocommerce', defined( 'WC_PLUGIN_FILE' ) && Helpers::is_enhanced_measurement_enabled( 'revenue' ) ); + return apply_filters( 'plausible_analytics_integrations_woocommerce', function_exists( 'WC' ) && Helpers::is_enhanced_measurement_enabled( 'revenue' ) ); } /** @@ -53,7 +53,7 @@ public static function is_wc_active() { * @return bool */ public static function is_edd_active() { - return apply_filters( 'plausible_analytics_integrations_edd', defined( 'EDD_PLUGIN_FILE' ) && Helpers::is_enhanced_measurement_enabled( 'revenue' ) ); + return apply_filters( 'plausible_analytics_integrations_edd', function_exists( 'EDD' ) && Helpers::is_enhanced_measurement_enabled( 'revenue' ) ); } /** From 8a45337316eae674dfee6d982bf69bee5ec357b7 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:05:14 +0100 Subject: [PATCH 12/15] Fixed: WooCommerceTest --- tests/integration/Integrations/WooCommerceTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integration/Integrations/WooCommerceTest.php b/tests/integration/Integrations/WooCommerceTest.php index b7c3ca7..ab1b8bd 100644 --- a/tests/integration/Integrations/WooCommerceTest.php +++ b/tests/integration/Integrations/WooCommerceTest.php @@ -18,6 +18,7 @@ class WooCommerceTest extends TestCase { */ public function testTrackEnteredCheckout() { when( 'is_checkout' )->justReturn( true ); + when( 'wc_get_permalink_structure' )->justReturn( [ 'product_base' => 'product' ] ); $cart_mock = $this->getMockBuilder( 'WC_Cart' )->setMethods( [ @@ -49,6 +50,8 @@ public function testTrackEnteredCheckout() { * @return void */ public function testTrackPurchase() { + when( 'wc_get_permalink_structure' )->justReturn( [ 'product_base' => 'product' ] ); + $class = new WooCommerce( false ); $mock = $this->getMockBuilder( 'WC_Order' )->setMethods( [ From 86f86a2f6c0b1d31f6f76a2c72f66999cfe44dce Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:12:41 +0100 Subject: [PATCH 13/15] Fixed: IntegrationsTest --- src/Proxy.php | 2 +- tests/integration/IntegrationsTest.php | 18 ++++++------------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Proxy.php b/src/Proxy.php index 79e6476..dd1c7dd 100644 --- a/src/Proxy.php +++ b/src/Proxy.php @@ -114,7 +114,7 @@ public function do_request( $name = 'pageview', $domain = '', $url = '', $props // Revenue events use a different approach. if ( isset( $props[ 'revenue' ] ) ) { - $body[ 'revenue' ] = reset( $props ); + $body[ 'revenue' ] = reset( $props ); // @codeCoverageIgnore } elseif ( ! empty( $props ) ) { $body[ 'p' ] = $props; // @codeCoverageIgnore } diff --git a/tests/integration/IntegrationsTest.php b/tests/integration/IntegrationsTest.php index f1ce43d..77b9f38 100644 --- a/tests/integration/IntegrationsTest.php +++ b/tests/integration/IntegrationsTest.php @@ -19,15 +19,12 @@ class IntegrationsTest extends TestCase { * and finally removes the applied filter. */ public function testIsWcActive() { - add_filter( 'plausible_analytics_settings', [ $this, 'enableRevenue' ] ); + add_filter( 'plausible_analytics_integrations_woocommerce', '__return_true' ); // WC is already mocked. $this->assertTrue( Integrations::is_wc_active() ); - remove_filter( - 'plausible_analytics_settings', - [ $this, 'enableRevenue' ] - ); + remove_filter( 'plausible_analytics_integrations_woocommerce', '__return_true' ); } /** @@ -38,14 +35,11 @@ public function testIsWcActive() { * state of the EDD integration. It then removes the applied filter after testing. */ public function testIsEddActive() { - add_filter( 'plausible_analytics_settings', [ $this, 'enableRevenue' ] ); - - $edd_mock = $this->getMockBuilder( 'Easy_Digital_Downloads' )->getMock(); - when( 'EDD' )->justReturn( $edd_mock ); + add_filter( 'plausible_analytics_integrations_edd', '__return_true' ); $this->assertTrue( Integrations::is_edd_active() ); - remove_filter( 'plausible_analytics_settings', [ $this, 'enableRevenue' ] ); + remove_filter( 'plausible_analytics_integrations_edd', '__return_true' ); } /** @@ -56,10 +50,10 @@ public function testIsEddActive() { * and then removes the applied filter. */ public function isFormSubmitActive() { - add_filter( 'plausible_analytics_settings', [ $this, 'enableFormCompletions' ] ); + add_filter( 'plausible_analytics_integrations_form_submit', '__return_true' ); $this->assertTrue( Integrations::is_form_submit_active() ); - remove_filter( 'plausible_analytics_settings', [ $this, 'enableFormCompletions' ] ); + remove_filter( 'plausible_analytics_integrations_form_submit', '__return_true' ); } } From c201c89e003115f514e6207192f0378ae2184ae6 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:15:07 +0100 Subject: [PATCH 14/15] Ignore the integration methods, because all they would test is the Plugins API. --- src/Admin/Provisioning/Integrations/EDD.php | 4 ++++ src/Admin/Provisioning/Integrations/WooCommerce.php | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/Admin/Provisioning/Integrations/EDD.php b/src/Admin/Provisioning/Integrations/EDD.php index ccd1dbb..20fac0c 100644 --- a/src/Admin/Provisioning/Integrations/EDD.php +++ b/src/Admin/Provisioning/Integrations/EDD.php @@ -44,6 +44,8 @@ private function init() { * @param array $settings The updated settings array. * * @return void + * + * @codeCoverageIgnore Because it interacts with the Plugins API */ public function maybe_create_edd_funnel( $old_settings, $settings ) { if ( ! Helpers::is_enhanced_measurement_enabled( 'revenue', $settings[ 'enhanced_measurements' ] ) || ! Integrations::is_edd_active() ) { @@ -63,6 +65,8 @@ public function maybe_create_edd_funnel( $old_settings, $settings ) { * @param array $settings The current updated settings. * * @return void + * + * @codeCoverageIgnore Because it interacts with the Plugins API. */ public function maybe_delete_edd_goals( $old_settings, $settings ) { $enhanced_measurements = array_filter( $settings[ 'enhanced_measurements' ] ); diff --git a/src/Admin/Provisioning/Integrations/WooCommerce.php b/src/Admin/Provisioning/Integrations/WooCommerce.php index 9b6596c..caa1de7 100644 --- a/src/Admin/Provisioning/Integrations/WooCommerce.php +++ b/src/Admin/Provisioning/Integrations/WooCommerce.php @@ -45,6 +45,8 @@ private function init() { * @param array $settings The updated settings to check for enhanced measurement and WooCommerce integration. * * @return void + * + * @codeCoverageIgnore Because it interacts with the Plugins API. */ public function maybe_create_woocommerce_funnel( $old_settings, $settings ) { if ( ! Helpers::is_enhanced_measurement_enabled( 'revenue', $settings[ 'enhanced_measurements' ] ) || ! Integrations::is_wc_active() ) { @@ -64,6 +66,7 @@ public function maybe_create_woocommerce_funnel( $old_settings, $settings ) { * @param $settings * * @return void + * * @codeCoverageIgnore Because we don't want to test if the API is working. */ public function maybe_delete_woocommerce_goals( $old_settings, $settings ) { From 85b6c8fb4204e3402169113eb04a5e98ace51245 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:21:42 +0100 Subject: [PATCH 15/15] Added: testInit --- tests/integration/IntegrationsTest.php | 31 +++++++++++++++----------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/integration/IntegrationsTest.php b/tests/integration/IntegrationsTest.php index 77b9f38..6873e23 100644 --- a/tests/integration/IntegrationsTest.php +++ b/tests/integration/IntegrationsTest.php @@ -10,13 +10,26 @@ use function Brain\Monkey\Functions\when; class IntegrationsTest extends TestCase { + public function testInit() { + add_filter( 'plausible_analytics_integrations_woocommerce', '__return_true' ); + add_filter( 'plausible_analytics_integrations_edd', '__return_true' ); + add_filter( 'plausible_analytics_integrations_form_submit', '__return_true' ); + + when( 'wc_get_permalink_structure' )->justReturn( [ 'product_base' => 'product' ] ); + + new Integrations(); + + remove_filter( 'plausible_analytics_integrations_woocommerce', '__return_true' ); + remove_filter( 'plausible_analytics_integrations_edd', '__return_true' ); + remove_filter( 'plausible_analytics_integrations_form_submit', '__return_true' ); + + $this->assertTrue( class_exists( '\Plausible\Analytics\WP\Integrations\WooCommerce' ) ); + $this->assertTrue( class_exists( '\Plausible\Analytics\WP\Integrations\EDD' ) ); + $this->assertTrue( class_exists( '\Plausible\Analytics\WP\Integrations\FormSubmit' ) ); + } + /** * Tests whether the WooCommerce integration is currently active. - * - * This method temporarily applies a filter to enable revenue tracking functionality, - * mocks the function_exists call to simulate the existence of WooCommerce functions, - * verifies the active state of the WooCommerce integration through an assertion, - * and finally removes the applied filter. */ public function testIsWcActive() { add_filter( 'plausible_analytics_integrations_woocommerce', '__return_true' ); @@ -29,10 +42,6 @@ public function testIsWcActive() { /** * Tests if the Easy Digital Downloads (EDD) integration is active. - * - * This method applies a temporary filter to enable revenue tracking, mocks - * the existence of required functions using a stub, and asserts the active - * state of the EDD integration. It then removes the applied filter after testing. */ public function testIsEddActive() { add_filter( 'plausible_analytics_integrations_edd', '__return_true' ); @@ -44,10 +53,6 @@ public function testIsEddActive() { /** * Determines if the form submission functionality is currently active. - * - * This method temporarily applies a filter to enable form completions, - * verifies the active state of the form submission through an assertion, - * and then removes the applied filter. */ public function isFormSubmitActive() { add_filter( 'plausible_analytics_integrations_form_submit', '__return_true' );